Domain Deep-Dive
IntermediateInput 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 libraryFrame-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