Handling Player Input
As I’ve added more and more game mechanics to Flex, I’ve experimented with different input methods a player can use to interact with the world besides the typical ‘Press E to interact’ (spinning a valve by rotating a joystick is one alternative method). So far I’ve added support for keyboard, mouse, and XBOX controllers, but I will likely further expand the number of supported devices in the future (midi keyboards anyone?). The code I have been using to handle each input method is quite messy and needed to be cleaned up. This post will cover the process I went through to get where I’m at today with the system.
To see why I felt this change was necessary, take a look at how I was checking for a jump input:
if (g_InputManager->IsGamepadButtonPressed(m_Index, GamepadButton::A) ||
(g_InputManager->bPlayerUsingKeyboard[m_Index] &&
g_InputManager->GetKeyPressed(KeyCode::KEY_SPACE)))
{
// ...
}
Clearly not good in a number of ways:
1. The player can not edit keybindings. This is the first item on the “basic” category on the great Game Accessibility Guidelines, so I’d say it’s a necessary feature to have if you have any desire to cater toward disabled gamers, plus it’s a useful feature for able gamers alike.
2. That’s simply too much code for catching an action event, and it will only grow longer as more input methods are added. This will inevitably lead to typos and copy-paste errors.
3. Single events could be handled multiple times by different systems leading to, for example, the player walking forward when ‘W’ was pressed, even when a text box showing above the play area already handled the keypress.
Abstraction
To get around the first two issues noted above, we can add a layer of abstraction between the usage code and the input events. (It’s the Fundamental theorem of software engineering after all!)
Once a mapping exists between game-specific-actions (like MOVE_FORWARD
and INTERACT
) and inputs (like KeyCode::KEY_W
and GamepadButton::A
), the usage code can be simplified to just check for an Action
and players will be able to remap keys as they like.
To implement this mapping, I created an enumeration which defines all possible actions that can be taken in-game. I also created an InputBinding
struct which holds all necessary info about which actual inputs each action maps to. The mapping process is then simply indexing into the list using an Action
as the index.
enum class Action
{
MOVE_LEFT,
MOVE_RIGHT,
MOVE_FORWARD,
MOVE_BACKWARD,
INTERACT,
// ...
_NONE
};
struct InputBinding
{
KeyCode keyCode = KeyCode::_NONE;
MouseButton mouseButton = MouseButton::_NONE;
MouseAxis mouseAxis = MouseAxis::_NONE;
GamepadButton gamepadButton = GamepadButton::_NONE;
GamepadAxis gamepadAxis = GamepadAxis::_NONE;
bool bNegative = false;
};
InputBinding m_InputBindings[(i32)Action::_NONE + 1];
Now, rather than that mess shown earlier, we can catch jump event as follows:
if (g_InputManager->GetActionPressed(Action::JUMP))
{
// ...
}
For the curious, see how GetActionPressed
is implemented here.
This system addresses the first two issues noted above, but to solve the third problem (event handling duplication) we’ll need go deeper.
Callbacks
In order to allow one system to “consume” an event, thereby preventing other systems from also handling it, I added a callback system for each event type.
To allow an event caller to call member functions on miscellaneous types, I created an abstract class that contains one virtual function. The event caller can maintain a list of listeners using this abstract type, and call the function without knowing anything about the subclass that implements it and keeps a reference to the object which the member function should be called on. The syntax is a little strange, but it works.
enum class EventReply
{
CONSUMED,
UNCONSUMED
};
class ICallbackMouseButton
{
public:
virtual EventReply Execute(MouseButton button, KeyAction action) = 0;
};
template<typename T>
class MouseButtonEventCallback : public ICallbackMouseButton
{
public:
using CallbackFunction = EventReply(T::*)(MouseButton, KeyAction);
MouseButtonEventCallback(T* obj, CallbackFunction fun) :
mObject(obj),
mFunction(fun)
{
}
virtual EventReply Execute(MouseButton button, KeyAction action) override
{
return (mObject->*mFunction)(button, action);
}
private:
CallbackFunction mFunction;
T* mObject;
};
The trickiest part about that code was the using
declaration syntax, but luckily I know a template wizard who helped me out.
I defined similar classes for mouse move & keyboard events, but I’ll leave them out for the sake of brevity.
To register a callback, a system has to define a function matching the signature of the callback, as well as an instance of the subclassed callback object. This instance takes the type of the listener as a template argument, which is how it’s able to call the member function you point it towards.
EventReply OnMouseButtonEvent(MouseButton button, KeyAction action);
MouseButtonEventCallback<DebugCamera> mouseButtonCallback;
Because the callback object has no default constructor you must initialize it in the constructor of the listener. You must also bind and unbind at the appropriate times:
DebugCamera::DebugCamera() :
mouseButtonCallback(this, &DebugCamera::OnMouseButtonEvent)
{
}
void DebugCamera::Initialize()
{
g_InputManager->BindMouseButtonEventCallback(&mouseButtonCallback);
}
void DebugCamera::Destroy()
{
g_InputManager->UnbindMouseButtonEventCallback(&mouseButtonCallback);
}
EventReply DebugCamera::OnMouseButtonEvent(MouseButton button, KeyAction action)
{
if (button == MouseButton::LEFT && action == KeyAction::PRESS)
{
// ...
return EventReply::CONSUMED;
}
return EventReply::UNCONSUMED;
}
The binding/unbinding functions simply add and remove entries into the list of listeners.
std::vector<ICallbackMouseButton*> m_MouseButtonCallbacks;
When an event is generated, the event caller can iterate over the listeners until one listener consumes it, at which point the propagation stops.
// Called by OS callback on mouse button press
void InputManager::MouseButtonCallback(MouseButton mouseButton,
KeyAction action, i32 mods)
{
// ...
for (auto iter = m_MouseButtonCallbacks.rbegin();
iter != m_MouseButtonCallbacks.rend();
++iter)
{
if ((*iter)->Execute(mouseButton, action) == EventReply::CONSUMED)
{
break;
}
}
}
This system nicely handles the third issue I noted at the start, but it ignores the first issue! To solve all three, I added yet another callback, this time for Action
events. Determining when to call these took a bit of fiddling, especially since I wanted to keep the other callbacks. This was made more complex by the priority system I had since added which determines the order in which the callbacks are called. Priority is determined simply by an integer specified at event bind-time. (diff) You can find the code in its entirety on GitHub if you’d like to dig through it further.
Conclusion
I would prefer to be able to store the callback objects in the event caller classes so that each listener doesn’t require an extra member, but I don’t believe that’s possible without a reflection system (which I’m not keen enough on to bother implementing). Maybe one of these days someone will release a decent programming language with reflection support…
While implementing this system I was very wary of compile times, and knowing that templates and modern C++ classes are known for being slow in several senses, I tried to keep things as simple as I could. However, I think there’s still some work to be done in that regard. With that said, this system feels like a big step forward and I’ve been really enjoying cleaning up the old code to use it.