Better Gamepad Support

I’ve been looking for something gamepad related like a getAButton() method that, when called, would return me a Trigger (JoyAxisTrigger or JoyButtonTrigger) corresponding to the button located at the position of the A button on a Xbox gamepad – which would be the cross button on a PlayStation controller or the B button on a SNES controller. As many of you know, this is exactly what SDL GameControllerDB is meant for.

I couldn’t find anything like that on jME, so I created my own class. It works, but I have that itchy feeling that I’m reinventing the wheel, so I decided to come here and ask. Is there anything like that in the engine?

This is the class I created:

public class JoystickInput {

    private final Logger logger = Logger.getLogger(JoystickInput.class.getName());

    private final int id;

    private final String guid;

    private final Map<String, Trigger> joyMapping;

    public JoystickInput(final int id) {
        this.id = id;
        this.guid = GLFW.glfwGetJoystickGUID(id);
        this.joyMapping = new HashMap<>();
    }

    public int getId() {
        return id;
    }

    public String getGUID() {
        return guid;
    }

    public Trigger getA() {
        return joyMapping.get("a");
    }

    public Trigger getB() {
        return joyMapping.get("b");
    }

    public Trigger getX() {
        return joyMapping.get("x");
    }

    public Trigger getY() {
        return joyMapping.get("y");
    }

    public Trigger getCross() {
        return getA();
    }

    public Trigger getCircle() {
        return getB();
    }

    public Trigger getSquare() {
        return getX();
    }

    public Trigger getTriangle() {
        return getY();
    }

    public Trigger getBackButton() {
        return joyMapping.get("back");
    }

    public Trigger getSelect() {
        return getBackButton();
    }

    public Trigger getStart() {
        return joyMapping.get("start");
    }

    public Trigger getHomeButton() {
        return joyMapping.get("guide");
    }

    public Trigger getPSButton() {
        return getHomeButton();
    }

    public Trigger getLeftShoulder() {
        return joyMapping.get("leftshoulder");
    }

    public Trigger getRightShoulder() {
        return joyMapping.get("rightshoulder");
    }

    public Trigger getL1() {
        return getLeftShoulder();
    }

    public Trigger getR1() {
        return getRightShoulder();
    }

    public Trigger getL2() {
        return getLeftTrigger();
    }

    public Trigger getR2() {
        return getRightTrigger();
    }

    public Trigger getLeftStickButton() {
        return joyMapping.get("leftstick");
    }

    public Trigger getRightStickButton() {
        return joyMapping.get("rightstick");
    }

    public Trigger getL3() {
        return getLeftStickButton();
    }

    public Trigger getR3() {
        return getRightStickButton();
    }

    public Trigger getXAxis() {
        return joyMapping.get("leftx");
    }

    public Trigger getXAxisInv() {
        return joyMapping.get("leftx~");
    }

    public Trigger getYAxis() {
        return joyMapping.get("lefty");
    }

    public Trigger getYAxisInv() {
        return joyMapping.get("lefty~");
    }

    public Trigger getRightStickXAxis() {
        return joyMapping.get("rightx");
    }

    public Trigger getRightStickXAxisInv() {
        return joyMapping.get("rightx~");
    }

    public Trigger getRightStickYAxis() {
        return joyMapping.get("righty");
    }

    public Trigger getRightStickYAxisInv() {
        return joyMapping.get("righty~");
    }

    public Trigger getLeftTrigger() {
        return joyMapping.get("lefttrigger");
    }

    public Trigger getLeftTriggerInv() {
        return joyMapping.get("lefttrigger~");
    }

    public Trigger getRightTrigger() {
        return joyMapping.get("righttrigger");
    }

    public Trigger getRightTriggerInv() {
        return joyMapping.get("righttrigger~");
    }

    public Trigger getDpadUp() {
        return joyMapping.get("dpup");
    }

    public Trigger getDpadRight() {
        return joyMapping.get("dpright");
    }

    public Trigger getDpadDown() {
        return joyMapping.get("dpdown");
    }

    public Trigger getDpadLeft() {
        return joyMapping.get("dpleft");
    }

