Skip to content

Instantly share code, notes, and snippets.

@JustSlavic
Last active June 8, 2023 20:25
Show Gist options
  • Save JustSlavic/0f3570dec2d738aa6a4b624c7fed5000 to your computer and use it in GitHub Desktop.
Save JustSlavic/0f3570dec2d738aa6a4b624c7fed5000 to your computer and use it in GitHub Desktop.

Data oriented input in games

Many game engines implement inputs in a more OOP style: you have an object, that represents an input device, and you can query its state. Something like this:

class input_device {};

class mouse : public input_device
{
    vector2 position() { /* ... */ }
    bool get_key_state(key k) { /* ... */ }
    // ...
};

But I don't like OOP so I don't like it.

Another way to do input is to send events to the game like this:

while(have_pending_events())
{
    event *e = get_event();
    switch(e->type)
    {
        case EVENT_KEYBOARD_W: // ...
        case EVENT_KEYBOARD_A: // ...
        case EVENT_KEYBOARD_S: // ...
        case EVENT_KEYBOARD_D: // ...
        // ...
    }
}

I do not like this either. Do you know why? Because I already did it in the platform layer!

For example, this is the Win32 code that does the same thing:

MSG message;
while (PeekMessageA(&message, 0, 0, 0, PM_REMOVE))
{
    TranslateMessage(&message);
    switch (message.message)
    {
        case WM_SYSKEYDOWN:
        case WM_SYSKEYUP:
        case WM_KEYDOWN:
        case WM_KEYUP:
        {
            uint32 virtual_key_code  = (uint32) message.wParam;
            bool32 alt_down = (message.lParam & (1 << 29)) != 0;
            bool32 was_down = (message.lParam & (1 << 30)) != 0;
            bool32 is_down  = (message.lParam & (1 << 31)) == 0;

            switch (virtual_key_code)
            {
                case 'W': // ...
                case 'A': // ...
                case 'S': // ...
                case 'D': // ...
        // ...

So you would just be translating one type of event into another, and that's just not what I want to do.

I like the other approach where you think of the input just as the data. You still store the state of an input device, but you do it in a format that is more like an answer to the question: "what happened during the last frame?"

How to store it

And the answer to that question is, some buttons went up, some buttons went down, the mouse went in a certain direction, or the stick on the gamepad went in a certain direction.

I store this data like this:

struct button_state
{
    uint32 transition_count;
    bool32 is_down; 
};

struct mouse_device
{
    enum key_ {
        LMB, MMB, RMB, // ...
        KEY_COUNT
    };
    
    button_state keys[KEY_COUNT];
};

How to use it

We can use this data however we want. We can advance our game forward, serialize it, store it as a file, send it over the network and it will be trivial to do, unlike the OOP example above.

The most common operation would be to determine whether a key was pressed or held during the frame. This distinction is very important. Some actions we want to do once, and then stop, like shooting a revolver. Other actions we want to continue after holding a key, such as shooting from a riffle.

I use three functions to do this:

uint32 get_press_count(button_state button)
{
    uint32 result = (button.transition_count + (button.is_down > 0)) / 2;
    return result;
}

uint32 get_release_count(button_state button)
{
    uint32 result = (button.transition_count - (button.is_down > 0) + 1) / 2;
    return result;
}

uint32 get_hold_count(button_state button)
{
    uint32 result = (button.transition_count + (button.is_down > 0) + 1) / 2;
    return result;
}

I don't think I can prove to you that these functions work, but I claim they do!

Let's look at the different situations:

Press and release on the same frame

The corresponding button_state state is:

button_state{
  transition_count = 2,
  is_down = false
}

So these functions will produce the following results:

get_press_count(mouse[mouse::LMB]);   // = (button.transition_count + (button.is_down > 0)) / 2
                                      // = (2 + 0) / 2
                                      // = 1

get_release_count(mouse[mouse::LMB]); // = (button.transition_count - (button.is_down > 0) + 1) / 2
                                      // = (2 - 0 + 1) / 2
                                      // = 1

get_hold_count(mouse[mouse::LMB]);    // = (button.transition_count + (button.is_down > 0) + 1) / 2
                                      // = (2 + 0 + 1) / 2
                                      // = 1

Note that I count the one frame click as a hold, so events like shooting from a riffle or walking would also count!

Only press on the frame

get_press_count(mouse[mouse::LMB]);   // = (button.transition_count + (button.is_down > 0)) / 2
                                      // = (1 + 1) / 2
                                      // = 1

get_release_count(mouse[mouse::LMB]); // = (button.transition_count - (button.is_down > 0) + 1) / 2
                                      // = (1 - 1 + 1) / 2
                                      // = 0

get_hold_count(mouse[mouse::LMB]);    // = (button.transition_count + (button.is_down > 0) + 1) / 2
                                      // = (1 + 1 + 1) / 2
                                      // = 1

Here we have 0 releases which is makes sense.

Only release on the frame

get_press_count(mouse[mouse::LMB]);   // = (button.transition_count + (button.is_down > 0)) / 2
                                      // = (1 + 0) / 2
                                      // = 0

get_release_count(mouse[mouse::LMB]); // = (button.transition_count - (button.is_down > 0) + 1) / 2
                                      // = (1 - 0 + 1) / 2
                                      // = 1

get_hold_count(mouse[mouse::LMB]);    // = (button.transition_count + (button.is_down > 0) + 1) / 2
                                      // = (1 + 0 + 1) / 2
                                      // = 1

Here we have 0 presses, but have 1 release instead. Notice how in all three examples we had hold.

I claim that these functions scale to any number of presses and releases that happened in the frame.

Conclusion

Basically, that's how I do input. This is really easy to use, easy to implement. I just had to spend a little bit of time to figuring out these weird functions, but they work and using them is really easy. In game code I just do this:

if (get_press_count(input->keyboard[keyboard::ESC]))
{
    // Code to do when ESC is pressed
}

References

All credit for this idea of storing input as data goes to Casey Muratori, this is the link you could see him doing this in Handmade Hero: https://handmadehero.org

I just spiced it up a bit with my little functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment