Easier Game Controller Input in SDL with SDL_GameController

Game controllers on computers are somewhat irritating to manage compared to a console. Has the user plugged in an XBox controller? A PS4 controller, or have they obtained some random USB controller they found on eBay?

Coping with this in SDL was difficult, with SDL just telling you “button 13 pressed” or “joystick axis 4 moved”, which is great except all your code really wants to know is “did the user just press the A button?”.

SDL_GameController fixes all this, and it needs better documentation, so this is my attempt at providing some useful information.

Include the headers

#include <vector>
#include <SDL_gamecontroller.h>Code language: CSS (css)

The vector is needed to hold the attached game controllers so our games can work with multiple players.

Create a structure for the controller data

Each controller gets a struct to hold the button states and values of each axis.

struct  GamePad {
    bool buttons[SDL_CONTROLLER_BUTTON_MAX];
    int axis[SDL_CONTROLLER_AXIS_MAX];
}

Set up variables

We’re keeping a list of connected controllers, and then two lists of controller input states. Having a previous as well as the current state means checking for a button being pressed and then released is easy.

std::vector<SDL_GameController*> connectedControllers;
std::vector<GamePad> controllerInputs;
std::vector<GamePad> lastControllerInputs;
int numGamepads;Code language: HTML, XML (xml)

Give things meaningful names with enums

Nobody wants to be asking for controllerInputs[0].buttons[0] in their code, they want to be writing controllerInputs[PLAYER1].buttons[SDL_CONTROLLER_BUTTON_A] because self documenting code is good. Note that this isn’t how the code actually works, there’s functions to get the button states.

enum Controllers {PLAYER1, PLAYER2, PLAYER3, PLAYER4};

Buttons are referenced by SDL’s own enums which are found in SDL_gamecontroller.h

/**
 *  The list of buttons available from a controller
 */
typedef enum
{
    SDL_CONTROLLER_BUTTON_INVALID = -1,
    SDL_CONTROLLER_BUTTON_A,
    SDL_CONTROLLER_BUTTON_B,
    SDL_CONTROLLER_BUTTON_X,
    SDL_CONTROLLER_BUTTON_Y,
    SDL_CONTROLLER_BUTTON_BACK,
    SDL_CONTROLLER_BUTTON_GUIDE,
    SDL_CONTROLLER_BUTTON_START,
    SDL_CONTROLLER_BUTTON_LEFTSTICK,
    SDL_CONTROLLER_BUTTON_RIGHTSTICK,
    SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
    SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
    SDL_CONTROLLER_BUTTON_DPAD_UP,
    SDL_CONTROLLER_BUTTON_DPAD_DOWN,
    SDL_CONTROLLER_BUTTON_DPAD_LEFT,
    SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
    SDL_CONTROLLER_BUTTON_MAX
} SDL_GameControllerButton;Code language: PHP (php)

Initialise SDL’s game controller subsystem

There’s quite a bit of code here, but all it does is

  • Initialise SDL’s game controller system
  • Count how many controllers are connected and add them to a list
  • Clear the vectors of button/stick states
if (SDL_WasInit(SDL_INIT_GAMECONTROLLER) != 1)
	SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER);

int nJoysticks = SDL_NumJoysticks();
numGamepads = 0;

// Count how many controllers there are
for (int i = 0; i < nJoysticks; i++)
	if (SDL_IsGameController(i))
		numGamepads++;
		
// If we have some controllers attached
if (numGamepads > 0)
{
	for (int i = 0; i < numGamepads; i++)
	{
		// Open the controller and add it to our list
		SDL_GameController* pad = SDL_GameControllerOpen(i);
		if (SDL_GameControllerGetAttached(pad) == 1)
			connectedControllers.push_back(pad);
		else
			std::cout << "SDL_GetError() = " << SDL_GetError() << std::endl;
	}
	SDL_GameControllerEventState(SDL_ENABLE);
}

// Vectors are empty to begin with, this sets their size
controllerInputs.resize(numGamepads);
lastControllerInputs.resize(numGamepads);

// Set the status of the controllers to "nothing is happening"
for (int i = 0; i < numGamepads; i++) {
	for (int a = 0; a < SDL_CONTROLLER_AXIS_MAX; a++) {
		controllerInputs[i].axis[a] = 0;
		lastControllerInputs[i].axis[a] = 0;
	}
	for (int b = 0; b < SDL_CONTROLLER_BUTTON_MAX; b++) {
		controllerInputs[i].buttons[b] = false;
		lastControllerInputs[i].buttons[b] = false;
	}
}Code language: PHP (php)

