Even stunning visuals and perfect mechanics fall flat if controls feel mushy. After building input systems for everything from platformers to fighting games, I've learned that input handling is deceptively complex—and absolutely critical to game feel. Here's everything you need to build an input system that feels tight and responsive.
Table of Contents
- Input System Fundamentals
- Architecture Patterns
- Input Buffering
- Key Rebinding
- Multi-Device Support
- Advanced Techniques
- Common Mistakes
- FAQ
Input System Fundamentals
An input system translates physical player actions (button presses, stick movements) into game actions (jump, attack, move). The gap between pressing a button and seeing the result determines how responsive your game feels.
Core Concepts
- Input Events: Raw data from hardware (key pressed, button released, axis moved)
- Actions: Game-meaningful inputs (Jump, Attack, MoveRight)
- Bindings: Mappings from input events to actions
- Context: Different input meanings in different game states (menus vs gameplay)
Input States
Every input has three states you'll query constantly:
- Pressed: True only on the frame the input begins
- Held: True every frame the input is active
- Released: True only on the frame the input ends
// Common query pattern
if (Input.GetButtonDown("Jump")) // Pressed this frame
if (Input.GetButton("Jump")) // Currently held
if (Input.GetButtonUp("Jump")) // Released this frame
Architecture Patterns
The Action Map Pattern
Instead of checking raw inputs directly, define abstract actions that can be bound to any input:
// Define actions
enum GameAction { Jump, Attack, MoveLeft, MoveRight, Pause }
// Map inputs to actions
Dictionary<KeyCode, GameAction> keyBindings = new Dictionary<KeyCode, GameAction>() {
{ KeyCode.Space, GameAction.Jump },
{ KeyCode.Z, GameAction.Attack },
{ KeyCode.A, GameAction.MoveLeft },
{ KeyCode.D, GameAction.MoveRight },
{ KeyCode.Escape, GameAction.Pause }
};
// Query actions, not raw inputs
if (inputSystem.IsActionPressed(GameAction.Jump)) {
player.Jump();
}
Benefits of Action Maps
- Rebinding: Change controls without touching gameplay code
- Multi-device: Same action triggered by keyboard, controller, or touch
- Clarity: Gameplay code reads intent, not hardware specifics
Input Context/Modes
Different game states need different input handling:
enum InputContext { Gameplay, Menu, Dialogue, Cutscene }
// Gameplay context
if (context == InputContext.Gameplay) {
// Jump button jumps
}
// Menu context
else if (context == InputContext.Menu) {
// Jump button selects menu item
}
Input Buffering
Input buffering is the secret weapon for responsive game feel. Instead of discarding inputs that arrive slightly early, buffer them and execute when valid.
Why Buffer?
Players anticipate actions. If they press jump a few frames before landing, they expect to jump immediately upon landing—not to have the input ignored. Buffering captures this intent.
Basic Buffer Implementation
class InputBuffer {
Queue<BufferedInput> buffer = new Queue<BufferedInput>();
float bufferDuration = 0.15f; // 150ms window
void AddInput(GameAction action) {
buffer.Enqueue(new BufferedInput(action, Time.time));
}
GameAction? ConsumeBufferedInput() {
while (buffer.Count > 0) {
var input = buffer.Peek();
// Input too old, discard
if (Time.time - input.timestamp > bufferDuration) {
buffer.Dequeue();
continue;
}
// Valid input found
buffer.Dequeue();
return input.action;
}
return null;
}
}
Buffer Timing Guidelines
- Platformers: 100-200ms buffer for jumps and attacks
- Fighting games: 150-300ms for combo inputs
- Action RPGs: 100-150ms for responsiveness without sloppiness
- Precision games: 50-100ms or disable entirely
The Over-Buffering Trap
Too much buffering creates "sticky" controls where actions execute after you've moved on mentally. If players complain controls feel "laggy" or "delayed," your buffer might be too long. Start small and increase based on testing.
Key Rebinding
Players expect to customize controls. A solid rebinding system is table stakes for PC games.
Rebinding Architecture
class RebindableInput {
Dictionary<GameAction, List<InputBinding>> bindings;
void Rebind(GameAction action, InputBinding newBinding) {
// Remove old binding if it exists elsewhere
RemoveBindingFromAllActions(newBinding);
// Add new binding
bindings[action].Add(newBinding);
// Save to persistent storage
SaveBindings();
}
void ResetToDefaults() {
bindings = LoadDefaultBindings();
SaveBindings();
}
}
Rebinding UI Flow
- Player selects action to rebind
- Show "Press any key" prompt
- Capture next input event
- Check for conflicts with existing bindings
- Apply new binding or prompt to resolve conflict
- Save to persistent storage
Conflict Resolution
When a player binds a key that's already used:
- Swap: Exchange bindings between the two actions
- Clear: Remove the binding from the other action
- Cancel: Abort the rebind operation
- Allow duplicates: Some games permit the same key for multiple actions
Multi-Device Support
Abstraction Layer
Create a unified input interface that works across devices:
interface IInputSource {
bool IsPressed(GameAction action);
bool IsHeld(GameAction action);
bool IsReleased(GameAction action);
Vector2 GetAxis(GameAction action);
}
class KeyboardInput : IInputSource { /* keyboard implementation */ }
class GamepadInput : IInputSource { /* gamepad implementation */ }
class TouchInput : IInputSource { /* touch implementation */ }
Device Switching
Detect which device the player is using and update UI prompts accordingly:
void Update() {
if (AnyKeyboardInputThisFrame()) {
currentDevice = InputDevice.Keyboard;
UpdateUIPrompts("Press Space to Jump");
}
else if (AnyGamepadInputThisFrame()) {
currentDevice = InputDevice.Gamepad;
UpdateUIPrompts("Press A to Jump");
}
}
Simultaneous Input
Some players use keyboard and mouse simultaneously, or switch mid-game. Your system should handle this gracefully without forcing a single device.
Advanced Techniques
Input Prediction
For networked games, predict input to reduce perceived latency:
// Apply input immediately locally
ApplyInputLocally(input);
// Send to server
SendInputToServer(input);
// Reconcile when server confirms/denies
void OnServerResponse(InputResult result) {
if (result.rejected) {
RollbackAndReapply();
}
}
Combo Detection
Detect sequences of inputs within a time window:
class ComboDetector {
List<GameAction> inputHistory;
float comboWindow = 0.5f;
bool CheckCombo(GameAction[] sequence) {
// Find sequence in recent input history
// within the combo window timeframe
}
}
// Usage
if (comboDetector.CheckCombo(new[] { Down, DownRight, Right, Punch })) {
ExecuteHadouken();
}
Analog Input Processing
Joystick and trigger inputs need processing:
// Deadzone - ignore small inputs
float ApplyDeadzone(float value, float deadzone = 0.15f) {
if (Mathf.Abs(value) < deadzone) return 0;
return Mathf.Sign(value) * (Mathf.Abs(value) - deadzone) / (1 - deadzone);
}
// Response curve - adjust sensitivity
float ApplyResponseCurve(float value, float exponent = 2.0f) {
return Mathf.Sign(value) * Mathf.Pow(Mathf.Abs(value), exponent);
}
Input Recording and Playback
Useful for replays, tutorials, and testing:
class InputRecorder {
List<TimestampedInput> recording;
void StartRecording() { recording.Clear(); isRecording = true; }
void StopRecording() { isRecording = false; }
void RecordInput(GameAction action, float timestamp) {
recording.Add(new TimestampedInput(action, timestamp));
}
IEnumerator Playback() {
foreach (var input in recording) {
yield return new WaitForSeconds(input.timestamp - lastTime);
SimulateInput(input.action);
}
}
}
Common Mistakes
Checking Input in FixedUpdate
Input events are captured per-frame, but FixedUpdate runs at a fixed rate. Checking GetButtonDown in FixedUpdate can miss inputs. Capture input in Update, apply physics in FixedUpdate.
Not Handling Focus Loss
When the game loses focus, release all held inputs. Otherwise players alt-tab back to find their character still running.
Ignoring Frame-Rate Independence
Buffer durations and combo windows should use real time, not frame counts. A 10-frame buffer means different things at 30fps vs 144fps.
Hardcoding Bindings
Never check KeyCode.Space directly in gameplay code. Always go through an action mapping layer. Your future self will thank you when adding controller support.
Frequently Asked Questions
Should I use the engine's built-in input system or build my own?
Use built-in systems (Unity's Input System, Unreal's Enhanced Input) for most projects. Build custom only when you need behavior these systems can't provide. The built-in systems handle edge cases you won't think of.
How do I handle input during loading screens?
Either disable input entirely during loads, or buffer important inputs (like skip buttons) and process them when loading completes. Don't let inputs queue up and execute unexpectedly.
What's the difference between polling and event-based input?
Polling checks input state every frame (Input.GetKey). Event-based fires callbacks when inputs occur. Polling is simpler; events are more efficient for complex systems. Most engines support both.
How do I support both WASD and arrow keys?
Map both to the same action. Your action map should support multiple bindings per action. Default to common layouts (WASD + Arrows for movement) and let players rebind.
Summary
A responsive input system separates good games from great ones. Abstract raw inputs into actions, implement input buffering for forgiving timing, support key rebinding, and handle multiple devices through a unified interface. Test your buffer durations extensively—too short feels unforgiving, too long feels laggy. The goal is to translate player intent into game action with zero perceived delay.


