Pie menu implementation

Hi monkeys :chimpanzee_amused:
I implemented a pie menu in JME. It acts rather as a selection wheel where you can select your different weapons, spells etc. as in games like Dishonored or Splinter Cell: Blacklist. I didn’t use any GUI system for it as I wanted it to be independent of such a system and just use the GUI node instead. Here you can see a short video demonstrating the selection process:

What does it do?

The code lets you create a pie menu by specifying images for the menu itself and the nodes. It manages all the selection for you (the selection is dependent of the direction of the mouse movement) and scales everything correctly. It also aligns the nodes properly in a circle with a constant angle between them. When opening and closing some animations are also played but you will have to modify some of the code to change them or to change the highlighting of a selected item and so on. The code is designed to be extended however.

Two modes are supported: The one in the video where the inner part rotates and shows the current angle of the mouse movement and another one where there is a static image in the center and the user only knows which node is selected as it is highlighted.

Sample code

package mygame;

import com.jme3.app.FlyCamAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.math.ColorRGBA;
import com.jme3.renderer.RenderManager;
import java.util.ArrayList;
import java.util.List;

public class Main extends SimpleApplication implements ActionListener {

    private PieMenuState pms;

    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        flyCam.setEnabled(false);
        stateManager.detach(stateManager.getState(FlyCamAppState.class));

        inputManager.addMapping("TAB", new KeyTrigger(KeyInput.KEY_TAB));
        inputManager.addListener(this, "TAB");

        initPieMenu();

        viewPort.setBackgroundColor(ColorRGBA.White);
        inputManager.setCursorVisible(false);
    }

    private void initPieMenu() {
        pms = new PieMenuState(assetManager, inputManager, cam, guiNode);
        pms.setRotatingWheel(true);  //True: mode where picture rotates
        pms.setOuterWheelImage("Textures/outerWheel.png"); //Only used when rotatingWheel. Outer part of the wheel (circle)
        pms.setInnerWheelImage("Textures/innerWheel.png"); //Only used when rotatingWheel. Inner (rotating) part

        pms.setWheelImage("Textures/wheel.png");  //Only used when not rotatingWheel. Whole wheel image (with the inside)
        pms.setNumNodes(3);  //The number of selectable items
        pms.setRadius(280);  // The radius of the center of the screen to the center of the items in pixels
        pms.setNodeSize(100); //Size of the items in pixels
        pms.setDefaultCamHeight(720);  //Radius and size are proportional to this value. When the cameraHeight equals defaultCamHeight, the radius is the same as specified in setRadius. Otherwise it will be scaled accordingly.
        pms.setAnimSpeed(4);  //Speed of show and hide anim
        List<String> paths = new ArrayList<>();
        paths.add("Textures/flame.png");
        paths.add("Textures/snowflake.png");
        paths.add("Textures/lightning.png");
        pms.setTextures(paths);  //Textures (in order)
        pms.setNames("Fire", "Ice", "Lightning"); //Names - needed to get the selected Node
    }

    @Override
    public void simpleUpdate(float tpf) {
    }

    @Override
    public void simpleRender(RenderManager rm) {
    }

    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        if (name.equals("TAB")) {
            if (isPressed) {
                stateManager.attach(pms);
                pms.show();
            } else {
                stateManager.detach(pms);
                System.out.println("Selected item: " + pms.getSelectedItemName());
            }
        }
    }
}

As for the defaultCamHeight part (I didn’t want to explain it all in the comment):
The radius is in pixels so it has to be different when the game is in Fullscreen vs 1280x720 for example. So you can set all your values for 1280x720 (nodeSize, radius, etc.) via trial and error until everything looks good. Then you set the DefaultCamHeight to 720 and the AppState will scale everything up or down when the camHeight is different.

All the classes and code + the sample code above can be found at:

8 Likes