    /**
     * Initializes a joystick based on SDL GameControllerDB, where:<br>
     * - Data are comma separated.<br>
     * - The first value is the joystick model GUID number.<br>
     * - The second value is the joystick model name.<br>
     * - The other values are in the form &lt;field&gt;:&lt;value&gt; and describe the joystick mapping.<br>
     *        * &lt;value&gt; can be <b>aN</b> for axis, <b>bN</b> for button or <b>hN.N</b> for hat
     *        (D-pad). The axis value may also include a <b>~ (aN~)</b> which represents an inversion modifier.<br>
     *
     * @param inputManager the application input manager.
     * @see <a href="https://github.com/gabomdq/SDL_GameControllerDB" target="_top">SDL GameControllerDB</a>
     * @see <a href="https://www.glfw.org/docs/3.3/input_guide.html#gamepad_mapping" target="_top">GLFW Gamepad mappings</a>
     */
    public boolean initialize(final InputManager inputManager) {
        if (id >= inputManager.getJoysticks().length) {
            logger.log(Level.WARNING, "Joystick not connected: " + id);
            return false;
        }

        final Joystick joystick = inputManager.getJoysticks()[id];
        logger.log(Level.INFO,
                "Initializing joystick " + id + "\n * Name: " + joystick.getName() + "\n * GUID: " + guid);

        String line = "";
        try (final InputStream stream = JoystickInput.class.getResourceAsStream("/gamecontrollerdb.txt")) {
            final Scanner scanner = new Scanner(Objects.requireNonNull(stream));
            while (scanner.hasNext() && !line.startsWith(guid)) {
                line = scanner.nextLine();
            }
        } catch (final IOException e) {
            logger.log(Level.SEVERE, "One or more joysticks cannot be initialized");
            return false;
        }

        if (!line.startsWith(guid)) {
            logger.log(Level.WARNING, "Joystick {0} cannot be identified or is not a gamepad controller", id);
            return false;
        }

        final String[] joymap = line.split(",");
        for (int i = 2; i < joymap.length; i++) {
            final String[] entry = joymap[i].split(":");
            if (!entry[0].equals("platform")) {
                final Trigger trigger;
                final int value = Integer.parseInt(entry[1].replaceAll("\\D+", ""));
                final boolean negative = entry[1].contains("~");

                if (entry[1].contains("a")) {
                    trigger = new JoyAxisTrigger(id, value, negative);
                    joyMapping.put(entry[0] + "~", new JoyAxisTrigger(id, value, !negative));
                } else if (entry[1].contains("b")) {
                    trigger = new JoyButtonTrigger(id, value);
                } else {
                    trigger = switch (entry[1]) {
                        case "h0.1" -> new JoyButtonTrigger(id, GLFW.GLFW_GAMEPAD_BUTTON_DPAD_UP);
                        case "h0.2" -> new JoyButtonTrigger(id, GLFW.GLFW_GAMEPAD_BUTTON_DPAD_RIGHT);
                        case "h0.4" -> new JoyButtonTrigger(id, GLFW.GLFW_GAMEPAD_BUTTON_DPAD_DOWN);
                        case "h0.8" -> new JoyButtonTrigger(id, GLFW.GLFW_GAMEPAD_BUTTON_DPAD_LEFT);
                        default -> null;
                    };
                }

                joyMapping.put(entry[0], trigger);
            }
        }

        return true;
    }
}

Usage: create an instance of JoystickInput passing the joystick id in the constructor, then make a call to initialize() with your application’s inputManager. Use the get methods to retrieve the corresponding trigger.

1 Like

There is already some joystick mapping support built in. There is a default database that comes with JME and you can override it with your own, too:

This is done underneath the existing joystick classes for you already.

1 Like

I’m not sure if using the joystick’s name is the right approach. As seen in SDL GameControllerDB, the A (cross) button on a PS3 controller can be reported as b0, b1, b2 or b14. That’s why they use GUID instead.

I don’t know where my game would run and, consenquently, I don’t know with which controller either. How can I use joystick-mapping.properties to cover most know controllers similar to what SDL GameControllerDB does?

The point is that there is a thing already. That thing may not work like you want it yet but maybe we can build on what’s already there instead of something brand new.

The idea behind joystick-mapping.properties was that JME users could contribute mappings as they found them and we could build that out to have our own database of mappings. Game developers can also provide their own.

The current approach is based on the limited information that jinput provided (lwjgl2 based). If running lwjgl3 we could also support GUID-based mappings… but I found these to be at least as inconsistent as the joystick names. (I have two gamepads here that are the same brand, bought them as a pair even and they report different IDs… it’s weird.)

In the end, whatever joystick database we provide is never going to be enough and games will always need to provide a way to remap controls in the game. I will probably be writing a Lemur-based GUI for this in the next 6 months or so. That will take some of the pressure off requiring a 100% complete and accurate database.

Edit: also note that the trigger mapping is not 100% straight forward either… as some treat l2/r2 as independent axis and some treat them as the same axis. We’ve had some hacks done to kind of make this work with the existing mapping but I don’t remember if that’s 100% working. (works with all of my gamepads so far… but I’m also stuck on lwjgl2)

2 Likes

Sure, that’s why I asked in the first post of this thread. If there is something, better use it them. If it’s not complete, I’ll try to contribute any way I can.

There is already the aforementioned game controller database made by the SDL team. It has an open-source license and is also used by GLFW. I understand the idea behind having our own database, but SDL already has it and is very comprehensive. Shouldn’t we do like GLFW and use it as well? Also, we could contribute to SDL’s database with any new data we find, making it even more complete – such contribution would be useful not only for us, but also for SDL and GLFW.

I’ve noticed that when writing my class. Since my methods return instances of Trigger you get either a JoyButtonTrigger or a JoyAxisTrigger.

1 Like

I’d assume the GUID based db to be correct at least for the controllers that have those GUIDs, so maybe it should be used preferentially and then fallback to the name db only if the controller is not found there (eg. if it’s a weird clone or something) or the library can’t provide the guid?

1 Like

Yeah, that could work.

My only point was that we should keep the name based side around… because sometimes even if you don’t have a GUID the name “Xbox” is good enough. Or even “!xbox” because all of the Playstation style controllers I’ve gotten from the fancy final fantasy themed ones to the cheap $5 ones have almost identical button mappings.

It would also let it keep working with jinput which doesn’t seem to provide a GUID at all… just the name.

And our existing DB is good enough to let the user move around a UI and click a “remap this function” button at least.

2 Likes

I am already doing this:

Please note that only the master branch is leveraging GameControllerDB.
As I previously said, I do think this approach will better serve your end user in the long run and jme’s community efforts should not try to compete with GameControllerDB.

3 Likes

@fba, from where do you have this database?

SDL GameControllerDB is available on GitHub - gabomdq/SDL_GameControllerDB: A community sourced database of game controller mappings to be used with SDL2 Game Controller functionality