Repository navigation
Input System
The Input System handles keyboard, mouse, and gamepad input with frame-based state tracking, key bindings, and accessibility features. It consists of three main classes: InputManager for direct keyboard/mouse input, GamepadInput for controller support, and InputBindingManager for rebindable controls and presets.
Source: SparkEngine/Source/Input/
┌───────────────────────────────────────────────────────────────────┐
│ Game Code │
│ input.IsKeyDown('W') | input.IsActionDown("Jump") │
│ gamepad.GetLeftStick() | bindings.IsActionActive("Sprint") │
└───────────────┬─────────────────┬───────────────────────────────┘
│ │
▼ ▼
┌───────────────────┐ ┌──────────────────┐
│ InputManager │ │ GamepadInput │
│ (keyboard/mouse) │ │ (XInput gamepads│
│ Win32/SDL2 msgs │ │ up to 4) │
└───────────────────┘ └──────────────────┘
┌───────────────────────────────────────────────────────────────────┐
│ InputBindingManager │
│ Rebindable controls, presets ("WASD", "ESDF", "Controller"), │
│ accessibility flags, JSON persistence │
└───────────────────────────────────────────────────────────────────┘
| File | Responsibility |
|---|---|
InputManager.h |
Keyboard and mouse input, frame-based state tracking, console integration |
GamepadInput.h |
Xbox controller support via XInput, dead zones, vibration, action mapping |
InputBindings.h |
Runtime rebinding, presets, accessibility flags, JSON serialization |
The InputManager class handles all keyboard and mouse input processing via Windows messages.
InputManager input;
input.Initialize(hwnd); // Pass the window handleCall Update() once per frame, after the frame's input messages have been delivered and before anything queries
input states:
// In game loop: pump messages (HandleMessage) first, then:
input.Update(); // Latches key/button states for edge queries, calculates deltasEdge queries (WasKeyPressed, WasKeyReleased, WasMouseButtonPressed, WasMouseButtonReleased) compare the
state latched by this frame's Update() with the state latched by the previous one. Every change delivered since the
previous Update() -- by the message pump, the SDL event loop, or a console SimulateKeyPress -- therefore reads as
an edge for exactly one frame. IsKeyDown / IsMouseButtonDown stay live. Update() does not need a window for
this; only mouse capture does. (Before the fix, Update() copied the live state into "previous" after the hosts
had already delivered the frame's messages, so every edge query made by modules and AngelScript getKeyDown
returned false. Regression: Tests/TestInputFrameEdgesReal.cpp.)
// Currently held down (returns true every frame while held)
if (input.IsKeyDown(VK_SPACE)) { /* Space is held */ }
// Currently not pressed
if (input.IsKeyUp('W')) { /* W is not pressed */ }
// Just pressed this frame (rising edge -- true for one frame only)
if (input.WasKeyPressed('W')) { /* W was pressed this frame */ }
// Just released this frame (falling edge -- true for one frame only)
if (input.WasKeyReleased(VK_SHIFT)) { /* Shift was released this frame */ }Generic modifier queries (VK_SHIFT, VK_CONTROL, VK_MENU) include either physical side as well as
console-injected generic state. Side-specific queries retain their identity. The generic press edge occurs when
the first side goes down; releasing one side while the other remains held does not produce a generic release.
The Win32 host resolves left/right modifiers from scan-code and extended-key metadata and forwards Alt's
system-key messages through the same frame-latched input path. SDL retains its side-specific key translation.
// Mouse button state (0 = Left, 1 = Right, 2 = Middle)
if (input.IsMouseButtonDown(0)) { /* Left mouse button held */ }
if (input.WasMouseButtonPressed(1)) { /* Right mouse just clicked */ }
if (input.WasMouseButtonReleased(2)) { /* Middle mouse just released */ }
// Mouse position (window-relative pixels)
int x, y;
input.GetMousePosition(x, y);
// Mouse movement delta (since last frame, with sensitivity/deadzone applied)
int deltaX, deltaY;
if (input.GetMouseDelta(deltaX, deltaY)) {
// Valid delta values available
camera.Rotate(deltaX * sensitivity, deltaY * sensitivity);
}For first-person camera controls, capture the mouse to hide the cursor and track relative movement:
input.CaptureMouse(true); // Capture: hide cursor, confine to window
input.CaptureMouse(false); // Release: show cursor, free movement
bool captured = input.IsMouseCaptured();Map action names to virtual key codes for configurable controls:
// Bind actions to keys
input.BindKey("MoveForward", 'W');
input.BindKey("MoveBack", 'S');
input.BindKey("MoveLeft", 'A');
input.BindKey("MoveRight", 'D');
input.BindKey("Jump", VK_SPACE);
input.BindKey("Crouch", VK_CONTROL);
input.BindKey("Fire", VK_LBUTTON);
// Check bound actions
if (input.IsActionDown("Jump")) {
player.Jump();
}input.Console_SetMouseSensitivity(1.0f); // Sensitivity multiplier (0.1-10.0)
input.Console_SetMouseDeadZone(0.01f); // Ignore tiny movements (0.0-10.0)
input.Console_SetMouseAcceleration(false); // Disable OS mouse acceleration
input.Console_SetInvertMouseY(false); // Normal Y-axis
input.Console_SetRawMouseInput(true); // Bypass OS acceleration curve
input.Console_SetInputLogging(true); // Log all input events to consoleclass InputManager {
// Key state maps
std::unordered_map<int, bool> m_keyStates; // Current frame
std::unordered_map<int, bool> m_prevKeyStates; // Previous frame
// Mouse buttons [Left, Right, Middle]
bool m_mouseButtons[3];
bool m_prevMouseButtons[3];
// Mouse position and delta
int m_mouseX, m_mouseY;
int m_prevMouseX, m_prevMouseY;
int m_mouseDeltaX, m_mouseDeltaY;
// Settings
float m_mouseSensitivity; // Multiplier applied to delta
float m_mouseDeadZone; // Minimum delta to register
bool m_mouseAcceleration; // Enable/disable acceleration
bool m_invertMouseY; // Invert Y-axis
bool m_rawMouseInput; // Use raw input API
bool m_inputLogging; // Log events to console
// Metrics
size_t m_keyPressCount; // Total key presses this session
size_t m_mousePressCount; // Total mouse clicks this session
float m_totalMouseDistance; // Cumulative mouse movement
// Thread safety
mutable std::mutex m_inputMutex;
};struct InputMetrics {
size_t keyPressCount; // Total key presses this session
size_t mousePressCount; // Total mouse clicks this session
float totalMouseDistance; // Total mouse movement distance
size_t activeKeys; // Currently pressed key count
size_t activeMouseButtons; // Currently pressed button count
bool mouseCaptured; // Mouse capture state
float mouseSensitivity; // Current sensitivity
float mouseDeadZone; // Current dead zone
bool mouseAcceleration; // Acceleration state
bool invertMouseY; // Y-axis inversion state
bool rawMouseInput; // Raw input state
bool inputLogging; // Logging state
size_t totalKeyBindings; // Number of active bindings
};| Method | Description |
|---|---|
Console_SetMouseSensitivity(float) |
Set sensitivity (0.1-10.0) |
Console_SetMouseDeadZone(float) |
Set dead zone (0.0-10.0) |
Console_SetMouseAcceleration(bool) |
Enable/disable acceleration |
Console_SetInvertMouseY(bool) |
Toggle Y-axis inversion |
Console_SetRawMouseInput(bool) |
Toggle raw input mode |
Console_SetInputLogging(bool) |
Toggle input event logging |
Console_BindKey(action, keyName) |
Bind key by name string |
Console_UnbindKey(action) |
Remove a key binding |
Console_ListKeyBindings() |
Get formatted binding list |
Console_SimulateKeyPress(keyName, duration) |
Simulate a key press |
Console_ClearInputStates() |
Reset all input states |
Console_GetRecentEvents(count) |
Get recent input events |
Console_IsActionActive(action) |
Check if action is active |
Console_GetMetrics() |
Get comprehensive metrics |
Console_GetSettings() |
Get current settings |
Console_ApplySettings(settings) |
Apply settings structure |
Console_ResetToDefaults() |
Reset all settings |
Console_RefreshInput() |
Force input system refresh |
GamepadInput provides Xbox controller support for up to 4 simultaneous controllers via XInput.
enum class GamepadButton {
A, B, X, Y, // Face buttons
LeftBumper, RightBumper, // Shoulder bumpers (LB/RB)
Back, Start, // Menu buttons
LeftStick, RightStick, // Thumbstick clicks (L3/R3)
DPadUp, DPadDown, // D-pad
DPadLeft, DPadRight,
Count // Sentinel for array sizing
};
enum class GamepadTrigger { Left, Right };
enum class GamepadStick { Left, Right };
enum class DeadZoneMode {
Circular, // Radial dead zone (recommended -- smooth diagonal movement)
Axial // Per-axis dead zone (each axis independent)
};struct GamepadState {
bool connected; // Controller connected?
XINPUT_STATE xinputState; // Raw XInput state
XINPUT_STATE prevXinputState; // Previous frame (for edge detection)
XMFLOAT2 leftStick, rightStick; // Processed [-1, 1] with dead zone
float leftTrigger, rightTrigger; // Processed [0, 1] with threshold
float leftMotor, rightMotor; // Vibration intensity [0, 1]
float vibrationTimer; // Remaining vibration time (seconds)
};GamepadInput gamepad;
// In game loop:
gamepad.Update(deltaTime);
// Connection
if (gamepad.IsConnected()) {
// Thumbsticks (dead-zone-corrected, [-1, 1])
XMFLOAT2 leftStick = gamepad.GetLeftStick();
float moveX = leftStick.x;
float moveY = leftStick.y;
XMFLOAT2 rightStick = gamepad.GetRightStick();
camera.Rotate(rightStick.x, rightStick.y);
// Individual axis access
float lx = gamepad.GetLeftStickX();
float ly = gamepad.GetLeftStickY();
float rx = gamepad.GetRightStickX();
float ry = gamepad.GetRightStickY();
// Triggers ([0, 1])
float leftTrigger = gamepad.GetLeftTrigger();
float rightTrigger = gamepad.GetRightTrigger();
if (gamepad.IsTriggerDown(GamepadTrigger::Right, 0.3f)) { /* Shooting */ }
// Buttons
if (gamepad.IsButtonDown(GamepadButton::A)) { /* A held */ }
if (gamepad.WasButtonPressed(GamepadButton::X)) { /* X just pressed */ }
if (gamepad.WasButtonReleased(GamepadButton::B)) { /* B just released */ }
}// Set vibration (left = low frequency rumble, right = high frequency buzz)
gamepad.SetVibration(0.5f, 0.3f, 0.5f); // 50% left, 30% right, 0.5s duration
gamepad.SetVibration(1.0f, 1.0f); // Full intensity, indefinite
// Stop
gamepad.StopVibration(); // Stop controller 0
gamepad.StopAllVibrations(); // Stop all controllersgamepad.SetDeadZone(GamepadStick::Left, 0.24f); // Default XInput left stick
gamepad.SetDeadZone(GamepadStick::Right, 0.26f); // Default XInput right stick
gamepad.SetDeadZoneMode(DeadZoneMode::Circular); // Radial (recommended)
gamepad.SetTriggerThreshold(0.12f); // Minimum trigger activation
gamepad.SetStickSensitivity(1.5f); // 1.5x sensitivity multiplier// Bind named actions to gamepad inputs
gamepad.BindAction("Jump", GamepadButton::A);
gamepad.BindAction("Crouch", GamepadButton::B);
gamepad.BindAction("Shoot", GamepadTrigger::Right, 0.3f); // Trigger with threshold
gamepad.BindAction("AimDownSights", GamepadTrigger::Left, 0.2f);
// Query by action name
if (gamepad.IsActionDown("Jump")) { player.Jump(); }
if (gamepad.WasActionPressed("Shoot")) { weapon.Fire(); }All methods accept an optional controllerIndex parameter (0-3):
// Player 2's controller
if (gamepad.IsConnected(1)) {
auto stick = gamepad.GetLeftStick(1);
if (gamepad.WasButtonPressed(GamepadButton::Start, 1)) { /* P2 joined */ }
gamepad.SetVibration(0.5f, 0.5f, 0.3f, 1); // Rumble controller 1
}
int connectedCount = gamepad.GetConnectedCount(); // 0-4Disconnected controllers are polled at a reduced rate (every 2 seconds) to avoid XInput performance overhead:
static constexpr float CONNECTION_POLL_INTERVAL = 2.0f;The InputBindingManager provides rebindable controls, presets, and accessibility features.
struct InputBinding {
int primaryKey = 0; // Primary keyboard key code
int alternateKey = 0; // Alternate key (0 = none)
int gamepadButton = -1; // Gamepad button index (-1 = none)
float gamepadAxis = 0.0f; // Gamepad axis for analog actions
bool holdToToggle = false; // Tap toggles on/off instead of hold
};InputBindingManager bindings;
// Set individual bindings
bindings.SetBinding("MoveForward", InputBinding{'W'});
bindings.SetBinding("Jump", InputBinding{VK_SPACE});
// Check for conflicts
std::string conflict = bindings.FindConflict("Jump");
if (!conflict.empty()) { /* "Jump" conflicts with another action */ }
// Get all bindings
const auto& allBindings = bindings.GetAllBindings();// Create built-in presets
bindings.CreateDefaultPresets(); // Creates "Default", "Left-Handed", "Accessibility"
// Apply a preset (replaces all current bindings)
bindings.ApplyPreset("WASD");
// List available presets
for (const auto& name : bindings.GetPresetNames()) {
LOG("Preset: " + name);
}
// Register a custom preset
InputPreset customPreset;
customPreset.name = "Custom";
customPreset.description = "My custom layout";
customPreset.bindings["Jump"] = InputBinding{VK_SPACE};
// ...
bindings.RegisterPreset(customPreset);enum class AccessibilityFlags : uint32_t {
None = 0,
HoldToToggle = 1 << 0, // Convert hold actions to toggle
LargeSubtitles = 1 << 1, // Increase subtitle font size
HighContrastUI = 1 << 2, // High contrast UI mode
ColorblindProtanopia = 1 << 3, // Red-green (protan) filter
ColorblindDeuteranopia = 1 << 4, // Red-green (deutan) filter
ColorblindTritanopia = 1 << 5, // Blue-yellow filter
ReducedMotion = 1 << 6, // Reduce screen shake/motion
ScreenNarrator = 1 << 7, // Screen reader support
AutoAim = 1 << 8, // Aim assist
OneTouchMode = 1 << 9 // Simplified single-button input
};
// Enable accessibility features
bindings.SetAccessibility(
AccessibilityFlags::HoldToToggle |
AccessibilityFlags::AutoAim |
AccessibilityFlags::LargeSubtitles
);
// Check individual flags
if (bindings.IsAccessibilityEnabled(AccessibilityFlags::ColorblindProtanopia)) {
ApplyColorblindFilter();
}// Save bindings + accessibility settings to JSON
bindings.SaveToFile("Config/keybinds.json");
// Load from JSON
bindings.LoadFromFile("Config/keybinds.json");bindings.OnBindingChanged([](const std::string& action, const InputBinding& binding) {
UpdateControlsUI(action, binding);
});| Input | Action |
|---|---|
| W / A / S / D | Move forward / left / back / right |
| Mouse movement | Look around (yaw/pitch) |
| Space | Jump |
| Ctrl | Crouch |
| Left Click | Fire / Capture mouse |
| Right Click | Aim down sights |
| Esc | Release mouse / Pause menu |
| ` (Backtick) | Toggle debug console |
| Tab | Scoreboard |
| R | Reload |
| E | Interact |
| Component | Thread Safety | Details |
|---|---|---|
InputManager |
Partial | Read access protected by m_inputMutex. Update() and HandleMessage() must be called from main thread only. |
GamepadInput |
Single-threaded | Call all methods from main thread. XInput is not thread-safe. |
InputBindingManager |
Single-threaded | Designed for main-thread access only. |
-
InputManager::Initialize()requires a validHWND. Passingnullptrwill result in mouse capture failures. -
GamepadInputreturns safe defaults (0, false, {0,0}) for disconnected or out-of-range controller indices. -
InputBindingManager::LoadFromFile()returnsfalseif the JSON file is missing or malformed; existing bindings are preserved. - Key name lookups return
KeyCode::Unknown(or equivalent) for unrecognized names.
-
State tracking: Key states use
std::unordered_mapfor sparse key coverage (only pressed keys are stored). - Gamepad polling: Connected controllers are polled every frame; disconnected controllers are polled every 2 seconds to minimize XInput overhead.
-
Dead zone processing: Circular dead zone uses a single
sqrt()per stick per frame. Axial mode uses nosqrt(). -
Event callbacks: Dispatched synchronously during
Update(). Keep callbacks lightweight to avoid frame time impact. -
Platform backend: Win32 backend processes queued messages; SDL2 backend calls
SDL_PollEvent().
input_info # Input system status (mouse state, key count, gamepad connection)
input_sensitivity <v> # Set mouse sensitivity multiplier (0.1-10.0)
input_deadzone <v> # Set mouse dead zone threshold (0.0-10.0)
input_invert_y # Toggle Y-axis inversion for mouse look
input_raw <on|off> # Toggle raw mouse input (bypasses OS acceleration)
input_bindings # List all current key bindings
input_log <on|off> # Toggle input event logging to console
input_stats # Show input statistics (press counts, mouse distance)
- Reduce sensitivity:
input_sensitivity 0.5 - Enable raw mouse input:
input_raw on - Increase dead zone:
input_deadzone 0.05 - Disable mouse acceleration:
Console_SetMouseAcceleration(false)
- Verify controller is connected via Windows Settings
- Check
gamepad.IsConnected()-- disconnected controllers are polled every 2 seconds - XInput only supports Xbox-compatible controllers; other gamepads may need SDL2 backend
- Check
input_infofor system status - Verify
Initialize()was called with a valid window handle - Ensure
Update()is called every frame before input queries - Check that
HandleMessage()is being called fromWndProc
- Verify file path exists and is writable
- Check JSON format in the bindings file
-
LoadFromFile()returnsfalseon failure; check return value
- Ensure
Update()is called at the start of the frame, before any input queries - Reduce input processing time by minimizing event callback work
- Enable raw mouse input to bypass OS processing pipeline
- Creating a Game Module -- Accessing InputManager via IEngineContext
- SparkConsole -- Input debug commands
- Gameplay Systems -- Player controller and FPS controls
- Scripting with AngelScript -- Input API available in scripts
- Event System -- Input-related event publishing
- SparkEditor -- Input binding configuration UI
Published from 614bca17b41e. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Platform Certification
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- CI Controlled-Failure Rehearsal
- Release Publication Stages
- Session Handoff 2026-09-27
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Online Service Boundary
- Server Operations Runbook
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers