Skip to content

Instantly share code, notes, and snippets.

@Cygon
Last active October 26, 2017 14:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Cygon/f1346b7b2db5233b14777bd6c7785828 to your computer and use it in GitHub Desktop.
Save Cygon/f1346b7b2db5233b14777bd6c7785828 to your computer and use it in GitHub Desktop.
Dealing with Player Input in Games

Dealing with Player Input in Games

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.

DO NOT: Query input devices directly in your controllers

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.

DO NOT: Access an 'Input Manager' in your controllers

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.

DO: Pass your input state via a descriptive structure

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.

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