Handmade Penguin Chapter 6: Gamepad and Keyboard Input

<-- Chapter 5 | Back to Index | Chapter 7 -->

Controller Input

On Linux (and to an extent with DirectInput and other older Windows APIs), game controllers (gamepads, steering wheels, guitars, etc) are all considered joysticks. To make this work, joysticks are basically treated as an arbitrary collection of buttons, axes and assorted other input-y bits. This is good, in the sense that you can handle pretty much any kind of game controller you can get, no matter how many weird buttons and stuff it has.

SDL has a great Joystick API that lets you handle these. Unfortunately, we wouldn't know what to do if "button 200" was pressed, as we wouldn't know what that button actually was on the physical controller. The traditional solution to this is letting the user remap the buttons, but (a) that's a lot of work and (b) these days, most controllers are pretty similar (Xbox, PlayStation) etc. SDL provides a Game Controller API that handles all of that mapping to a standard Xbox 360 (and hence XInput) style controller. It supports a bunch of common controllers out of the box, and if you launch your game throught Steam's Big Picture mode, Steam will pass its mapping to your game.

SDL's game controller API lets you either get messages when buttons are pressed and axes moved or poll the state of the controller. As we want to mimic XInput as much as possible, we'll be polling.

Opening the Controller

The very first thing we need to do is initialise the controller subsystem. If we find our SDL_Init() call, we see that we're only initialising the video subsystem: SDL_INIT_VIDEO . We also want to initialise the Game Controller subsystem, so let's add SDL_INIT_GAMECONTROLLER to that bitfield by bitwise-oring it to SDL_INIT_VIDEO .

SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER);

Check this out! You can also initialise subsystems after your SDL_Init() call using SDL_InitSubsystem() .

Before we can use controllers, we have to open them. As the game controller API is basically a wrapper on top of the underlying Joystick API, the way we do this is by looping through all of the Joysticks, finding out which ones are gamepads (or at least, which ones have had mappings set up) and then opening them. First, we need to get the number of joysticks using SDL_NumJoystics() . We then loop over all of the joystick IDs (which will be the integers from 0 to SDL_NumJoysticks() - 1) and check to see if they are valid game controllers, using the SDL_IsGameController() .

If SDL_IsGameController() returns true, then we want to open our game controller. We'll use SDL_GameControllerOpen() . If we look at the documentation, SDL_GameControllerOpen() takes the index of the underlying joystick as an argument and returns a pointer to an SDL_GameController . We know what the joystick index will be (it's just the index of our loop), but we'll need somewhere to put our SDL_GameController pointer. If we want to put this in an array, we're going to need to know the size of the array in advance (or resize the array using something like realloc() ). We'll limit this to 4 now, by having a global MaxControllers define:

#define MAX_CONTROLLERS 4 SDL_GameController *ControllerHandles[MAX_CONTROLLERS];

MAX_CONTROLLERS

4

We can now open all of our game controllers:

int MaxJoysticks = SDL_NumJoysticks(); int ControllerIndex = 0; for(int JoystickIndex=0; JoystickIndex < MaxJoysticks; ++JoystickIndex) { if (!SDL_IsGameController(JoystickIndex)) { continue; } if (ControllerIndex >= MAX_CONTROLLERS) { break; } ControllerHandles[ControllerIndex] = SDL_GameControllerOpen(JoystickIndex); ControllerIndex++; }

Be warned! This obviously doesn't work if you plug in a new controller halfway through playing the game: we'll have already intialised all of the controllers, and so it won't pick up any new ones. The way to fix this is to handle the SDL_CONTROLLERDEVICEADDED , SDL_CONTROLLERDEVICEREMOVED and SDL_CONTROLLERDEVICEREMAPPED events.

To close these controllers again once we've finished with them, we can use the SDL_GameControllerClose() function. It behaves much as you'd expect: taking a SDL_GameController* and closing it.

for(int ControllerIndex = 0; ControllerIndex < MAX_CONTROLLERS; ++ControllerIndex) { if (ControllerHandles[ControllerIndex]) { SDL_GameControllerClose(ControllerHandles[ControllerIndex]); } }

Polling for Input

To actually get input from our game controllers, we use the SDL_GameControllerGetAxis() and SDL_GameControllerGetButton() functions. These both accept an SDL_GameController pointer as well as an SDL_GameControllerAxis and SDL_GameControllerButton respectively. So, for example, to find out of the A button was pressed on the controller Controller , we would use:

