"Game Controller API in SDL2"

Posted by Ryan C. Scott on Tue 14 January 2014

SDL2 brought with it a new API for interacting with game controllers such as the Xbox 360 controller (which I've heard tell is the most widely used and supported controller for PC gaming). The documentation is a little light, so here's a quick and dirty tutorial and a few notes on it.

The Purpose

Mapping joystick buttons and axises to inputs in your game is deceptively complicated. It generally requires several layers of mapping in order to provide the flexibility users will need in order to hookup up various devices.

The game controller API provides a way of treating joysticks as logical collections of named buttons and axises. It takes in mappings of raw inputs to symbolic names, such as "START" or "RIGHTX".

The Utility

This enables you to transparently support things like using axises as buttons and vice verse.

Symbolic Names for Axises & Buttons

This little bit of logic cleans up a lot of code that would likely end up littering your project; e.g. checking that a particular axis is past a threshold to consider it pressed in order to trigger firing from a trigger button, for example.

Also, and this is nice, all of your gameplay code can focus on mapping that set of symbolic names to your own functionality, freeing you up to focus on supporting a single re-mapping interface for user button preferences. As an example, it's much easier to deal exclusively with allowing players to change their primary firing functionality from the right trigger to the left.

Hot Plugging

Device plug-in and removal are now handled as SDL events.

One pitfall here is that the joystick instance IDs that are sent as the identifying information with all of controller events will change on re-opening the controller. It's not a huge deal, but if you have code that's expecting to treat the instance ID as the joystick index for callbacks, etc. you'll need some sort of mapping.

XInput support on Windows

It'd be nice if this weren't a real issue, but it is. On windows, you can read an XInput device as a raw joystick, but you get strattled with a number of quirks such as the left and right triggers being treated as a single axis (one positive direction and one negative). SDL2 doing the lifting here saves you from having to bother with supporting XInput devices separately.

Tutorial

Initialization

As with other SDL subsystems, you'll need to add controller support to your SDL_INIT call.

SDL_INIT( SDL_INIT_GAMECONTROLLER );

In your main event handling code, you can respond to the follow events:

  • SDL_CONTROLLERDEVICEADDED:
  • SDL_CONTROLLERDEVICEREMOVED:
  • SDL_CONTROLLERBUTTONDOWN:
  • SDL_CONTROLLERBUTTONUP:
  • SDL_CONTROLLERAXISMOTION:

Button and axis events embed SDL_ControllerButtonEvent and SDL_ControllerAxisEvent respectively.

Controller Detection

SDL_GameController *pad = SDL_GameControllerOpen( id );
SDL_Joystick *joy = SDL_GameControllerGetJoystick( pad );
int instanceID = SDL_JoystickInstanceID( joy );

SDL_CONTROLLERDEVICEADDED and SDL_CONTROLLERDEVICEREMOVED do not run on application startup for already connected devices. In that case you'll want to iterate over the joysticks and attempt to add each as a controller.

Controller Removal

Removal is pretty straightforward as well, however the SDL_CONTROLLERDEVICEREMOVED event passes in the underlying joystick ID as opposed to anything directly identifying the controller.

SDL_GameController *pad = YOUR_FUNCTION_THAT_RETRIEVES_A_MAPPING( id );
SDL_GameControllerClose( pad );

Button and Axis Events

Button and axis events are sent for all button and axis events; i.e. pressing a button also looks like a digital axis. The threshold used to determine what amount of motion on an access is considered a button press is handled by the controlling mapping string which I am not even remotely covering here. It is worth noting however that the XBox360 controller did not need an extra setup and worked without an explicit mapping whatsoever.

Everything past this point is left for you to implement in whatever way your project dictates; script callbacks, hardcoded handlers, etc., etc.

A Full Example That I Didn't Test At All

The following code is a pared down example from my engine. I've removed everything except for the core workings in the hopes that it makes things clearer. So when I say I didn't test it I don't mean that I just read the documentation and then just spat this out... it at least comes from working code even if I couldn't be bothered to make a full stand-alone example.

void AddController( int id )
{
    if( SDL_IsGameController( id ) ) {
        SDL_GameController *pad = SDL_GameControllerOpen( id );

        if( pad ) {
            SDL_Joystick *joy = SDL_GameControllerGetJoystick( pad );
            int instanceID = SDL_JoystickInstanceID( joy );

            // You can add to your own map of joystick IDs to controllers here.
            YOUR_FUNCTION_THAT_CREATES_A_MAPPING( id, pad );
        }
    }
}

void RemoveController( int id )
{
    SDL_GameController *pad = YOUR_FUNCTION_THAT_RETRIEVES_A_MAPPING( id );
    SDL_GameControllerClose( pad );
}

void OnControllerButton( const SDL_ControllerButtonEvent sdlEvent )
{
    // Button presses and axis movements both sent here as SDL_ControllerButtonEvent structures
}

void OnControllerAxis( const SDL_ControllerAxisEvent sdlEvent )
{
    // Axis movements and button presses both sent here as SDL_ControllerAxisEvent structures
}

void EventLoop()
{
    SDL_Event sdlEvent;

    while( SDL_PollEvent( &sdlEvent ) ) {
        switch( sdlEvent.type ) {

        case SDL_CONTROLLERDEVICEADDED:
            AddController( sdlEvent.cdevice );
            break;

        case SDL_CONTROLLERDEVICEREMOVED:
            RemoveController( sdlEvent.cdevice );
            break;

        case SDL_CONTROLLERBUTTONDOWN:
        case SDL_CONTROLLERBUTTONUP:
            OnControllerButton( sdlEvent.cbutton );
            break;

        case SDL_CONTROLLERAXISMOTION:
            OnControllerAxis( sdlEvent.caxis );
            break;

        // YOUR OTHER EVENT HANDLING HERE

        }
    }
}