Some notes on implementing player input in video games.
I've seen many games and even engines that implement player input processing in a way that makes it very hard to run unit tests on input consuming controllers and that create unnecessary dependencies from the game's movement code to whatever input library or engine is being used.
This gist shows a better way of adding input processing to your game.
Many games do things like this:
public: virtual void HandleInput() override {
// Accessing input devices via static wrappers
bool shouldShoot = Input::IsKeyDown(KeyCode::LeftControl);
// Accessing input devices via managers
bool shouldJump = this->inputManager->IsKeyDown(KeyCode::Space);
}
This will later require the developer to track down any place where input devices are queried to add support for, i.e. game pads or VR.
Supporting dynamic key bindings this way will require the key binding code to be spread out across dozens of classes in which it didn't belong in the first place.
Unit tests will depend on input devices being simulated in their entirety and be susceptible to failure if different input bindings are loaded or the default bindings are changed.
Abstracting input often leads to 'Actions' being looked up directly in the controller classes like this:
public: virtual void HandleInput() override {
// Querying inputs by name
float horizontalMovement = this->inputManager->GetAxis("Horizontal");
// Accessing input actions by name
const IAction &throttleState = this->inputManager->Actions["Throttle"];
float throttle = throttleState.AnalogValue;
}
This will create a dependeny from your game code in the input manager, making it hard to change or replace the input manager.
Using string lookups consumes needless CPU cycles, storing actions adds additional complexity and possibly life cycle management into your controllers.
Unit tests will have to simulate the entire input manager rather to provide mock inputs to your controller classes.
Create an input state structure that stores each possible input to the game by a descriptive name:
public: virtual void HandleInput(PlatformerInputState inputState) override {
// Analog inputs
this->velocity.x += inputState.Horizontal;
// Digital inputs
if(this->inputState.Jump.PressedThisFrame) {
jump();
}
}
This makes it very clear what is being checked.
Controllers have no dependency on a specific type of input manager.
Unit tests can simply assign the inputs to the input state structure to mock input states when testing your controllers.
Input state structures can be derived to support common implementations, for example of platformer movement code that can then be specialized on a case-per-case basis:
struct PlatformerInputState {
/// <summary>Controls horizontal movement of the character</summary>
public: float Horizontal;
/// <summary>Causes the character to jump when grounded</summary>
public: Trigger Jump;
}
struct AwesomeGameInputState : public PlatoformerInputstate {
/// <summary>Use special ability that only this game has</summary>
public: Trigger UseAwesomeAbility;
}
Using Reflection or RTTI (in that case, combined with hand-written glue code), different input states can be filled before being provided to controllers dynamically.