Skip to content
C++
Domain Deep-Dive
Intermediate

Input System Design in C++ Games

Designing a game input system in C++: raw input, action mapping, gamepad support, input buffering, and frame-accurate input timing.

Input system layers

cpp
Physical layer      Abstraction layer       Game layer
─────────────       ─────────────────       ──────────
Keyboard scan       InputAction mapping     Jump pressed?
Gamepad axis    →   "Jump" = [Space,    →   is_jumping = true
Mouse delta         Gamepad A, ...]         move_dir = {1, 0}
Touch points        "Move" = [WASD,
                    Left stick]

Never scatter raw key checks (IsKeyDown(KEY_SPACE)) throughout game logic — use action names.


Action-based input

cpp
enum class InputAction : uint32_t {
    Jump, Attack, Crouch, MoveForward, MoveRight,
    // ...
};

struct InputState {
    bool pressed  : 1;  // became pressed this frame
    bool held     : 1;  // held for >1 frame
    bool released : 1;  // became released this frame
    float value;        // for axes: -1.0 to 1.0
};

class InputSystem {
    std::unordered_map<InputAction, InputState> m_states;
    std::unordered_map<InputAction, std::vector<int>> m_key_bindings;
    std::unordered_map<InputAction, int> m_gamepad_bindings;

public:
    void Bind(InputAction action, int key) {
        m_key_bindings[action].push_back(key);
    }

    void Update(const RawInputFrame& raw) {
        for (auto& [action, state] : m_states) {
            bool was_held = state.held || state.pressed;
            bool now_down = IsActionDown(action, raw);

            state.pressed  = !was_held && now_down;
            state.held     =  was_held && now_down;
            state.released =  was_held && !now_down;
            state.value    = GetActionValue(action, raw);
        }
    }

    InputState Get(InputAction action) const {
        auto it = m_states.find(action);
        return it != m_states.end() ? it->second : InputState{};
    }
};

// Usage
if (input.Get(InputAction::Jump).pressed) {
    character.BeginJump();
}
float move = input.Get(InputAction::MoveForward).value;
character.velocity.z = move * MOVE_SPEED;

SDL3 raw input

cpp
#include <SDL3/SDL.h>

struct RawInputFrame {
    const uint8_t* keyboard;   // SDL_GetKeyboardState
    SDL_Gamepad* gamepad;
    float mouse_dx, mouse_dy;
};

RawInputFrame PollInput() {
    SDL_Event ev;
    while (SDL_PollEvent(&ev)) {
        // Handle window events, quit, etc.
        if (ev.type == SDL_EVENT_QUIT) RequestQuit();
    }

    RawInputFrame frame;
    frame.keyboard = SDL_GetKeyboardState(nullptr);

    // Mouse delta (relative mode for FPS camera)
    SDL_GetRelativeMouseState(&frame.mouse_dx, &frame.mouse_dy);

    // First connected gamepad
    int count;
    SDL_JoystickID* gamepads = SDL_GetGamepads(&count);
    frame.gamepad = (count > 0) ? SDL_OpenGamepad(gamepads[0]) : nullptr;
    SDL_free(gamepads);

    return frame;
}

Input buffering

Input buffering makes games feel responsive — store the last N frames of input so actions register even if pressed slightly before the "window":

cpp
class InputBuffer {
    static constexpr int BUFFER_SIZE = 6;  // 6 frames @ 60fps = 100ms window

    struct BufferedInput {
        InputAction action;
        int frames_remaining;
    };

    std::vector<BufferedInput> m_buffer;

public:
    void Push(InputAction action) {
        // Remove existing entry for same action
        std::erase_if(m_buffer, [&](auto& b) { return b.action == action; });
        m_buffer.push_back({ action, BUFFER_SIZE });
    }

    bool Consume(InputAction action) {
        auto it = std::find_if(m_buffer.begin(), m_buffer.end(),
            [&](auto& b) { return b.action == action; });
        if (it == m_buffer.end()) return false;
        m_buffer.erase(it);
        return true;
    }

    void Tick() {
        std::erase_if(m_buffer, [](auto& b) { return --b.frames_remaining <= 0; });
    }
};

// In landing logic:
if (character.IsGrounded() && input_buffer.Consume(InputAction::Jump)) {
    character.Jump();  // Registers even if Jump was pressed 4 frames ago
}

Gamepad vibration and trigger effects

cpp
// Rumble (left = low-frequency, right = high-frequency)
SDL_RumbleGamepad(gamepad,
    0xFFFF,   // left motor (0–0xFFFF)
    0x8000,   // right motor
    200       // duration ms
);

// PS5 DualSense adaptive trigger (via SDL3)
SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_ACCEL, SDL_TRUE);

// Note: full DualSense haptics require platform-specific APIs
// or the dualsense-haptics library

Frame-accurate timing

For fighting games and precision platformers, input must be sampled at a consistent sub-frame granularity:

cpp
// Sample input at start of each physics tick, not render frame
void GameLoop() {
    while (!quit) {
        float now = GetHighResTime();
        float dt = now - last_time;
        last_time = now;

        // Poll raw hardware state
        RawInputFrame raw = PollInput();
        input_system.Update(raw);

        // Physics at fixed step
        physics_accumulator += dt;
        while (physics_accumulator >= FIXED_DT) {
            // Input is read at the START of each physics step
            // ensuring no frame is missed
            game.PhysicsUpdate(FIXED_DT, input_system);
            physics_accumulator -= FIXED_DT;
        }

        // Render at uncapped framerate with interpolation
        game.Render(physics_accumulator / FIXED_DT);
    }
}
Edit on GitHubUpdated 2026-05-24T00:00:00.000Z