Game loop code

This small piece of code copies the button states from the previous frame into our “previous” list.

for (int i = 0; i < numGamepads; i++) {
	for (int a = 0; a < SDL_CONTROLLER_AXIS_MAX; a++) {
		lastControllerInputs[i].axis[a] = controllerInputs[i].axis[a];
	}
	for (int b = 0; b < SDL_CONTROLLER_BUTTON_MAX; b++) {
		lastControllerInputs[i].buttons[b] = controllerInputs[i].buttons[b];
	}
}

SDL’s event system is used to actually update the controller states

SDL_Event event;
if (SDL_PollEvent(&event))
{
	switch (event.type) {
	case SDL_QUIT:
		return false;
		break;
	
	// This happens when a device is added
	// A future improvement is to cope with new controllers being plugged in
	// when the game is running
	case SDL_CONTROLLERDEVICEADDED:
		std::cout << "DEVICEADDED cdevice.which = " << event.cdevice.which << std::endl;
		break;

	// If a controller button is pressed
	case SDL_CONTROLLERBUTTONDOWN:
		// Cycle through the controllers
		for (int i = 0; i < numGamepads; i++) {
			// Looking for the button that was pressed
			if (event.cbutton.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(connectedControllers[i]))) {
				// So the relevant state can be updated
				controllerInputs[i].buttons[event.cbutton.button] = true;
			}
		}
		break;
		
	// Do the same for releasing a button
	case SDL_CONTROLLERBUTTONUP:
		for (int i = 0; i < numGamepads; i++) {
			if (event.cbutton.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(connectedControllers[i]))) {
				controllerInputs[i].buttons[event.cbutton.button] = false;
			}
		}
		break;

	// And something similar for axis motion
	case SDL_CONTROLLERAXISMOTION:
		for (int i = 0; i < numGamepads; i++) {
			if (event.cbutton.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(connectedControllers[i]))) {
				controllerInputs[i].axis[event.caxis.axis] = event.caxis.value;
			}
		}
		break;
	}
}Code language: PHP (php)

Use the controller data easily

Checking nested structures within vectors would quickly become tedious, so I wrote some routines to do it for me. They’re part of a larger “input” class, but the logic should make sense.

Has a button been pressed?

bool Input::isControllerButtonPressed(Inputs::Controllers controllerID, SDL_GameControllerButton button)
{
	if (controllerID < 0 || controllerID > numGamepads) return false;

	return controllerInputs[controllerID].buttons[button] && !lastControllerInputs[controllerID].buttons[button];
}Code language: PHP (php)

Is a button being pressed?

bool Input::isControllerButtonHeld(Inputs::Controllers controllerID, SDL_GameControllerButton button)
{
	if (controllerID < 0 || controllerID > numGamepads) return false;

	return controllerInputs[controllerID].buttons[button] && lastControllerInputs[controllerID].buttons[button];
}Code language: PHP (php)

Get an axis value

float Input::getControllerAxis(Inputs::Controllers controllerID, SDL_GameControllerAxis axis)
{
	if (controllerID < 0 || controllerID > numGamepads) return 0.0;

	return controllerInputs[controllerID].axis[axis] / 32768.0f;
}Code language: PHP (php)

Example

This code is inside the update loop of my player character.

if (inputs->isControllerButtonHeld(PLAYER1, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) {
	rightAcc = -acceleration/3.0;
} else
if (inputs->isControllerButtonHeld(PLAYER1, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) {
	leftAcc = -acceleration/3.0;	
} else

if (inputs->isControllerButtonHeld(PLAYER1, SDL_CONTROLLER_BUTTON_DPAD_UP)) {
	leftAcc = rightAcc = acceleration;
} else
if (inputs->isControllerButtonHeld(PLAYER1, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) {
	leftAcc = rightAcc = -acceleration;
}
if (inputs->isControllerButtonPressed(PLAYER1, SDL_CONTROLLER_BUTTON_A)) {
	Vec2D temp = (muzzle->transform->globalPosition - transform->globalPosition).getNormal() * 10;
	bullets->Emit(1, muzzle->transform->globalPosition, temp);
	leftAcc = rightAcc = -acceleration * 5;
	assets->GetSample("shoot")->Play();
}Code language: PHP (php)

References

I used these sites: