• 2D shoot 'em up
SDL2 Gunner tutorial
SDL2 Shooter 2 tutorial
SDL2 Widget tutorial
SDL2 Adventure tutorial
— Creating a Run and Gun game —
Something that's missing from our game to give it a real arcade feeling is joystick controls. SDL has always supported various gamepads, so in this part of the tutorial we're going to look into how to implement them into our game. Please note that this tutorial makes use of and expands upon the widgets found in the Widgets tutorial and the SDL2 Shooter 2 tutorial. Also, you should connect your joypad before running the program, so that they can be detected upon startup. Note: I'm using "joysticks" and "joypads" here interchangeably. They both refer to the same thing, however.
Extract the archive, run make, and then use ./gunner14 to run the code. You will see a window open like the one above. Press Escape to bring up the in-game menu and use the arrow keys, plus Space / Return to action the selected item. Navigate to Options, then Controls. Highlight the control you wish to change, then press Space / Return. The control will change to "...". Press the key or button you wish to use, or Escape to cancel. Pressing Backspace will also clear the control. These controls will now be in effect. Note that the character movement is always controlled with the left analog stick (or dpad, if no analog stick is available). Press Escape or select Back to leave the menu. Once you're finished, close the window to exit.
Inspecting the code
While supporting controllers is not hard at all, what takes a lot of time is creating the interface for the player to configure their controller and plumb in everything around it. We've had to make quite a few changes, though some of them are rather minor. We'll start with defs.h:
Two new defines: MAX_JOYPAD_BUTTONS and DEADZONE_MAX. MAX_JOYPAD_BUTTONS is the maximum number of buttons we'll supoort for a joypad, while DEADZONE_MAX is the maximum deadzone threshold for an analog joystick. Some joysticks when plugged in report a small non-zero value on the axis to the computer. The deadzone will help us to ignore this and prevent the character from moving while there is no user interaction.
We've also added in a few new enums:
The CONTROL enum will be used to define the type of control we wish to query or work with. This should be familiar to you, if you've looked at the SDL2 Shooter code.
The JOYPAD_AXIS enum will be used to determine which axis (x or y) we're working with when it comes to testing analog sticks.
We'll move onto the updates to structs.h now. We've introduced all the widgets from SDL2 Shooter into our structs.h file, though we've now modified ControlWidget:
`x` and `y` are the positions of the widget on screen, while `keyboard` will hold the value of the keyboard key we've configured (an SDL scancode) and `joypad` will hold the value of the joypad button we've configured.
We've also added in a Game struct:
Again, this should look familiar to you if you've studied the Widgets and SDL2 Shooter 2 tutorial. There is now the addition of joypadControls, as an array of ints up to CONTROL_MAX (6 elements).
Lastly, we've updated App:
We've added in five new fields: joypadButton, joypadAxis, lastKeyPressed, lastButtonPressed, and `joypad`. joypadButton is an array of int, like `keyboard`, to hold the state of the joypad buttons. joypadAxis does likewise for the axis of our analog sticks. lastKeyPressed will hold the value of the last key that was pressed, while lastButtonPressed will hold the value of the last joypad button that was pressed. `joypad` is a pointer to the SDL joystick that's currently in use.
That's our defs.h and structs.h done, so we can now look at the how we actually use our joysticks. Turning to init.c, we've updated initSDL:
We've added in SDL_INIT_JOYSTICK to the SDL_Init function. Without this, SDL will not open the joystick subsystem and our joysticks will not be found. It will therefore mean that SDL will report 0 joysticks being available for use. We've also added in a new function called initJoypad:
The goal behind this function is to search for a joystick and use it (note - we only care about the first joystick we find). The first thing this function does is call SDL_NumJoysticks and assign the value to a variable named `n`. We then log a message saying how many joysticks are found. If this message reports 0, there are no joysticks available (which may be a result of SDL_INIT_JOYSTICK failing) or your joystick not being plugged in.
We then setup a for-loop, looping through all the joysticks that were detected and attempt to open (use) them. We do this by calling SDL_JoystickOpen and passing in the index of the joystick (`i`). We assign this to `app.joypad`. Next, we test if `app.joystick` is not NULL (quite possible, if the joystick was removed between SDL_INIT_JOYSTICK and this call). If it's not NULL, we can consider this joystick valid. We log the details of the joystick and then return from the function, as we've found a joystick to use.
We've also tweaked our initGameSystem function:
We've now calling initJoypad and initGame (which we'll see later on).
So, we can now detect our joystick. Let's look at how we handle it. Turning to input.c, we've made a few updates and additions. Starting with doInput:
We've handling three new SDL events: SDL_JOYBUTTONDOWN, SDL_JOYBUTTONUP, and SDL_JOYAXISMOTION. Upon processing these events, we'll call doButtonDown, doButtonUp, and doJoyAxis respectively. For SDL_JOYBUTTONDOWN and SDL_JOYBUTTONUP, we'll pass over the event's `jbutton` event. For SDL_JOYAXISMOTION, we'll pass over the `jaxis` event.
We'll look at doButtonDown first:
A simple function. We're testing the state of the SDL_JoyButtonEvent, to ensure that the button has been pressed and that the button id is less than MAX_JOYPAD_BUTTONS (so we don't overflow our array), and then setting the value of App's joypadButton to 1, at the array index of the button id. We're also assigning App's lastButtonPressed to the id of the button. This means we can now query the joypadButton array in App to find out if a button has been pressed.
doButtonUp is quite similar:
The difference, as you can see, is that we're testing if the button has been released and that we're setting the value of the button index to 0, to say it's no longer in use.
doJoyAxis is equally simple:
We've ensuring that the value of axis can fit within our array (JOYPAD_AXIS_MAX) and then setting the value of App's joypadAxis appropriately.
And that's all we need to do to handle our joystick input. However, the problem we now face is that all joysticks are different and will report different ids for their buttons. Some controllers will have 3 buttons, others 8 or more. The controller I used for development has 4 face buttons, two shoulder buttons, and two additional buttons that might be considered Start and Select. We therefore cannot assume that to jump the player will press button 1, as it might not be the same on one joystick to the next.
Therefore, we're going to update our widget code to allow for the user to input the joystick buttons they wish to use. Turning to widgets.c, we've made several changes to our ControlWidget processing (again, ensure you're familiar with the Widget tutorial). Starting with doWidgets:
When it comes to handling our WT_CONTROL widgets, we're now setting App's lastButtonPressed to -1, in addition to lastKeyPressed. Now, when we come to doControlWidget, we're checking both these values:
Focusing on lastButtonPressed for now, you'll remember that input.c recorded the last button that was pressed during the doButtonDown function. We test to see if this is not -1 and respond. We're setting the ControlWidget's `joypad` value to the value of App's lastButtonPressed, then zeroing the value in App's joypadButton array, to ensure no unwanted behaviour when we return to the game (such as jumping or firing, since we just assigned the key). We're also first testing to see if the Backspace key has been pressed, in which case we'll clear the control, to allow it to be reset.
Pretty simple. The only other thing we need to do is update the rendering code for the ControlWidget, so we can show the user which key and button is in use for the control in question. We've updated drawControlWidget for this purpose:
Now, when creating the `text` that we're going to pass to drawText, we're testing the `keyboard` and `joypad` fields of our ControlWidget. If they're both valid (`keyboard` is not 0 and `joypad` is not -1), we're rendering text to say which key or joypad button can be used. Otherwise, if `keyboard` is not 0 (meaning that `joypad` is -1) we're rendering just the name of the key. If `joypad` is not -1 (`keyboard` is 0), we'll be drawing just the button id. Finally, we'll be setting the text as "N/A" if neither a key nor a joypad button is in use. This satisifies all 4 possible conditions of our ControlWidget.
We're now able to set a keyboard key or button to our Control widgets. What we then need to do is set this data into our game configuration. If we turn to options.c (introduced as part of pulling in the widgets and other config files from SDL2 Shooter 2), we can see how this is done in updateControls:
This function is largely the same as in SDL2 Shooter 2, except that as well as setting Game's keyControls from the ControlWidget, we're now also setting Game's joypadControls from the Widget's `joypad` value.
Additionally, we've updated setupWidgets:
Taking our "left" ControlWidget as an example - we're setting both the `keyboard` and `joypad` fields from Game's keyControls and joypadControls for the CONTROL_LEFT value of the arrays. In short, the opposite of what is happening with updateControls, to keep the two in sync with one another.
That's all our setup done. We now have the ability to configure the controls as we want. The only thing remaining is to actually use them. To do this, we've introduced a new function into player.c, called isControl:
The function takes a single argument: the type of control we wish to query (such as CONTROL_LEFT, CONTROL_JUMP, etc). To begin with, we're testing if `type` is CONTROL_LEFT. If so, we're checking if the value of App's joypadAxis array at index JOYPAD_AXIS_X is less than the negative of Game's `deadzone`. In short, we're testing to see if the left analog stick (or dpad) is being pushed left. If so, we'll return 1. App's joypadAxis array values can be anything from -32,768 or +32,767. That's a large range! In an ideal world, we need only test if the value at the index is less than 0 (0 meaning the stick is at rest). However, joysticks can report small values, such as -21, 311, -89, etc., due to their analog nature. We therefore want to check against our `deadzone`, to ensure the value is within the range we're expecting. So, for example, if the joystick at rest is reporting that JOYPAD_AXIS_X is 362 and our deadzone is set to 1000, we'll ignore the value. Similarly, if JOYPAD_AXIS_X is reporting -111 and our deadzone is 1000, we'll ignore a value of greater than -1000.
We're repeating this check for our Right, Up, and Down controls, testing the JOYPAD_AXIS_Y for our Up and Down directions (since these are for pushing the stick up and down, and are therefore stored in a different array index).
If we're not testing a direction control, we'll start checking the other controls. We first grab the values of the keyboard control and the joypad control from the keyControls and joypadControls arrays in Game, and assign them to variables named `key` and `btn`. This is done just to make the next part more readable and isn't strictly necessary. We're then checking whether `key` is not 0 and the value in App's `keyboard` array at index `key` is set. We're also testing whether `btn` is not -1 and if the value at App's joypadButton array at index `btn` is set. If either of these conditions are met, we'll return 1.
Now that we can test which control is in use, we can use the logic in our movement and firing code. Turning to handleMovement:
Where before we would test App's `keyboard` for SDL_SCANCODE_A for moving left, we're now calling isControl and passing in CONTROL_LEFT. This will allow our Gunner to move left whether we press the relevant keyboard key or hold left on our joystick. The same is true of moving right, where we now call isControl and pass over CONTROL_RIGHT. The remainder of the function has also seen similar changes.
handleShoot has also been changed:
Now, instead of testing if the J key on the keyboard has been pressed, we're calling isControl and passing over CONTROL_FIRE.
We're almost done! Before we wrap up, we'll look at how we're saving our control data. Again, this is very similar to what we were doing in SDL2 Shooter 2, so we'll keep things brief. Turning to game.c, we'll start with initGame:
We're setting all Game's joypadControls to -1, to say that no button is assigned to that control.
When it comes to loading our saved game, we've made a small adjustment to the JSON. Starting with saveGame:
Now that we have both keyboard and joystick controls available, we need to save both. We're creating a JSON object named "keyboard", into which we're saving all our keyboard config. We're also creating one named "joypad", in which we're saving all our joypad config. We're grabbing the values from Game's joypadControls at each control index and using cJSON_AddNumberToObject to create a JSON object. With both the keyboard and joypad JSON objects created, we're creating another JSON object named "controls", and adding both keyboard and joypad to it. We're then saving the JSON as normal.
loadGame has also been tweaked, to support the new format:
We're grabbing the "controls" JSON object and pulling out the "keyboard" and "joypad" JSON objects from it, setting Game's keyControls and joypadControls values as needed.
And there we have it! Joypad controls. It might seem like a lot, but remember that we were also making sure the process of configuring the controller is as simple as possible. We also want the user to retain the settings they've selected, so we need to save the joystick config when they're finished.
Only one part to go - the finishing touches! We'll be adding in some music and sound effects, a title screen, and adjusting the HUD.
The source code for all parts of this tutorial (including assets) is available here:
It is also available as part of the SDL2 tutorial bundle (with on-going updates):
If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal. This method will be slower, however, as it will require manual verification of the transaction.