Day 17. Unified Keyboard and Gamepad Input

Day 17. Unified Keyboard and Gamepad Input
Video Length (including Q&A): 1h41

Welcome to “Handmade Hero Notes”, the book where we follow the footsteps of Handmade Hero in making the complete game from scratch, with no external libraries. If you'd like to follow along, preorder the game on handmadehero.org, and you will receive access to the GitHub repository, containing complete source code (tagged day-by-day) as well as a variety of other useful resources.

We were putting away the work on finalizing the input system for long enough. It's not like we don't have anything to do in our program, not at all. But time has come to tighten the screws on the keyboard and gamepad input and finalize it for our cross-platform layer. It's not going to be difficult, just a bit laborious.

Day 16 Day 18

(Top)
Implement Keyboard Input Inside WinMain
  1.1  Window Class, Messages and Callback
  1.2  Move out Keyboard Messages
  1.3  Keyboard Input Storage: First Pass
Finish Directional Stick Code
  2.1  Dead Zone Handling
  2.2  Optimize Stick Value Capture
  2.3  Hook up the Real DPad
  2.4  Add Move Buttons to Keyboard
Improve Keyboard Input Processing
  3.1  Add a Fifth Controller (the Keyboard)
  3.2  Ensure Keyboard State Persistance
  3.3  Add IsConnected Indicator to the Controller
  3.4  Bug Hunting: Buffer Overrun
Recap
Programming Notions
  5.1  Intro to Functional Programming
Navigation

   

Implement Keyboard Input Inside WinMain

If you remember, up until now there was a significant difference between the gamepad input and the keyboard input: keyboard wasn't processed inside WinMain. We had to use the callback function Win32MainWindowCallback, a function completely separated from the rest of our program.

   

Window Class, Messages and Callback

Let's have a quick refresher on “why” we have this problem, so that we can find a solution to it.

In order to create a window in Windows, a program must register the so-called “Window Class”. You might remember the structure WNDCLASSA that we filled out for that purpose.

Yes, but why?

Once we register the window class, Windows will start sending it related messages. These are all sorts of things including, you guessed it, keyboard. Some of these we intercept (inside our PeekMessage loop), “translate” and send to back to Windows.

However, the operating system wants to reserve itself the right to call each window process at its own accord. It might happen, for example, that the program's window becomes covered by another window. In that case Windows would need to redraw that area without really alerting the program which might not even be “active” at that moment.

Here is where the callback function comes into play. With it, Windows “calls us back”, including the message type, related window handle (because a process might have many windows) and a couple parameters that each message fills out as it requires.

This is all nice and good, but it leaves us with the problem. The way Windows forces us to do callbacks, we have no context, it all remains inside WinMain... So we should either use a global variable or do some tricks like using local storage on the window. The latter is actually a thing, Windows allows to store some information related to the window handle (which we get inside the loop). But it's complex, so we'll need to find another way before going for that one.

   

Move out Keyboard Messages

Windows is free to call us back when it wants to, however keyboard messages don't seem to be dispatched like that. This means that we can process them in our main loop, without bothering to go through the callback.

Let's try it. These messages are dispatched in the loop, so we can “peek” into them and see what they are. If they're what we want, we translate them right there. And inside Win32MainWindowCallback we can leave an assertion that would fire if we ever catch messages we shouldn't.

Step 1. Set the stage. Inside the WinMain, we will convert existing message handling to a switch statement, so that it'll have the same structure as in the Win32MainWindowsCallback. The MSG Message contains a member called message which is effectively the same thing you will receive in the callback, so we'll iterate over it. The default case (that is, unless we capture and process the message) will still be translating and dispatching it. The resulting refactoring should be compilable and work the same.

