Handmade Penguin Chapter 17: Unified Keyboard and Gamepad Input

This chapter covers roughly the content in the Unified Keyboard and Gamepad Input part of the Handmade Hero course, under the Linux operating system.

<-- Chapter 16 | Back to Index | Chapter 18 -->

Be warned! There are a number of changes we have to make to our build file to keep up to date. Because we enabled warnings-are-errors mode in the last chapter, we now have to make sure we have no warnings. Unfortunately, gcc has some extra warnings Visual Studio doesn't, so we'll need to pass the -Wno-sign-compare option to make handmade.cpp compile with the latest changes.
We'll also want to add -std=gnu++11 to tell the compiler we want to support C++11 features and non-standard extensions. This will let us use things like the = {} initialisation syntax.

Updating our Controller Input

If you've played around with our gamepad input, you might have noticed that it falls short of perfection in a couple of ways. Firstly, we're going to want to allow the user to use the D-Pad rather than the analogue stick, should they so desire. Indeed: we're going to want to rethink our game_controller_input structure to better match the way our game is going to work.

Casey's been pretty tight-lipped about the game, but we know that we're going to need two directional inputs: one for movement and one for actions. A lot of these inputs are, in a sense, binary: we're either performing an action to the left, or not. We can then also provide an intensity or magnitude value, if we want to preserve the analogue information.

Our new game_controller_input structure looks like this:

struct game_controller_input
{
    bool32 IsConnected;
    bool32 IsAnalog;
    real32 StickAverageX;
    real32 StickAverageY;

    union
    {
        game_button_state Buttons[12];
        struct
        {
            game_button_state MoveUp;
            game_button_state MoveDown;
            game_button_state MoveLeft;
            game_button_state MoveRight;

            game_button_state ActionUp;
            game_button_state ActionDown;
            game_button_state ActionLeft;
            game_button_state ActionRight;

            game_button_state LeftShoulder;
            game_button_state RightShoulder;

            game_button_state Back;
            game_button_state Start;

            // NOTE(casey): All buttons must be added above this line

            game_button_state Terminator;
        };
    };
};

We now need to update our code to fill this in correctly. The mapping we're using is:

To start off with, we're going to need some way of mapping an axis to a (pair of) button(s). What we're going to need is a threshold value: if the axis is above the threshold, it triggers the button press, otherwise it won't. Our SDLProcessGameControllerButton() function isn't quite up to the task, however. It accepts the SDL controller handle and button index, so we can't just pass our own threshold value in. Let's alter it to simply take a boolean instead:
internal void
SDLProcessGameControllerButton(game_button_state *OldState,
                               game_button_state *NewState,
                               bool Value)
{
    NewState->EndedDown = Value;
    NewState->HalfTransitionCount += ((NewState->EndedDown == OldState->EndedDown)?0:1);
}
We then need to alter all of our calls to it, as they need to use the new button names in the game_controller_input structure and the new SDLProcessGameControllerButton() function. For example, the A button handler would become:
SDLProcessGameControllerButton(&(OldController->ActionDown),
                               &(NewController->ActionDown),
                               SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_A));

Now we could handle our analogue sticks. We'll pick a threshold (0.5 is good), and simply pass it through to our SDLProcessGameControllerButton() function:

real32 Threshold = 0.5f;
SDLProcessGameControllerButton(&(OldController->MoveLeft),
                               &(NewController->MoveLeft),
                               NewController->StickAverageX < -Threshold);
Easy! We'll also set the IsAnalog variable to true if the analogue stick is not zero.
if((NewController->StickAverageX != 0.0f) ||
   (NewController->StickAverageY != 0.0f))
{
    NewController->IsAnalog = true;
}

Now let's get the D-Pad working. We want to have it set up to match the analogue stick. What we'll do, therefore, is have them just set StickAverage{X,Y} to 1.0 or -1.0 if pressed. We'll also set IsAnalog to 0:

if(SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_DPAD_UP))
{
    NewController->StickAverageY = 1.0f;
    NewController->IsAnalog = false;
}

Getting in the (Dead) Zone

Despite sounding like a B-grade Sci-Fi flick, "the Dead Zone" is not a seething horde of zombies but rather a seething horde of workarounds for inexact hardware. Joysticks (including the analogue sticks in gamepads) will often be constructed in such a way that their axes do not sit firmly on "0" when left to their own devices. The deadzone is simply a range of values that the program should interpret as the axis being left in its neutral position.