bool IsAPressed = SDL_GameControllerGetButton(Controller, SDL_CONTROLLER_BUTTON_A);

Because we want to do this for all of the plugged in controllers, we'll just loop over them:

for (int ControllerIndex = 0; ControllerIndex < MAX_CONTROLLERS; ++ControllerIndex) { if(ControllerHandles[ControllerIndex] != 0 && SDL_GameControllerGetAttached(ControllerHandles[ControllerIndex])) { // NOTE: We have a controller with index ControllerIndex. bool Up = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_DPAD_UP); bool Down = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_DPAD_DOWN); bool Left = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_DPAD_LEFT); bool Right = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_DPAD_RIGHT); bool Start = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_START); bool Back = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_BACK); bool LeftShoulder = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_LEFTSHOULDER); bool RightShoulder = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_RIGHTSHOULDER); bool AButton = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_A); bool BButton = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_B); bool XButton = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_X); bool YButton = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_Y); int16 StickX = SDL_GameControllerGetAxis(ControllerHandles[ControllerIndex], SDL_CONTROLLER_AXIS_LEFTX); int16 StickY = SDL_GameControllerGetAxis(ControllerHandles[ControllerIndex], SDL_CONTROLLER_AXIS_LEFTY); } else { // TODO: This controller is note plugged in. } }

SDL_GameControllerGetAttached()

SDL_GameControllerGetAttached()

SDL_GameController*

SDL_GameControllerGetAttached()

Check it out! The SDL_GameControllerGetAttached() function actually checks to see if the SDL_GameController pointer we pass it is null, and treats that as a controller not being plugged in. This means that we didn't actually have to do the check ourselves.

We can now do things differently depending on the state of the game controller. Let's move the

YOffset += 2;

if (AButton) { YOffset += 2; }

Be warned! It can sometimes be hard to get SDL to recognise your game controller, particularly if it isn't one of the really popular console ones. To fix this, you usually need to provide a mapping for your controller. The easiest way to do this is throught Steam: configure your controller in Steam's "Big Picture" mode, then open the ~/.steam/root/config/config.vdf file. Find the SDL_GamepadBind variable, and copy its contents. You can then export that into a terminal with the: export SDL_GAMECONTROLLERCONFIG="contents of SDL_GamepadBind " command. It should end up looking something like: export SDL_GAMECONTROLLERCONFIG="030000008f0e00000300000010010000,SPARTAN PS3 Knockoff,a:b2,b:b1,y:b0,x:b3,start:b9,guide:,back:b8,leftstick:b10,rightstick:b11, leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,"

Aside: Loading Libraries

Since our game controller library is a part of SDL (which we need anyway), there isn't much point trying to dynamically load it. It's worth knowing how it works, so that when we do try to add a new library, we can.

Linux has a function equivalent to LoadLibrary() called dlopen() . dlopen() is a pretty simple function, it takes the filename to the library we want to load and some flags that specify extra options. dlopen() returns a null pointer if there's an error, and a pointer to something otherwise.

To load SDL, for example, we would use:

void *LibHandle = dlopen("libSDL2-2.0.so.0", RTLD_NOW);

RTLD_NOW

dlopen()

RTLD_LAZY

The equivalent function to GetProcAddress() is dlsym() . This is, if anything, even more simple: it takes the handle that dlsym() returned and a string for the symbol we want to resolve. It then returns a void pointer which we can then cast to the function pointer type we wanted.

Finally, we just need to close the library with dlclose() . Easy!

SDL also provides its own functions to load libraries and symbols. These will work on Windows and Linux (and several other platforms besides) if you're already using SDL. Here you use SDL_LoadObject() instead of LoadLibrary() or dlopen() , SDL_LoadFunction() instead of GetProcAddress() or dlsym() and SDL_UnloadObject() instead of FreeLibrary() or dlclose() .

Force Feedback: Rumble in the Haptic Jungle

Be warned! I don't have a controller with rumble support lounging about here, so this code may be entirely wrong: I haven't had a chance to test it. Let me know if it's totally broken.

The bad news is that, much like the rest of controller input, adding rumble support (also known as "force-feedback" or — more geekily — "haptic devices") is much more complicated in SDL than it is in XInput. This largely comes from the flexibility of dealing with many more devices than just Xbox controllers, though some of it is still rather overcomplicated (how often do you need to controll a force-feedback mouse, or have the rumble motors play arbitrary waves?).

First, we need to add the SDL_INIT_HAPTIC subsystem to the list of subsystems we're initialising with SDL_Init() .

SDL_Init( SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC );

SDL_Haptic

SDL_GameController*

SDL_Haptic *RumbleHandles[MAX_CONTROLLERS];

SDL_HapticOpenFromJoystick()

SDL_Joystick

SDL_Joystick

SDL_GameController

SDL_GameControllerGetJoystick()

SDL_Joystick

SDL_GameController

SDL_Joystick *JoystickHandle = SDL_GameControllerGetJoystick(ControllerHandles[ControllerIndex]); RumbleHandles[ControllerIndex] = SDL_HapticOpenFromJoystick(JoystickHandle);

SDL_HapticClose()

if (RumbleHandles[ControllerIndex]) SDL_HapticClose(RumbleHandles[ControllerIndex]);

We then need to intialise the simple rumble effect. SDL's haptic subsystem supports a huge number of effects, and most of them use a complicated SDL_HapticEffect system. We can avoid this if we just want a simple rumble feature (though we'll need to use the SDL_HAPTIC_LEFTRIGHT effect type if we want to controll the Xbox 360 controller's left and right motors individually). We need to use the SDL_HapticRumbleInit() function to set up the simple rumble effect. It accepts an SDL_Haptic pointer as input, and returns 0 if the simple rumble effect was initialised successfully, and a negative error code otherwise. We could use the SDL_HapticRumbleSupported() function to check if our haptic device supports the simple rumble effect, but we know that, if it doesn't, initialising it will fail, so we can just check that. We'll initialise this when we open the haptic device, and just close the device otherwise, as it'll be of no use to us:

if (SDL_HapticRumbleInit(RumbleHandles[ControllerIndex]) != 0) { SDL_HapticClose(RumbleHandles[ControllerIndex]); RumbleHandles[ControllerIndex] = 0; }

RumbleHandles[ControllerIndex]

To actually make our controller rumble (finally), we then just need to use the SDL_HapticRumblePlay() function. This takes three parameters:

haptic : Our SDL_Haptic pointer. strength : The strength of the rumble, as a floating-point fraction between 0 and 1. So 0.3 would be 30%. length : The number of milliseconds for the controller to stay rumbling. Can be SDL_HAPTIC_INFINITY to have it never stop!

if (BButton) { if (RumbleHandle[ControllerIndex]) { SDL_HapticRumblePlay(RumbleHandle[ControllerIndex], 0.5f, 2000); } }

SDL_HapticRumbleStop()

SDL_Haptic

Keyboard Input

The good news is that the keyboard is much easier to handle: we just get SDL_KEYDOWN and SDL_KEYUP events when keys are pressed or released. Let's add them to our HandleEvent() function:

case SDL_KEYDOWN: case SDL_KEYUP: { SDL_Keycode KeyCode = Event->key.keysym.sym; } break;

SDL_Keycode

SDL_Keycode

SDL_Scancode

SDL_Keycode

SDL_Scancode

We can therefore just switch or if on the value of KeyCode . To check if the value is W, we can use

if(KeyCode == SDLK_w) { printf("W

"); }

SDLK_w

Instead of a "was down" flag, SDL provides us with two flags, the current state of the key, Event->state , which will be either SDL_PRESSED or SDL_RELEASED ; and whether or not this key is a repeat, Event->repeat . We can reconstruct the "was down" flag from these:

If the key is now released ( Event->state is SDL_RELEASED or our event type is SDL_KEYUP ), then the key was down.

is or our event is ), then the key was down. If the key is now pressed (otherwise), and it is a repeat ( Event->repeat is not 0), then the key was down.

is not 0), then the key was down. Otherwise, the key was not down.

bool WasDown = false; if (Event->key.state == SDL_RELEASED) { WasDown = true; } else if (Event->key.repeat != 0) { WasDown = true; }

End of Lesson!

Wow: that was a lot of work. This is one of the few places where Windows is significantly less verbose than SDL, thanks to XInput being really simple. Our code is not without its advantages: it supports all sorts of gamepads that XInput doesn't! The pieces are starting to fall in place for a real game now. Tomorrow we'll be looking at (and listening to) some sound code. See you then!

If you've bought Handmade Hero, the source for the Linux version can be downloaded here.

Update (2014-11-25): Thanks to Stanisław Gackowski for helping to debug the many problems with the rumble code. It should work now!

<-- Chapter 5 | Back to Index | Chapter 7 -->