Efficient input bindings

Hello again! Today, I want to quickly touch on how I handled input bindings in Halogen, and later on provide a bit of info on what’s being worked on now, and what to expect in the near future.

So, Halogen uses RawInput for its lowest-level keyboard and mouse handling. Using a keyboard with RawInput only provides us with a message that a key was pressed or released (and if you’re thinking about using RawInput, it should be noted that decoding that message can be a huge pain). Since RawInput doesn’t support any kind of polling (which would probably be too slow for realtime gameplay anyway), we have to use these messages to manually keep track of the state of each key. The same goes for the mouse, RawInput will provide a message with only the info that’s necessary, and it’s up to our input handler to make sure we properly track that information.

Before we get too much further, I want to explain what bindings are, and their value. First, here’s the terminology I use for discussing input in Halogen:

  • Action: any input that has a distinct “on” and “off” state, such as keys and mouse buttons.
  • Axis: any input whose value can vary, such as mouse position, thumbstick position, etc.
  • Trigger: an Action or Axis that is part of a Binding
  • Binding: A construct that maps a user-friendly ID to a set of Triggers

So, in Halogen jargon, a Binding is simply a mapping between some ID, and a set of triggers. Bindings are useful because 1) they allow us to map the same in-game action to different triggers without testing several inputs in our game-code (for example, you might want your game’s Reload binding to trigger from Keyboard R and Gamepad X), and 2) they provide a simple way for us to allow players to customize their bindings – and in this day and age, that should absolutely be a standard feature in any PC game.

One naive approach to implementing input bindings might look something like this:

struct ActionBinding
{
    const char* m_BindingName;
    Action m_Trigger;
    bool m_State;
}

void RegisterActionBinding(const char* bindingName, Action trigger)
{
    // Add the binding to some kind of map
    ActionBinding binding;
    binding.m_BindingName = bindingName;
    binding.m_Trigger = trigger;
    binding.m_State = false;

    m_ActionBindings[m_RegisteredBindings++] = binding;
}
...
void UpdateBindings()
{
    // please don't actually do this in your code...
    for (unsigned i = 0; i < m_RegisteredBindings; ++i)
    {
        Binding& binding = m_ActionBindings[i];
        binding.m_State = Core::Input::GetActionState(binding.m_Trigger);
    }
}
...
bool IsActionPressed(const char* bindingName)
{
    Binding& binding = GetBindingByName(bindingName);

    return binding.m_State;
}

It works, sure, but string comparisons are hardly quick enough for us to be using them so frequently. Bare in mind, inputs are tested several times every frame, which means we want it to be fast. In that regard, string comparisons won’t do. It’s also not particularly efficient to be storing the state of the binding in the binding itself; the state of each trigger is already stored in the main input handler, there’s no need to double up.

In order to optimize, let’s think about some of the traits of a binding system. First off, we know that each binding will probably need a user-friendly name, for UI elements. Next, we know that each binding will be (or at least, can be) unique from all others. Finally, at least for Halogen, I decided that having a distinct upper limit on the number of bindings allowed can help with optimizing. Knowing this, I came up with something like the following for Halogen:

struct Binding
{
    std::uint32_t m_BindingID;
    char* m_BindingName;
    Action m_Triggers[3];
}

void RegisterActionBinding(std::uint32_t bindingId, const char* name, Action defaultValue)
{
    m_ActionBindings[bindingId].m_BindingId = bindingId;
    m_ActionBindings[bindingId].m_BindingName = name;
    m_ActionBindings[bindingId].m_Triggers[0] = defaultValue;
}

bool IsPressed(std::uint32_t bindingId)
{
    bool state1 = Core::Input::GetActionState(m_ActionBindings[bindingId].m_Triggers[0]);

    // Get the state of the other triggers, if they're bound

    return state1 || state2 || state3;
}

const char* GetUiName(std::uint32_t bindingId)
{
    return m_ActionBindings[bindingId].m_BindingName;
}
...

enum MyGameBindings : std::uint32_t
{
    ACTION_RELOAD,
    ACTION_JUMP,
    ACTION_SHOOT
}

void Start()
{
    InputManager::RegisterActionBinding(ACTION_RELOAD, "Reload", KEY_R);
}

void MyObjectUpdate()
{
    bool reload = InputManager::IsPressed(ACTION_RELOAD);
    if (reload)
    {
        // Do reload stuff
    }
}

The binding layer keeps a simple array of a very small ActionBinding object, which is updated based on the info the user feeds in. The above is obviously pseudocode; the actual code in Halogen has error protection and verifies inputs to prevent reading unregistered bindings. It also uses the fixed-string implementation I detailed in a previous post instead of c-style strings, and it supports several other types of checks such as IsReleased, IsJustPressed (which is only true if any of the binding’s triggers were activated this frame), IsJustReleased, GetHoldTime, and more.

Users can create a simple enum of their input bindings, register them along with a string identifier, and poll with the enum values. Another huge upside to this is the bindings don’t store the state of their triggers; they just translate an ID to a handful of triggers, and push the trigger check to the lower-level input handler. As a result, this provides a relatively transparent layer through which we can access the state of our game’s inputs without hard-coding bindings into the game.

In the future, I intend to improve the way bindings are built, such that Axis and Action triggers can both be used on the same binding. For example, a keyboard would use Actions like WASD to move, but a gamepad would use a thumbstick, which is Axis. The goal is making the player’s choice of input transparent to the game logic, and eventually, that will be supported. Additionally, once the file system is complete, this system will support saving to and loading from a file.

A small roadmap

I want to quickly note some of the things I’ve been working on behind the scenes, and touch on where I want to take the engine next. As of right now, I’ve spent the vast majority of my time on the input and memory management code for Halogen; these two things give me a solid foundation on which to begin building other parts of the engine. I’ve also fleshed out the app entry and main game loop, and a simple logging system has been built.

Right now, my major focus is Halogen’s job manager. One of my goals with this engine is to take advantage of multithreading where possible; rather than using dedicated threads for certain major parts of the engine, like physics and audio, I’ve chosen to go with a job system; this allows each different module to queue small tasks that run asynchronously. The task system is coming along nicely; I hope to finish it up and write a short article on it soon, but it’s pretty standard stuff I imagine. Once that’s done, I plan to take a quick swing at file handling and get a very simple asset handler put together.

Coming up after the job manager is easily the most exciting part of the engine for me: rendering. I have always loved writing rendering code; one of the first major things I wrote was a deferred shading pipeline for an open-source Java game engine when I was 16. Writing shaders, reading about complex new rendering approaches, all of that stuff fascinates me. The problem is, I often want to dive into writing render code right off the bat. Last year, I wrote a simple engine in C# and the first thing I worked on was rendering. I quickly realized that the renderer in this engine was not so much a stand-alone module as it should be, but deeply woven into every part of the engine. So with Halogen, I’ve consciously put in effort to work on other aspects of the engine first, and to build a foundation for the renderer to stand on its own.

Thanks again for reading!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s