Every device will be different, and there's no reliable way of determining what your deadzone should be. As mentioned in the stream, Microsoft provides values on MSDN for its own XInput controllers. For other controllers, one typically has to either guess or perform some calibration.

We'll handle the deadzone in exactly the same way as we did on Windows, by introducing a SDLProcessGameControllerAxisValue() function which both does the normalising we were already doing and sets any value inside the deadzone to 0:

internal real32
SDLProcessGameControllerAxisValue(int16 Value, int16 DeadZoneThreshold)
{
    real32 Result = 0;

    if(Value < -DeadZoneThreshold)
    {
        Result = (real32)((Value + DeadZoneThreshold) / (32768.0f - DeadZoneThreshold));
    }
    else if(Value > DeadZoneThreshold)
    {
        Result = (real32)((Value - DeadZoneThreshold) / (32767.0f - DeadZoneThreshold));
    }

    return(Result);
}

Keyboard Input

Stange as it may sound, not everyone has a gamepad. We're going to need some other way of controlling our game and the keyboard is a rather common implement for PC users. Fortunately, we already have code for handling keyboard events, we just need to make it update a game_controller_input structure.

To start with, we'll need to have a game_controller_input structure to pass. We currently have an array of four of them, to handle the four controllers. Let's just bump that up to five in handmade.h.

We'll make our code use index 0 for the keyboard and indices 1 to 4 for any actual controllers. We'll simply add 1 to ControllerIndex when setting up NewController and OldController

We can then set up OldKeyboardController and NewKeyboardController variables:

game_controller_input *OldKeyboardController = &OldInput->Controllers[0];
game_controller_input *NewKeyboardController = &NewInput->Controllers[0];
*NewKeyboardController = {};
for(int ButtonIndex = 0;
    ButtonIndex < ArrayCount(NewKeyboardController->Buttons);
    ++ButtonIndex)
{
	NewKeyboardController->Buttons[ButtonIndex].EndedDown =
	OldKeyboardController->Buttons[ButtonIndex].EndedDown;
}
You'll notice that we also have to carry the EndedDown flag over. Otherwise, we'd think the button had been released every frame.

We're already checking our keyboard state in HandleEvent(), though we're not doing anything with it. Let's pass NewKeyboardController to HandleEvent(). What we do from there is practically the same as on Windows: we need to set the EndedDown variable for each button to the correct value and increment HalfTransitionCount. We'll move that out into a SDLProcessKeyPress() function:

internal void
SDLProcessKeyPress(game_button_state *NewState, bool32 IsDown)
{
    Assert(NewState->EndedDown != IsDown);
    NewState->EndedDown = IsDown;
    ++NewState->HalfTransitionCount;
}
We can then fill in our HandleEvent() function with calls:
if(KeyCode = SDLK_w)
{
    SDLProcessKeyPress(&NewKeyboardController->MoveUp, IsDown);
}
...and so on. Compile, and we'll have keyboard control. How exciting!

The Connectedness Of All Things

At some point, we'll want to know which controllers in our game_input structure are actually connected. We'll do this by adding a IsConnected boolean to the game_controller_input structure and setting it accordingly.

The first thing we'll do is set up the Keyboard Controller: it's always going to be available, so we'll just set:

NewKeyboardController->IsConnected = true;
when we initialise it.

For the other controllers, we can simply set IsConnected to true after we've checked the ControllerHandle is not 0 and that SDL_GameControllerGetAttached() returned true.

Finally, we'll want to make sure we don't accidentally try to access a controller that doesn't exist at all. We can do that with an assertion, which we'll wrap in a function in handmade.h for convenience:

inline game_controller_input *GetController(game_input *Input, int unsigned ControllerIndex)
{
    Assert(ControllerIndex < ArrayCount(Input->Controllers));

    game_controller_input *Result = &Input->Controllers[ControllerIndex];
    return(Result);
}
We can then use GetController() in place of &Input->Controllers[i] wherever it appears.

End of Lesson!

That's our input done! We'll probably come back at the end to do some crazy keyboard layout stuff (which will be so much better and easier in SDL than in Windows), but for now we need not think of input ever again.

Sorry for the delay: with some luck the next few chapters will be coming out more quickly!

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 16 | Back to Index | Chapter 18 -->