Handmade Penguin Chapter 13: Platform-independent User Input

This chapter covers roughly the content in the Platform-indpendent User Input part of the Handmade Hero course, under the Linux operating system.

<-- Chapter 12 | Back to Index | Chapter 14 -->

State of Play: Cross-platform Game State

There's something slightly ugly about passing our BlueOffset, GreenOffset and ToneHz parameters between our platform layer and our game code. There are two reasons we have been doing so:

  1. The values need to persist between calls to GameUpdateAndRender().
  2. They are affected by input, which is still entirely in our platform layer.
The first one is easy to fix: let's remove BlueOffset, GreenOffset and ToneHz parameters and make them local_persist (which, if you remember, is an alias for static) variables in our GameUpdateAndRender() function:
local_persist int BlueOffset = 0;
local_persist int GreenOffset = 0;
local_persist int ToneHz = 256;

To clean things up a bit, we'll also take all of our platform-specific structs and move them into a sdl_handmade.h file, which we'll include in sdl_handmade.cpp. This won't actually change anything, but it means all of those definitions will be in the same place when we want to look at them.

If we now run our code, we note that none of our input works any more. On the bright side, our code is now storing its game state in the cross-platform game code. Our next challenge will be reconstructing our input handling code in a cross-platform manner.

Designing the Input Interface

To match our video and sound code, we'll want to pass, each frame, some block of data that represents the user's input during the last frame. There are a couple of ways we could do this:

  1. Have a list of events.
  2. Some "summary" of the state over the last frame.
We'll be going with the latter, as it makes it easier to allow an unlimited number of input events, as well as making analogue inputs simpler.

Our game input structure is going to be quite complicated:

struct game_button_state
	int HalfTransitionCount;
	bool32 EndedDown;

struct game_controller_input
	bool32 IsAnalog;

	real32 StartX;
	real32 StartY;

	real32 MinX;
	real32 MinY;

	real32 MaxX;
	real32 MaxY;

	real32 EndX;
	real32 EndY;

		game_button_state Buttons[6];
			game_button_state Up;
			game_button_state Down;
			game_button_state Left;
			game_button_state Right;
			game_button_state LeftShoulder;
			game_button_state RightShoulder;

struct game_input
	game_controller_input Controllers[4];
That's actually quite nasty, so let's go through it: We now have a union. This basically lets us access each button as either an element in the Buttons array or as a named member. Our buttons are implemented as instances of the game_button_state struct:

Implementing the Input Interface with SDL

We now need some way of filling in this structure. Let's start with the buttons. Because all of the buttons behave similarly, we'll write a SDLProcessGameControllerButton() function to update our game_button_state struct. Given that we need to know how many transitions there are, we'll need to know the previous state of the button. We'll then need to write a new game_button_state. We'll also need a pointer to the SDL_GameController handle and the SDL_GameControllerButton This is what our function will look like:

internal void
SDLProcessGameControllerButton(game_button_state *OldState,
                               game_button_state *NewState,
                               SDL_GameController *ControllerHandle,
                               SDL_GameControllerButton Button)
    NewState->EndedDown = SDL_GameControllerGetButton(ControllerHandle, Button);
    NewState->HalfTransitionCount += ((NewState->EndedDown == OldState->EndedDown)?0:1);
Now we just need to call it. First, though, we'll need to have some game_input instances to put the data in. We'll do this by having two game_input instances: one for storing the previous input frame, and one for storing the current input frame. This double buffering exists so that we always have access to the OldState of the controller buttons:
game_input Input[2] = {};
game_input *NewInput = &Input[0];
game_input *OldInput = &Input[1];

Now, in the loop over all of our controllers, let's take pointers to the old and new states of the current controller:

game_controller_input *OldController = &OldInput->Controllers[ControllerIndex];
game_controller_input *NewController = &NewInput->Controllers[ControllerIndex];
We can then replace our
bool LeftShoulder = SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_LEFTSHOULDER);
calls with

We also need to handle the axes: this is not too difficult. We simply have to get the axis value (exactly as we did before), normalise it into the range [-1.0,1.0] and set the Start, End, Min and Max variables. Since we're only polling for input once per frame, we don't have to do much work to calculate those.

NewController->StartX = OldController->EndX;
NewController->StartY = OldController->EndY;

int16 StickX = SDL_GameControllerGetAxis(ControllerHandles[ControllerIndex], SDL_CONTROLLER_AXIS_LEFTX);
int16 StickY = SDL_GameControllerGetAxis(ControllerHandles[ControllerIndex], SDL_CONTROLLER_AXIS_LEFTY);

if (StickX < 0)
    NewController->EndX = StickX / 32768.0f;
    NewController->EndX = StickX / 32767.0f;

NewController->MinX = NewController->MaxX = NewController->EndX;

if (StickY < 0)
    NewController->EndY = StickY / 32768.0f;
    NewController->EndY = StickY / 32767.0f;

NewController->MinY = NewController->MaxY = NewController->EndY;
You might wonder why we have to divide by 32768.0 to normalise our number if it is negative, but 32767.0 if it's positive. This is just one of those strange flukes of signed integers. A 16-bit signed integer holds numbers between -32768 and 32767 inclusive. That's is because 0 counts as one of the positive numbers, so there isn't room for 32768. Another thing you might notice is that we're not setting Min and Max properly. This isn't much of a big deal yet, as we're not polling more than once a frame. We'll fix it when we need to.

Be warned! SDL treats the axes of game controllers differently to XInput: The directions of the axes are reversed. We should probably make all of those constants negative if we want to be compatible with the windows version. As an interesting aside, if you're running SDL on Windows, and SDL is using XInput behind the scenes, this means that the most negative value possible is -32767, not -32768 in that situation. In most other situations, though, -32768 is fair game.

As these are analogue controllers, we also need to set IsAnalog to true. Finally, we need to pass it to our GameUpdateAndRender function:

GameUpdateAndRender(NewInput, &Buffer, &SoundBuffer);
and swap our old and new input buffers:
game_input *Temp = NewInput;
NewInput = OldInput;
OldInput = Temp;

Check it out! Instead of polling, we could use the SDL events API. SDL provides many useful events for using game controllers, and they will operate at the operating system's polling speed where possible.

End of Lesson!

Our controller input is done! Next we'll be looking into getting keyboard input working, and maybe start looking into something fun like memory management.

If you've bought Handmade Hero, the source for the Linux version can be downloaded here. Note that you will require the official source code for handmade.h and handmade.cpp.

<-- Chapter 12 | Back to Index | Chapter 14 -->