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

1 Like

@fba, do you still use this approach to get the right button names? They are all over for different kind of controllers if I use Lemur, it’s quite hard to make something nice, especially if you wanna have hints in the game, it is almost impossible to display the right button. Or does meanwhile exist a different approach for this?

And I wondered how do you deal with a controller which can’t be found in this SDL DB (mine was not in it)?

1 Like

Yes, I still use this approach. I don’t use it with Lemur though since I write my GUI stuff directly to the guiNode, so I can’t say what are the difficulties.

When a gamepad GUID is not in the database, I check if the word Xbox is part of its name. If so, I choose a generic configuration. If not, it means it probably isn’t a Xbox-like gamepad, like a driving wheel or an airplane joystick controller.

If your controller is not in the database, I encourage you to add it via PR to the SDL project.

2 Likes

It is highly recommend to create a PR to add your controller to SDL.

Some things to take note of:
JME does not use the bleeding edge snapshot version of LWJGL (for good reason because it is a snapshot) but this version of LWJGL has a newer version of GLFW which may have a newer version of the SDL database. You might want to check if your controller is in the SDL database and if that version of the database is used by GLFW and if LWJGL is updated to that version of GLFW.

Also take note, you can manually update the SDL database to the latest version (GLFW has instructions for this on their website) but IIRC the latest version of the SDL database causes GLFW to crash and GLFW is working on a fix. (Fix might already be in place, you will want to verify if going this route).

If your user base is commonly using this controller, you can manually add it to the SDL database file in your app. AND open a PR with the SDL database to add it.

Finally, it is always a good idea to allow users to map controls to their controller as many custom controllers exist (and many of those do not follow standards of the controllers they are copying)

~Trevor

3 Likes

Agree. Allowing users to map their gamepads is a good practice.

I think you meant GLFW! :wink:

1 Like

Yep, sorry. This is why I should not use the computer before my morning coffee!

1 Like

I will make a PR for my controller. It is not in the SDL DB on github, so I guess it is not in the newest GLTW. Anyway, I load this DB “manually”. Lemur and the gltw3 seem to have issues so I currently fall back to GLTW 2.

I currently do my own mapping and bc I have not a clue what Button1…10 is exactly mapped to for the different controllers, I just say “Defined” or “Undefined”.

With the DB I hope I can get the proper names for the defined buttons. Will see, but at least there is hope :smiley:

NOTE: Would be cool if Lemur would give some sort of interface to overwrite the InputManager (maybe it does and I don’t know exactly how I could overwrite it with my own approach).

1 Like

What do you mean by “overwrite the InputManager”? Do you mean something beyond InputMapper? Or something specific to Joysticks?

I tried GWLT3 and now I only have the left joystick left and it behaves negative (joystick up to navigate down and vise verse)
I might be able to fix it with an own implementation. As well the button names are a bit all over on differed game controllers (A, B, X, Y). Even the button to click a Lemur button is sometimes the on south (A) or the one to the West (X), depending if it’s Nanco or PS4 or …
But somehow I have no say over that in Lemur or I simply can’t see it. So I thought if that input manager Thing is an interface and there is a possibility to hand over a custom one (whatever that might be in my case).

Lemur defers to the button remappings that JME can already do. If the gamepad has a name different than other names then JME can already remap the buttons to something sensible in a joystick.properties file.

…in the end, you will never get away from allowing the user to remap the buttons because the gamepad industry has never decided on a standard layout and seem to go out of their way to make sure every other gamepad has different buttons.

1 Like