MSG Message;
while (PeekMessageA(&Message, 0, 0, 0, PM_REMOVE))
{
switch(Message.message) { case WM_QUIT: { GlobalRunning = false; } break; // More messages will go here default: { TranslateMessage(&Message); DispatchMessage(&Message); } break; }
}
 Listing 1: [win32_handmade.cpp > WinMain] Converting if block to a switch.

Step 2. Pull out this block to an external function, for ease of readability, and call it from the same spot. Again, no change to the code.

internal void 
Win32ProcessPendingMessages()
{
    MSG Message;
    while (PeekMessageA(&Message, 0, 0, 0, PM_REMOVE))
    {
        switch(Message.message)
        {
            case WM_QUIT:
            {
                GlobalRunning = false;
            } break;
            
            default:
            {
                TranslateMessage(&Message);
                DispatchMessage(&Message);
            } break;
        }
    }
}

// ...
// WinMain 
// ...

while(GlobalRunning)
{
    Win32ProcessPendingMessages();
}
 Listing 2: [win32_handmade.cpp] Exporting the message loop outside of WinMain.

Step 3. Copy the key handling system. You'll notice that you're not compilable if you copy the code as is, because you don't have LParam and WParam any more. Luckily, these are also provided inside MSG structure.

switch(Message.message)
{
    case WM_QUIT:
    {
        GlobalRunning = false;
    } break;
case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM_KEYDOWN: case WM_KEYUP: { u32 VKCode = (u32)Message.wParam; bool IsDown = ((Message.lParam & (1 << 31)) == 0); bool WasDown = ((Message.lParam & (1 << 30)) != 0); if(IsDown != WasDown) { if (VKCode == 'W') { } else if (VKCode == 'A') { } else if (VKCode == 'S') { } else if (VKCode == 'D') { } else if (VKCode == 'Q') { } else if (VKCode == 'E') { } else if (VKCode == VK_UP) { } else if (VKCode == VK_DOWN) { } else if (VKCode == VK_LEFT) { } else if (VKCode == VK_RIGHT) { } else if (VKCode == VK_ESCAPE) { OutputDebugStringA("ESCAPE: "); if (IsDown) { OutputDebugStringA("IsDown "); } if (WasDown) { OutputDebugStringA("WasDown"); } OutputDebugStringA("\n"); } else if (VKCode == VK_SPACE) { } b32 AltKeyWasDown = ((Message.lParam & (1 << 29)) != 0); if((VKCode == VK_F4) && AltKeyWasDown) { GlobalRunning = false; } } } break;
}
 Listing 3: [win32_handmade.cpp > Win32ProcessPendingMessages] Adding the key handling from Win32MainWindowCallback.

Step 4. Remove key handling from Win32MainWindowCallback. Instead we'll leave an assertion which will fire should Windows send us this type of message (which it shouldn't but we'll investigate in case).__

case WM_ACTIVATEAPP:
{
    OutputDebugStringA("WM_ACTIVATEAPP\n");
} break;

case WM_SYSKEYDOWN:
case WM_SYSKEYUP:
case WM_KEYDOWN:
case WM_KEYUP:
{
Assert(!"Keyboard input came in through a non-dispatch message!!!");
bool IsDown = ((LParam & (1 << 31)) == 0); bool WasDown = ((LParam & (1 << 30)) != 0); u32 VKCode = (u32)WParam; if(IsDown != WasDown) { if (VKCode == 'W') { } else if (VKCode == 'A') { } else if (VKCode == 'S') { } else if (VKCode == 'D') { } else if (VKCode == 'Q') { } else if (VKCode == 'E') { } else if (VKCode == VK_UP) { } else if (VKCode == VK_DOWN) { } else if (VKCode == VK_LEFT) { } else if (VKCode == VK_RIGHT) { } else if (VKCode == VK_ESCAPE) { OutputDebugStringA("ESCAPE: "); if (IsDown) { OutputDebugStringA("IsDown "); } if (WasDown) { OutputDebugStringA("WasDown"); } OutputDebugStringA("\n"); } else if (VKCode == VK_SPACE) { } b32 AltKeyWasDown = ((LParam & (1 << 29)) != 0); if((VKCode == VK_F4) && AltKeyWasDown) { GlobalRunning = false; } }
} break;
 Listing 4: [win32_handmade.cpp > Win32MainWindowCallback] Replacing key handling with an assertion.

We're still compilable, and we still have effectively the same functionality as before. Except now we can actually send things to the Win32ProcessPendingMessages function directly! That's some great progress right there.

   

Keyboard Input Storage: First Pass

To get things going, let's assign some function to the buttons. Let's say Escape kills our game, while for the other buttons we'll mimic our gamepad handling. Win32ProcessPendingMessages would receive a controller structure, not dissimilar from what we used for the gamepad. For this purpose we will use another function which we'll call Win32ProcessKeyboardMessage. This would do the work of storing the input.

if (VKCode == 'W')
{
}
else if (VKCode == 'A')
{
}
else if (VKCode == 'S')
{
}
else if (VKCode == 'D')
{
}
else if (VKCode == 'Q')
{
Win32ProcessKeyboardMessage(&KeyboardController->LeftShoulder, IsDown);
} else if (VKCode == 'E') {
Win32ProcessKeyboardMessage(&KeyboardController->RightShoulder, IsDown);
} else if (VKCode == VK_UP) {
Win32ProcessKeyboardMessage(&KeyboardController->Up, IsDown);
} else if (VKCode == VK_DOWN) {
Win32ProcessKeyboardMessage(&KeyboardController->Down, IsDown);
} else if (VKCode == VK_LEFT) {
Win32ProcessKeyboardMessage(&KeyboardController->Left, IsDown);
} else if (VKCode == VK_RIGHT) {
Win32ProcessKeyboardMessage(&KeyboardController->Right, IsDown);
} else if (VKCode == VK_ESCAPE) {
OutputDebugStringA("ESCAPE: "); if (IsDown) { OutputDebugStringA("IsDown "); } if (WasDown) { OutputDebugStringA("WasDown"); } OutputDebugStringA("\n");
GlobalRunning = false;
} else if (VKCode == VK_SPACE) { }
 Listing 5: [win32_handmade.cpp > Win32ProcessPendingMessages] Adding some functionality.

This of course presumes that we pass the function a KeyboardController. Let's do it right away. We need to change the signature of the function, as well as introduce a KeyboardController in WinMain, clear it to zero and pass to our program.

For now, we can use the controller 0 from our NewInput. In order to clear to zero however, we'll need to create a new variable of type game_controller_input, clear that to zero, and then use the contents of that structure to zero out the contents of the KeyboardController:

internal void
Win32ProcessPendingMessages(game_controller_input *KeyboardController)
{ //... } while (GlobalRunning) { game_controller_input *KeyboardController = &NewInput->Controllers[0]; game_controller_input ZeroController = {}; *KeyboardController = ZeroController; Win32ProcessPendingMessages(KeyboardController); // ... }
 Listing 6: [win32_handmade.cpp] Assigning keyboard controller.

This is a bit wanky, so we'll need to revisit it pretty soon.

One last thing that's missing is the Win32ProcessKeyboardMessage we assumed above. Let's create one based on Win32ProcessXInputDigitalButton, except it will be largely simplified: we need to do but a fraction of the work that the latter function does.

internal void
Win32ProcessXInputDigitalButton(DWORD XInputButtonState,
                                game_button_state *OldState, DWORD ButtonBit,
                                game_button_state *NewState)
{
    NewState->EndedDown = ((XInputButtonState & ButtonBit) == ButtonBit);
    NewState->HalfTransitionCount = (OldState->EndedDown != NewState->EndedDown) ? 1 : 0;
}
internal void Win32ProcessKeyboardMessage(game_button_state *NewState, b32 IsDown) { NewState->EndedDown = IsDown; ++NewState->HalfTransitionCount; }
 Listing 7: [win32_handmade.cpp] Defining Win32ProcessKeyboardMessage.

Finally we can compile and run. It... somewhat works. If you repeatedly press the down arrow you will notice offset changing. However, this is less than ideal: we can't zero out everything because the up/down state will be wrong. You need to repeatedly hit the button if you want the game to react to it. That's not how the games work, and we want to be able to hold down a key. This might need requiring more changes. Additionally, right now we're hacking into the existing controller system, and we want a more complete solution.

   

Finish Directional Stick Code

Before we can move on, we need to take a step back and return to the gamepad code which we left somewhat hanging.

   

Dead Zone Handling

As of now, we don't do any dead zone processing for our sticks. What is a dead zone anyway? Contrary to what one might imagine, it's not a game about zombies (there is one, but we aren't talking about it in this instance, anyway). Gamepad sticks are highly inaccurate at the bottom level of precision. Simply put, all the values below a specific threshold are garbage, and the API itself recommends you to filter them out. It's not a bug, it's just the tolerances that a particular controller is designed around. So let's implement... a dead zone, or a region around zero where the values are zero.

This region is defined to be as XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE and XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE, for the left and right sticks, respectively. These are defined as 7,849 and 8,689 which, if you think that the maximum value of a 16-bit signed integer is 32,767, is not an insignificant amount.

Looking back at our XInput reading code inside WinMain:

// ...

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

f32 X;
if(Pad->sThumbLX < 0)
{
    X = (f32)Pad->sThumbLX / 32768.0f;
}
else
{
    X = (f32)Pad->sThumbLX / 32767.0f;
}

f32 Y;
if(Pad->sThumbLY < 0)
{
    Y = (f32)Pad->sThumbLY / 32768.0f;
}
else
{
    Y = (f32)Pad->sThumbLY / 32767.0f;
}

// ...

We can start off by saying that both X and Y start at 0. If the value is smaller than the negative dead zone or greater than the dead zone of the left stick, only then do any operation with it, otherwise leave it at zero:

f32 X = 0; if(Pad->sThumbLX < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE)
{ X = (f32)Pad->sThumbLX / 32768.0f; }
else if(Pad->sThumbLX > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE)
{ X = (f32)Pad->sThumbLX / 32767.0f; }
f32 Y = 0; if(Pad->sThumbLY < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE)
{ Y = (f32)Pad->sThumbLY / 32768.0f; }
else if(Pad->sThumbLY > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE)
{ Y = (f32)Pad->sThumbLY / 32767.0f; }
 Listing 8: [win32_handmade.cpp > WinMain] Ignoring dead zone. Note that in the “less than” comparison the dead zone is negative!

The time has come to compress these two pieces into a utility function, Win32ProcessXInputStickValue. The code in WinMain will become:

f32 X = Win32ProcessXInputStickValue (Pad->sThumbLX, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
if(Pad->sThumbLX < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) { X = (f32)Pad->sThumbLX / 32768.0f; } else if(Pad->sThumbLX > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) { X = (f32)Pad->sThumbLX / 32767.0f; }
f32 Y = Win32ProcessXInputStickValue (Pad->sThumbLY, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
if(Pad->sThumbLY < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) { Y = (f32)Pad->sThumbLY / 32768.0f; } else if(Pad->sThumbLY > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) { Y = (f32)Pad->sThumbLY / 32767.0f; }
// TODO(casey): Min/Max macros!!! NewController->MinX = NewController->MaxX = NewController->EndX = X; NewController->MinY = NewController->MaxY = NewController->EndY = Y;
 Listing 9: [win32_handmade.cpp > WinMain]
internal void
Win32ProcessKeyboardMessage(game_button_state *NewState, b32 IsDown)
{
    NewState->EndedDown = IsDown;
    ++NewState->HalfTransitionCount;
}
internal f32 Win32ProcessXInputStickValue(SHORT Value, SHORT DeadZoneThreshold) { f32 Result = 0; if(Value < -DeadZoneThreshold) { Result = (f32)Value / 32768.0f; } else if(Value > DeadZoneThreshold) { Result = (f32)Value / 32767.0f; } return (Result); }
 Listing 10: [win32_handmade.cpp] Pulling out stick processing code.

At this point we can even make the code better! Looking at it this way we can see that we really should only transform the span from the dead zone end, and not from 0. This way our game will actually see values from zero to whichever point our deadzone is, just remapped in the usable area.

deadzone0---1usablevalues0-----1actualvalues(gameneverseesvaluesinthedeadzone)

 Figure 1: Different ways of mapping the positive X axis.

internal f32 Win32ProcessXInputStickValue(SHORT Value, SHORT DeadZoneThreshold) { f32 Result = 0; if(Value < -DeadZoneThreshold) { Result = (f32)((Value + DeadZoneThreshold) / (32768.0f - DeadZoneThreshold)); } else if(Value > DeadZoneThreshold) { Result = (f32)((Value - DeadZoneThreshold) / (32767.0f - DeadZoneThreshold)); } return (Result); }
 Listing 11: [win32_handmade.cpp] Improving dead zone processing.

If you compile and test now, you should find no differences from where we started, except now our code is more solid.

   

Optimize Stick Value Capture

If you recall, we've sketched out something that looks like quite a complicated analogue stick handling system. Four our X and Y axis, we have a Start, Min, Max and End value. The idea, of course, is that eventually we'll listen to more than one stick position per frame, and then we'll be able to give all these values to the game. Now, it might be a bit premature. What we really need for now is a single Average value per axis. This will be provided only if the input was marked as IsAnalog. At the same time, the primary values of the stick will be captured as Buttons, and any potential rapid movements will be reflected in the half-transition count.

In handmade.h, we currently have the following structure for game_controller_input:

struct game_controller_input
{
    b32 IsAnalog;
    
    f32 StartX;
    f32 StartY;
    
    f32 MinX;
    f32 MinY;
    
    f32 MaxX;
    f32 MaxY;
    
    f32 EndX;
    f32 EndY;
    
    union
    {
        game_button_state Buttons[6];
        struct
        {
            game_button_state Up;
            game_button_state Down;
            game_button_state Left;
            game_button_state Right;
            game_button_state LeftShoulder;
            game_button_state RightShoulder;
        };
    };
};

If we modify it as we said, this structure becomes as below (while we're at it, let's also add Start and Back buttons):

struct game_controller_input
{
    b32 IsAnalog;
    
f32 StickAverageX; f32 StickAverageY;
f32 StartX; f32 StartY; f32 MinX; f32 MinY; f32 MaxX; f32 MaxY; f32 EndX; f32 EndY;
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; // previously up game_button_state ActionDown; // previously down game_button_state ActionLeft; // previously left game_button_state ActionRight; // previously right
game_button_state LeftShoulder; game_button_state RightShoulder;
game_button_state Back; game_button_state Start;
}; }; };
 Listing 12: [handmade.h] Modifying game_controller_input structure.

Securing yourself against modifying Buttons length

If want to make sure you won't forget to update Buttons length when adding or removing new buttons, you can add the following assertion at the beginning of the GameUpdateAndRender:

Assert((&Input->Controllers[0].Start - &Input->Controllers[0].Buttons[0]) == (ArrayCount(Input->Controllers[0].Buttons) - 1));

We take the very last button available in the anonymous struct, subtract its address from the first one and we should receive the same value as the array count of the Buttons array - 1.

You could fool-proof this even further by adding a fake Terminator button at the very end of it and comparing against that. Then you don't even need to subtract 1 (or increase button size).

// handmade.h > game_controller_input
game_button_state Buttons[12]; // don't need to increase button array size!
struct
{
    game_button_state MoveUp;
    //...
    game_button_state Back;
    game_button_state Start;
// NOTE(casey): All buttons should be added above this line game_button_state Terminator;
}; // handmade.cpp > GameUpdateAndRender Assert((&Input->Controllers[0].Terminator - &Input->Controllers[0].Buttons[0]) == (ArrayCount(Input->Controllers[0].Buttons)));

We can also update the change in handmade.cpp right away:

if (Input0->IsAnalog)
{
    // NOTE(casey): Use analog movement tuning
GameState->XOffset += (int)(4.0f * Input0->StickAverageX); GameState->ToneHz = 256 + (int)(128.0f * (Input0->StickAverageY));
} // ...
if(Input0->ActionDown.EndedDown)
{ GameState->YOffset += 1; }
 Listing 13: [handmade.cpp > GameUpdateAndRender] Applying changes to the Game code.

As for the packing of the values, we need to think a second. Updating the now-Action buttons and Start/Back button states should be simple and straightforward:

Win32ProcessXInputDigitalButton(Pad->wButtons,
&OldController->ActionDown,XINPUT_GAMEPAD_A, &NewController->ActionDown);
Win32ProcessXInputDigitalButton(Pad->wButtons,
&OldController->ActionRight,XINPUT_GAMEPAD_B, &NewController->ActionRight);
Win32ProcessXInputDigitalButton(Pad->wButtons,
&OldController->ActionLeft,XINPUT_GAMEPAD_X, &NewController->ActionLeft);
Win32ProcessXInputDigitalButton(Pad->wButtons,
&OldController->ActionUp,XINPUT_GAMEPAD_Y, &NewController->ActionUp);
Win32ProcessXInputDigitalButton(Pad->wButtons, &OldController->LeftShoulder,XINPUT_GAMEPAD_LEFT_SHOULDER, &NewController->LeftShoulder); Win32ProcessXInputDigitalButton(Pad->wButtons, &OldController->RightShoulder,XINPUT_GAMEPAD_RIGHT_SHOULDER, &NewController->RightShoulder);
Win32ProcessXInputDigitalButton(Pad->wButtons, &OldController->Start,XINPUT_GAMEPAD_START, &NewController->Start); Win32ProcessXInputDigitalButton(Pad->wButtons, &OldController->Back,XINPUT_GAMEPAD_BACK, &NewController->Back);
 Listing 14: [win32_handmade.cpp > WinMain] Packing button values from the controller.

As for the stick and Move values, we have our stick that gives us real numbers from -1.0f (left/down) to 1.0f (right/up). We can say that if the stick position (after we've updated the dead zone) is above some random threshold, this button is considered pressed. Let's set the threshold to 0.5f for now, and will be able to fine-tune it later. We'll then take this value and pack it inside our button handling code.

We also want to define IsAnalog only when the stick actually moves.

bool Up = Pad->wButtons & XINPUT_GAMEPAD_DPAD_UP; bool Down = Pad->wButtons & XINPUT_GAMEPAD_DPAD_DOWN; bool Left = Pad->wButtons & XINPUT_GAMEPAD_DPAD_LEFT; bool Right = Pad->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT; NewController->IsAnalog = true; NewController->StartX = OldController->EndX; NewController->StartY = OldController->EndY;
NewController->StickAverageX = Win32ProcessXInputStickValue (
Pad->sThumbLX,XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
NewController->MinX = NewController->MaxX = NewController->EndX = X;
NewController->StickAverageY = Win32ProcessXInputStickValue (
Pad->sThumbLY,XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
NewController->MinY = NewController->MaxY = NewController->EndY = Y;
if ((NewController->StickAverageX != 0.0f) || (NewController->StickAverageY != 0.0f)) { NewController->IsAnalog = true; } f32 Threshold = 0.5f; Win32ProcessXInputDigitalButton((NewController->StickAverageX > Threshold) ? 1 : 0, &OldController->MoveRight, 1, &NewController->MoveRight); Win32ProcessXInputDigitalButton((NewController->StickAverageX < -Threshold) ? 1 : 0, &OldController->MoveLeft, 1, &NewController->MoveLeft); Win32ProcessXInputDigitalButton((NewController->StickAverageY > Threshold) ? 1 : 0, &OldController->MoveUp, 1, &NewController->MoveUp); Win32ProcessXInputDigitalButton((NewController->StickAverageY < -Threshold) ? 1 : 0, &OldController->MoveDown, 1, &NewController->MoveDown);
 Listing 15: [win32_handmade.cpp > WinMain] Updating stick/non-stick values using a “fake DPad”.
   

Hook up the Real DPad

Last, we have a DPad which gives you flat on/off value as any other button does. Usually user uses either one or the other, so we can simply overwrite whatever values DPad had and give it to the stick, before we throw it into the fake DPad code we've added. Moreover, at that point we also want to signal to our game that the input is no longer analog.

if ((NewController->StickAverageX != 0.0f) || (NewController->StickAverageY != 0.0f)) { NewController->IsAnalog = true; }
if (Pad->wButtons & XINPUT_GAMEPAD_DPAD_UP) { NewController->StickAverageY = 1.0f; NewController->IsAnalog = false; } if (Pad->wButtons & XINPUT_GAMEPAD_DPAD_DOWN) { NewController->StickAverageY = -1.0f; NewController->IsAnalog = false; } if (Pad->wButtons & XINPUT_GAMEPAD_DPAD_LEFT) { NewController->StickAverageX = -1.0f; NewController->IsAnalog = false; } if (Pad->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) { NewController->StickAverageX = 1.0f; NewController->IsAnalog = false; }
f32 Threshold = 0.5f; Win32ProcessXInputDigitalButton((NewController->StickAverageY < -Threshold) ? 1 : 0, &OldController->MoveDown, 1, &NewController->MoveDown); Win32ProcessXInputDigitalButton((NewController->StickAverageX > Threshold) ? 1 : 0, &OldController->MoveRight, 1, &NewController->MoveRight); Win32ProcessXInputDigitalButton((NewController->StickAverageX < -Threshold) ? 1 : 0, &OldController->MoveLeft, 1, &NewController->MoveLeft); Win32ProcessXInputDigitalButton((NewController->StickAverageY > Threshold) ? 1 : 0, &OldController->MoveUp, 1, &NewController->MoveUp);
 Listing 16: [win32_handmade.cpp > WinMain] Handling real DPad values.
   

Add Move Buttons to Keyboard

Behind all these changes we come out with a free benefit! We can immediately handle keyboard's WASD keys as if they were movement buttons. Let's do it right away!

if (VKCode == 'W')
{
Win32ProcessKeyboardMessage(&KeyboardController->MoveUp, IsDown);
} else if (VKCode == 'A') {
Win32ProcessKeyboardMessage(&KeyboardController->MoveLeft, IsDown);
} else if (VKCode == 'S') {
Win32ProcessKeyboardMessage(&KeyboardController->MoveDown, IsDown);
} else if (VKCode == 'D') {
Win32ProcessKeyboardMessage(&KeyboardController->MoveRight, IsDown);
} else if (VKCode == 'Q') { Win32ProcessKeyboardMessage(&KeyboardController->LeftShoulder, IsDown); } else if (VKCode == 'E') { Win32ProcessKeyboardMessage(&KeyboardController->RightShoulder, IsDown); } else if (VKCode == VK_UP) {
Win32ProcessKeyboardMessage(&KeyboardController->ActionUp, IsDown);
} else if (VKCode == VK_DOWN) {
Win32ProcessKeyboardMessage(&KeyboardController->ActionDown, IsDown);
} else if (VKCode == VK_LEFT) {
Win32ProcessKeyboardMessage(&KeyboardController->ActionLeft, IsDown);
} else if (VKCode == VK_RIGHT) {
Win32ProcessKeyboardMessage(&KeyboardController->Action, IsDown);
} else if (VKCode == VK_ESCAPE) { GlobalRunning = false; } else if (VKCode == VK_SPACE) {
Win32ProcessKeyboardMessage(&KeyboardController->Start, IsDown);
}
else if (VKCode == VK_BACK) { Win32ProcessKeyboardMessage(&KeyboardController->Back, IsDown); }
 Listing 17: [win32_handmade.cpp > Win32ProcessPendingMessages] Adding more keyboard buttons.

While we're at it, let's add some non-analog code to test out if it works:

if (Input0->IsAnalog)
{
    // NOTE(casey): Use analog movement tuning
    //...
}
else
{
    // NOTE(casey): Use digital movement tuning
if (Input0->MoveLeft.EndedDown) { GameState->XOffset -= 1; } if (Input0->MoveRight.EndedDown) { GameState->XOffset += 1; }
}
 Listing 18: [handmade.cpp > GameUpdateAndRender] Adding non-analog movement.

And... that's it. That just works. The keyboard now behaves effectively as if it was another controller. Let's go ahead and tighten the final screws on it.

   

Improve Keyboard Input Processing

We are now in position to finally start tightening down the code for the keyboard processing.

   

Add a Fifth Controller (the Keyboard)

If you recall, we have defined 4 controllers that our game would eventually loop through and pick the active one. So let's add a fifth controller which will be our keyboard. It's as simple as cranking up a number in handmade.h:

struct game_input
{
game_controller_input Controllers[5];
};
 Listing 19: [handmade.h] Adding an additional controller.

We already started using keyboard as controller 0, so let's keep it at that. Meanwhile for the gamepads, we'll simply store their input in the subsequent controllers.

When we read input from the controllers, we'll introduce our own controller index which will be offset by one. We'll also need to offset the MaxControllerCount to ensure that we always have the correct amount.

DWORD MaxControllerCount = 1 + XUSER_MAX_COUNT;
if(MaxControllerCount > ArrayCount(NewInput->Controllers)) { MaxControllerCount = ArrayCount(NewInput->Controllers); } for (DWORD ControllerIndex = 0; ControllerIndex < MaxControllerCount; ++ControllerIndex) {
DWORD OurControllerIndex = ControllerIndex + 1;
game_controller_input *OldController = &OldInput->Controllers[OurControllerIndex]; game_controller_input *NewController = &NewInput->Controllers[OurControllerIndex];
 Listing 20: [win32_handmade.cpp > WinMain] Moving over other controllers.

While we're at it, we could also go ahead and modify handmade.cpp to check input from all the controllers. For all values but ToneHz we're adding up, so the change should propagate automatically.

for (int ControllerIndex = 0; ControllerIndex < ArrayCount(Input->Controllers); ++ControllerIndex) {
game_controller_input *Controller = &Input->Controllers[ControllerIndex]; if (Controller->IsAnalog)
{ // NOTE(casey): Use analog movement tuning
GameState->XOffset += (int)(4.0f * Controller->StickAverageX); GameState->ToneHz = 256 + (int)(128.0f * (Controller->StickAverageY));
} else { // NOTE(casey): Use digital movement tuning
if (Controller->MoveLeft.EndedDown)
{ GameState->XOffset -= 1; }
if (Controller->MoveRight.EndedDown)
{ GameState->XOffset += 1; } }
if(Controller->Down.EndedDown)
{ GameState->YOffset += 1; }
}
 Listing 21: [handmade.cpp > GameUpdateAndRender] Reading from controllers.

To better visualize this new structure, take a look at the figure below:

WindowsAPIHandmadeHerogame_input_controllerKeyboardControllersController0XInputAPIController1Gamepad0Controller2Gamepad1Controller3Gamepad2Controller4Gamepad3...

 Figure 2: Input Packaging scheme for our game.

Truth is, if we've done our platform work correctly, game won't care if the input is coming from a keyboard, gamepad, or whichever input method we will define in the future. The game will only care about what is the value of the “left” or “start” or what have you. Maybe in the future we'll want to do something more complicated than that, but for now this system will be more than enough.

   

Ensure Keyboard State Persistance

Now that we have a permanent place for our keyboard, let's think about its state. Currently, we clear the it to zero at the start of each frame. So if a key is hit, we receive the message, we set EndedDown correctly, we even pass it to the game... and then we promptly discard this value on the next pass. We want the key to appear down until the user releases the key. So what we really want to do is to only reset HalfTransitionCount of each button state, EndedDown should remain set. Let's look at our keyboard clear code again:

game_controller_input *KeyboardController = &NewInput->Controllers[0];
game_controller_input ZeroController = {};
*KeyboardController = ZeroController;

In our gamepad code, we were swapping and OldInput and NewInput. We can use the same structures here, as well. Simply copying over the EndedDown value will suffice. While we're at it, we can also get rid of the ZeroController and reinitialize the controller directly. It's a newer C++ feature but it should be pretty simply and straightforward in usage to prevent us any issues.

game_controller_input *OldKeyboardController = &OldInput->Controllers[0];
game_controller_input *NewKeyboardController = &NewInput->Controllers[0];
game_controller_input ZeroController = {};
*NewKeyboardController = {};
for (int ButtonIndex = 0; ButtonIndex < ArrayCount(NewKeyboardController->Buttons); ++ButtonIndex) { NewKeyboardController->Buttons[ButtonIndex].EndedDown = OldKeyboardController->Buttons[ButtonIndex].EndedDown; }
Win32ProcessPendingMessages(NewKeyboardController);
 Listing 22: [win32_handmade.cpp > WinMain] Bringing over EndedDown.

Hopefully you can see what's going on here. We still do the full clear of the new state but also preserve the pointer to the previous state and then copy the pieces we're interested in.

Now, when we come in to Win32ProcessPendingMessages, we will already have a structure with the previous state preserved. We also want to add an assertion to Win32ProcessKeyboardMessage to make sure that the two states are different.

internal void
Win32ProcessKeyboardMessage(game_button_state *NewState, b32 IsDown)
{
Assert(NewState->EndedDown != IsDown);
NewState->EndedDown = IsDown; ++NewState->HalfTransitionCount; }
 Listing 23: [win32_handmade.cpp] Adding an additional assertion.

You can now compile and see that you can keep holding the arrow down key while your gradient will keep scrolling!

Why the assertion?

You might ask: Why do we even need to do this assertion? Isn't it double work? We already check in Win32ProcessPendingMessages that IsDown != WasDown before we even call Win32ProcessKeyboardMessage.

First, it doesn't cost us anything to add this assertion. During the final build of the game (when we remove HANDMADE_SLOW flag from the compiler) the assertions will disappear anyway. But during the development we're future-proofing our code, so that if it does change, we can catch unintended functionality right away. In this case, we want to make sure that our half-transition counts aren't messed up since recording a same event twice might lead to dire consequences in the game.

   

Add IsConnected Indicator to the Controller

While we're at it, let's have a new indicator inside our game_input_controller structure, so that to read only from the connected controllers (and avoid the frame rate hit on older XInput libraries). We'll start from expanding our struct definition:

struct game_controller_input
{
b32 IsConnected;
b32 IsAnalog; // ... }
 Listing 24: [handmade.h] Introducing IsConnected to the game_controller_input.

For now, our keyboard will be always connected:

game_controller_input *OldKeyboardController = &OldInput->Controllers[0];
game_controller_input *NewKeyboardController = &NewInput->Controllers[0];
*NewKeyboardController = {};
NewKeyboardController->IsConnected = true;
for (int ButtonIndex = 0; ButtonIndex < ArrayCount(NewKeyboardController->Buttons); ++ButtonIndex) { NewKeyboardController->Buttons[ButtonIndex].EndedDown = OldKeyboardController->Buttons[ButtonIndex].EndedDown; } Win32ProcessPendingMessages(NewKeyboardController);
 Listing 25: [win32_handmade.cpp > WinMain] Setting IsConnected for the keyboard.

As for the gamepads, we already have a system that does the checking for us. Once we're in, we can immediately mark that controller as connected. However, because we're cycling controller states, we should also remember to mark the disconnected controllers as false:

if (XInputGetState(ControllerIndex, &ControllerState) == ERROR_SUCCESS)
{
// NOTE(casey): This controller is plugged in NewController->IsConnected = true;
// TODO(casey): See if ControllerState.dwPacketNumber increments too rapidly XINPUT_GAMEPAD *Pad = &ControllerState.Gamepad; NewController->IsAnalog = true; // ... } else { // NOTE(casey): This controller is not available NewController->IsConnected = false; }
 Listing 26: [win32_handmade.cpp > WinMain] Setting IsConnected for the gamepads.
   

Bug Hunting: Buffer Overrun

If you compile and run now, you'll notice that everything will behave correctly until at a certain point, when you will crash due to the buffer overrun. We'll start hunting for the error right away, but in the meantime let's make ourselves a small helper function whose purpose would be to hand out the controllers as requested, provided they are within the allocated bounds. We'll add this function in handmade.h:

struct game_controller_input
{
    b32 IsConnected;
    // ... 
};

struct game_input
{
    game_controller_input Controllers[5];
};
inline game_controller_input *GetController(game_input *Input, int ControllerIndex) { Assert(ControllerIndex < ArrayCount (Input->Controllers)); game_controller_input *Result = &Input->Controllers[ControllerIndex]; return (Result); }
 Listing 27: [handmade.h] Defining GetController.

We can now go anywhere we're trying to pull the controllers from and use this function instead:

for (int ControllerIndex = 0; ControllerIndex < ArrayCount(Input->Controllers); ++ControllerIndex) {
game_controller_input *Controller = GetController(Input, ControllerIndex);
// ... }
 Listing 28: [handmade.cpp > GameUpdateAndRender]
game_controller_input *OldKeyboardController = GetController(OldInput, 0); game_controller_input *NewKeyboardController = GetController(NewInput, 0);
*NewKeyboardController = {};
 Listing 29: [win32_handmade.cpp > WinMain]
DWORD OurControllerIndex = ControllerIndex + 1;
game_controller_input *OldController = GetController(OldInput, OurControllerIndex); game_controller_input *NewController = GetController(NewInput, OurControllerIndex);
 Listing 30: [win32_handmade.cpp > WinMain] Using GetController to access controller

You can temporarily rename Controllers in the game_input structure to quickly find the places where this array is used.

Ok, we've set up our traps, now let's go bug hunting. Compile, run the game and... you'll crash right away, and at the assertion we've just set up no less! If you click up on the Call Stack, you'll notice that the error happened when we tried to set OldController but we went out of (array's) bounds. If you think about what we did in the last few sections, the error should become immediately apparent to you: we've added 1 to the wrong place.

When we were designing our controller system, we introduced a MaxControllerCount to make sure that we never go outside our borders. However, in the subsection 3.1, when we added the keyboard controller we also bumped this maximum count by one. Once we go inside the loop, we start accessing Controllers n+1, i.e. 1, 2, 3, 4, 5. (instead of 0, 1, 2, 3). What we really intended is this:

DWORD MaxControllerCount = XUSER_MAX_COUNT; if(MaxControllerCount > (ArrayCount(NewInput->Controllers) - 1))
{
MaxControllerCount = (ArrayCount(NewInput->Controllers) - 1);
}
 Listing 31: [win32_handmade.cpp > WinMain] Fixing Buffer overflow bug.
   

Recap

Today we went through a lot of refactoring but we can finally say that our input system is solid enough to move on. We will of course return to it once we need more features but for now it will suffice. We can move on to new and exciting things next time.

   

Programming Notions

   

Intro to Functional Programming

When programming, there're two prevalent programming paradigms and therefore ways of treating functions.

In the object-oriented programming, coding revolves around the idea of “objects”. They are constructed (mostly out of a series of structs), operate and are destroyed as if they were actual entities.

In such a context, functions are used almost exclusively as “methods”, i.e. related to very specific usage in service of those structs. They usually only take a reference to the object they report to, maybe a few more parameters to affect it with, but what happens inside is invisible to you. State of something may change. Memory may be overwritten. You don't know what's going on.

What this means is that you can overwrite something that's not visible to you. Calling the function with side effects changes the permanent data store in a program in a way that, say, if you call that function twice in a row or in different order, those calls would yield different results.

As an example we have seen, think about DirectSound. You don't even see the functions you need to call: these are passed to you as pointers inside the DirectSound structs (a recurring theme in Object-Oriented Programming in and of itself).

On the other hand, think about a function which doesn't have any side effects or internal repercussions. Think about a sine function: you ask it a sine of a given number, it returns the sine of that number. This value will never change if the input never changes, no matter what. A programming paradigm which is constructed on such functions is called functional programming.

Where possible, we will be trying to employ functional programming throughout this course. While we will still have our game state which will be most likely altered in some way, it's a good ideal to thrive towards, as the limited hidden functionality (if any at all) will prevent you many a headache later down the line. The understandability and the reliability of the program will increase if you use functional programming. It's not our objective to make our code purely functional but maintaining the focus on it is important.

   

Navigation

Previous: Day 16. Visual Studio Compiler Switches

Next: Day 18. Enforcing a Video Frame Rate

Back to Index

Glossary

Articles

Functional Programming

Getting started with XInput

formatted by Markdeep 1.10