Game input system development guide with controller and keyboard

How to Build an Input System for Your Game (Complete Guide)

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

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

  1. Player selects action to rebind
  2. Show "Press any key" prompt
  3. Capture next input event
  4. Check for conflicts with existing bindings
  5. Apply new binding or prompt to resolve conflict
  6. 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.

Sign up for email updates (coming soon)

A quarterly roundup of the best things from