<-- Chapter 5 | Back to Index | Chapter 7 -->
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.
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];A #define tells the preprocessor to replace the word MAX_CONTROLLERS with 4 before it compiles.
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]); } }
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. } }Pretty straightforward, huh? The most complicated thing is probably the if statement, which both uses a function we haven't looked at, SDL_GameControllerGetAttached() and relies on the short-circuiting behaviour of the if statement. SDL_GameControllerGetAttached() returns true if the controller is plugged in. We don't want to pass it an invalid SDL_GameController* though, so we check if it is not 0 and it is attached. Because the entire expression cannot be true if one part is false, the call to SDL_GameControllerGetAttached() will never happen if our controller is null.
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;line into our controller loop, and only run it if the A button is down.
if (AButton) { YOffset += 2; }Now our pattern will only scroll horizontally if we aren't pressing the A button on any controllers. If we have more controllers plugged in, we can make it scroll faster vertically by pressing the A button down on several of them.
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," |
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);The RTLD_NOW flag tells dlopen() to load all of the libraries that this library needs immediately. This means that if there are any errors, we'll get them immediately. If we want, we can use the RTLD_LAZY flag instead, to only load the libraries that SDL needs when SDL actually tries to call their functions. This means that we won't get missing library/function errors until those functions are actually used. Do check out the documentation for more details.
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().
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 );Then, to use a joystick's rumble features, we need to have a haptic device, in the form of an SDL_Haptic pointer. We'll keep an array of these next to our array of SDL_GameController*s:
SDL_Haptic *RumbleHandles[MAX_CONTROLLERS];To open these, we need to use SDL_HapticOpenFromJoystick(). This will return 0 if the SDL_Joystick pointer we pass it doesn't represent a joystick with haptic support. To get an SDL_Joystick pointer from our SDL_GameController pointer, we use the SDL_GameControllerGetJoystick() function, which just returns the corresponding SDL_Joystick pointer for the SDL_GameController pointer we pass it.
SDL_Joystick *JoystickHandle = SDL_GameControllerGetJoystick(ControllerHandles[ControllerIndex]); RumbleHandles[ControllerIndex] = SDL_HapticOpenFromJoystick(JoystickHandle);Similarly, to close a haptic device, we use SDL_HapticClose(). We probably should check if the controller we're dealing with has an assosciated haptic device before trying to close it, though:
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; }We'll now know that if our RumbleHandles[ControllerIndex] pointer is not null, that we have a valid haptic device with the rumble effect initialised.
To actually make our controller rumble (finally), we then just need to use the SDL_HapticRumblePlay() function. This takes three parameters:
if (BButton) { if (RumbleHandle[ControllerIndex]) { SDL_HapticRumblePlay(RumbleHandle[ControllerIndex], 0.5f, 2000); } }If we wanted to stop the controller from rumbling (before the length of the rumble is up), we use the SDL_HapticRumbleStop() function, which simply takes the SDL_Haptic pointer of the device we want to stop rumbling.
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;You'll notice that we're reading an SDL_Keycode: this tells us which key was pressed or released. SDL actually has two different types of key codes (three if you count text input): SDL_Keycode and SDL_Scancode. Basically, SDL_Keycode refers to what the key represents, and will change if the user reconfigures their keyboard layout, whereas SDL_Scancode represents the physical key directly. There's an excellent table showing the possible values for each.
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\n"); }Note that the 'w' in SDLK_w is in lowercase. That's because if you press the W key on your keyboard, by default it will give you a lowercase '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:
bool WasDown = false; if (Event->key.state == SDL_RELEASED) { WasDown = true; } else if (Event->key.repeat != 0) { WasDown = true; }
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!