Input Systems
Summary
An input system is the layer that turns raw hardware signals — a key press, a mouse move, a controller trigger pull — into meaningful gameplay actions like "jump" or "fire." Rather than gameplay code checking "is the spacebar down" directly, a well-designed input system maps physical devices to named actions once, so gameplay code asks "was Jump pressed" and never needs to know or care whether that came from a keyboard, controller, or touch screen.
Why This Matters
Code that checks a specific key directly — if (Input.GetKeyDown(KeyCode.Space)) Jump(); — ties gameplay logic permanently to one specific input device and
binding. Supporting a controller, letting the player rebind keys, or
porting to a platform with different input hardware then means hunting
down and rewriting every single direct key check throughout the codebase.
An input system that maps physical inputs to named actions once (Jump,
Fire, Interact) means gameplay code is written against those stable
action names — adding controller support or rebindable keys becomes a
change in one mapping layer, not a rewrite of gameplay logic.
Mental Model
An input system is a universal translator, not a direct wire from key to action. Two people speaking different languages can still communicate through a translator who converts each person's words into a shared, understood meaning — neither speaker needs to learn the other's language. An input system does the same thing between hardware and gameplay code: whatever physical device the player is using gets translated into the same shared vocabulary of named actions, and gameplay code only ever needs to understand that shared vocabulary.
Core Explanation
- Polling checks input state on demand, every frame.
if (Input.GetKey(KeyCode.W))asks "is this currently held?" once per frame inside the game loop — simple, but ties the check to being run every frame it matters. - Event-driven input notifies code when something happens. Rather than asking every frame, code subscribes to a callback ("notify me when Jump is pressed") and reacts only when that specific event actually occurs — built directly on delegates/events (see [[CON-0023]]).
- Action mapping decouples physical input from gameplay meaning.
Defining a
Jumpaction bound to spacebar (keyboard) and the South button (controller) lets gameplay code respond to "Jump," letting the mapping layer — not gameplay code — decide which physical inputs currently trigger it. - Input buffering smooths out timing-sensitive input. Recording a button press for a short window (even if the exact frame it's "needed" hasn't arrived yet — a jump pressed slightly before landing) makes controls feel more responsive than a strict frame-perfect check would.
Syntax / Theory
Engine-neutral (action mapping concept)
Action "Jump" ← bound to: Keyboard[Space], Gamepad[ButtonSouth]
Action "Fire" ← bound to: Mouse[LeftButton], Gamepad[RightTrigger]
Gameplay code asks: "Was Jump triggered this frame?" — never asks about Space directly.
C# (Unity — polling, legacy Input)
void Update()
{
if (Input.GetKeyDown(KeyCode.Space)) Jump(); // Tied directly to one specific key.
}
C# (Unity — Input System package, action-based)
public class PlayerInputHandler : MonoBehaviour
{
public InputActionReference jumpAction;
void OnEnable() => jumpAction.action.performed += ctx => Jump(); // Event-driven, decoupled.
}
C++ (Unreal — Enhanced Input)
void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
EnhancedInput->BindAction(JumpAction, ETriggerEvent::Triggered, this, &APlayerCharacter::Jump);
}
}
Both engines have moved toward the same action-mapping model shown here — Unity's Input System package and Unreal's Enhanced Input both bind named actions to configurable physical inputs, rather than checking specific keys directly throughout gameplay code.
Unreal Blueprint
An Input Action asset bound in the Enhanced Input framework exposes a
"Jump Triggered" event node in Blueprint, matching the C++ delegate
binding without requiring code.
Practical Example
A rebindable control scheme stores the mapping separately from the gameplay logic that reacts to it.
public class InputBindings
{
public Dictionary<string, KeyCode> Bindings = new()
{
{ "Jump", KeyCode.Space },
{ "Interact", KeyCode.E },
};
}
public class PlayerController : MonoBehaviour
{
public InputBindings bindings;
void Update()
{
if (Input.GetKeyDown(bindings.Bindings["Jump"])) Jump();
if (Input.GetKeyDown(bindings.Bindings["Interact"])) Interact();
}
}
Changing what key triggers "Jump" means editing one dictionary entry — no gameplay logic anywhere else needs to know or change.
Common Mistakes
- Checking specific keys directly throughout gameplay code, instead of through a named action layer — this is the exact coupling an input system is meant to remove, and it makes rebinding or adding controller/touch support require touching every call site.
- Using polling (
GetKey) for something that should be a discrete, one-time event. CheckingGetKey(held) instead ofGetKeyDown(pressed this frame) for a "fire one shot" action fires repeatedly for as long as the button is held, not once per press. - Ignoring input buffering for timing-sensitive actions, leading to inputs feeling "dropped" when pressed slightly before or after the exact frame the game was ready to accept them — especially noticeable in precise platformers or fighting games.
- Reading input outside of the appropriate per-frame timing. Reading
input state inside
FixedUpdate(which runs on its own fixed schedule, not every rendered frame) can miss or double-count presses that occur between fixed updates — input should typically be read once per regular frame and consumed by fixed-rate systems afterward.
Game Development Example
A combo-attack system uses buffered, event-driven input to accept an attack input slightly ahead of when the animation is ready to consume it.
public class ComboSystem : MonoBehaviour
{
private float bufferedInputTime = -1f;
private const float BufferWindow = 0.2f;
void OnAttackPressed() => bufferedInputTime = Time.time; // Recorded, not acted on immediately.
void OnAnimationReadyForNextHit()
{
if (Time.time - bufferedInputTime <= BufferWindow)
PerformNextComboHit(); // The buffered press still counts.
}
}
Without buffering, a player who presses attack a fraction of a second too early would have that input silently dropped — buffering makes the combo system feel responsive despite imperfect human timing.
Best Practices
- Route all gameplay input through named actions, never raw key/button checks scattered through gameplay code.
- Use event-driven input for discrete actions (jump, fire) and polling only for genuinely continuous state (movement axis held down).
- Add a short input buffer for time-sensitive actions where strict frame-perfect timing would feel unresponsive to players.
- Keep the input-to-action mapping in one clearly separate layer so rebinding, controller support, or new platforms only require changes there.
Exercises
- Refactor a
PlayerControllerthat checksInput.GetKeyDown(KeyCode.Space)directly into one that checks a named"Jump"entry in anInputBindingsdictionary instead. - Take the
ComboSystemexample and reduceBufferWindowto0f, then describe, in terms of player experience, what changes about how forgiving the combo timing feels. - You're adding controller support to a game originally built with direct keyboard checks throughout. Decide whether to retrofit an action mapping layer or add parallel controller-specific checks alongside the existing keyboard ones, and justify the trade-off in maintenance cost.
- Research the difference between "event vs. polling" input handling in more depth, and explain one scenario where polling is actually the more appropriate choice despite an action-mapping system being available.
Related Concepts
- Game Loop — input is read once per game loop iteration, one of the loop's core per-frame responsibilities alongside update and render.
- Delegates — event-driven input handling is built directly on the delegate/event mechanism, subscribing gameplay code to "notify me when this action occurs."
- Game State — input handling commonly checks current game state (only
responding to movement input while
Playing), tying these two concepts together in practice.
Further Reading
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules, which this document follows.- Unity documentation — Input System package: https://docs.unity3d.com/Packages/com.unity.inputsystem@latest
- Unreal Engine documentation — Enhanced Input: https://dev.epicgames.com/documentation/en-us/unreal-engine/enhanced-input-in-unreal-engine