Skip to main content
The cover image for "SDL_GameController: Making gamepads just work"

SDL_GameController: Making gamepads just work

Sidebar

When implementing controller support in a game, it's desirable for gamepads to just work without a lot of user configuration. Platform APIs are pretty useless for this, the solution is an API like SDL_GameController that allows you to target a large number of gamepads without much effort. Each operating system has its own API for gamepad input. Windows has XInput, and Linux has the joystick and evdev APIs. When a gamepad button is pressed, applications will receive a button id. This is a number, there's no OS way to know which button id corresponds with which button. The ids for a button are not the same on different gamepads and platforms, making it super hard to support more than a couple of devices. ```cpp if (SDL_JoystickGetButton(joystick, 8)) { std::cerr << "no idea what button 8 is" << std::endl; } ``` One thing platforms do give you is the name, model, and manufacturer of the game controller. If you test with a large number of gamepads, you can create a database from gamepad name to layout. Luckily, SDL_GameController has already done this for you. Instead of a random number, you can use a named button that will work no matter the gamepad and platform: ```cpp if (SDL_GameControllerGetButton(controller, SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_X)) { std::cerr << "X was pressed!" << std::endl; } ```

When implementing controller support in a game, it’s desirable for gamepads to just work without a lot of user configuration. Platform APIs are pretty useless for this, the solution is an API like SDL_GameController that allows you to target a large number of gamepads without much effort.

Each operating system has its own API for gamepad input. Windows has XInput, and Linux has the joystick and evdev APIs. When a gamepad button is pressed, applications will receive a button id. This is a number, there’s no OS way to know which button id corresponds with which button. The ids for a button are not the same on different gamepads and platforms, making it super hard to support more than a couple of devices.

if (SDL_JoystickGetButton(joystick, 8)) {
    std::cerr << "no idea what button 8 is" << std::endl;
}

One thing platforms do give you is the name, model, and manufacturer of the game controller. If you test with a large number of gamepads, you can create a database from gamepad name to layout. Luckily, SDL_GameController has already done this for you. Instead of a random number, you can use a named button that will work no matter the gamepad and platform:

if (SDL_GameControllerGetButton(controller, SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_X)) {
    std::cerr << "X was pressed!" << std::endl;
}

What is SDL_GameController? #

SDL_GameController is an abstraction that allows you to program input based on an Xbox-like controller layout, and have it work with a huge variety of devices. It’s a layer built on top of the raw SDL_Joystick API.

Xbox-like controllers have a DPAD, two analog sticks, 4 buttons on the right (often called A/B/X/Y), shoulder buttons, and 3 buttons in the middle (start/back/logo). Examples include PlayStation DualShock, Nintendo Switch, and Steam Deck (Neptune).

Mapping #

SDL2 comes with a database of game controllers, mapping from controller id to layout information. Users can also provide custom mappings, which is supported without you needing to do anything.

Steam also comes with built-in support for SDL_GameController, allowing users to remap their controllers for your game in Steam. If Steam supports the controller, your game will too.

Using SDL_GameController #

Setup #

When initializing your SDL2 device, add the SDL_INIT_GAMECONTROLLER flag:

if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) {
    std::cerr << "SDL could not initialize! SDL Error: " << SDL_GetError() << std::endl;
    return 1;
}

There are events for controllers connecting and disconnecting, but they’re not fired for controllers that are already connected. So, before starting the game loop, you’ll need to check for existing controllers:

SDL_GameController *findController() {
    for (int i = 0; i < SDL_NumJoysticks(); i++) {
        if (SDL_IsGameController(i)) {
            return SDL_GameControllerOpen(i);
        }
    }

    return nullptr;
}

For simplicity, we’ll only be setting up and tracking a single controller at a time. But it’s a similar process to handle multiple controllers.

SDL_GameController *controller = findController();

Controller connection and removal #

Next, you’ll need to listen for the SDL_CONTROLLERDEVICEADDED and SDL_CONTROLLERDEVICEREMOVED events in the SDL2 event handler:

switch (event.type) {
case SDL_CONTROLLERDEVICEADDED:
    if (!controller) {
        controller = SDL_GameControllerOpen(event.cdevice.which);
    }
    break;
case SDL_CONTROLLERDEVICEREMOVED:
    if (controller && event.cdevice.which == SDL_JoystickInstanceID(
            SDL_GameControllerGetJoystick(controller))) {
        SDL_GameControllerClose(controller);
        controller = findController();
    }
    break;
}

The controller removed event gives us the Joystick Instance ID as which. To check whether this is the same controller, we first need to get the SDL_Joystick and then the ID for the controller.

Handling Input #

There are two ways of receiving input; you can either use events or you can poll for input state. The following events are available:

  • Device Events:
    • SDL_CONTROLLERDEVICEADDED: A controller was added.
    • SDL_CONTROLLERDEVICEREMOVED: A controller was removed.
    • SDL_CONTROLLERDEVICEREMAPPED: A controller was remapped, you can mostly ignore this event unless you use raw joysticks.
  • Button Events:
    • SDL_CONTROLLERBUTTONDOWN: A button was pressed on a controller.
    • SDL_CONTROLLERBUTTONUP: A button was released on a controller.
  • Axis Events:
    • SDL_CONTROLLERAXISMOTION: An axis was moved, such as a thumbstick or analog trigger.

Here’s an example of handling the X button using the event:

case SDL_CONTROLLERBUTTONDOWN:
    if (controller && event.cdevice.which == SDL_JoystickInstanceID(
            SDL_GameControllerGetJoystick(controller))) {
        switch (event.cbutton.button) {
        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_X:
            std::cerr << "X pressed!" << std::endl;
            break;
        }
    }
    break;

and by polling for input state:

if (SDL_GameControllerGetButton(controller, SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_X)) {
    printf("X was pressed!\n")
}

Conclusion #

Using SDL_GameController, it’s possible to target a large number of gamepads on different platforms without much effort. How to manage multiple controllers, keyboard and mouse, and input binding is definitely a story for another time.

The complete example code for SDL_GameController is available for download from GitLab.

Sources #

Image © 2019 Stas Knop

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Comments

Leave comment

Shown publicly next to your comment. Leave blank to show as "Anonymous".
Optional, to notify you if rubenwardy replies. Not shown publicly.
Max 1800 characters. You may use plain text, HTML, or Markdown.