A powerful 3D/2.5D game controller plugin for Bevy Engine.
The Input System documentation provides comprehensive coverage of a flexible, production-ready input handling framework built for the Bevy game engine. This system delivers robust action mapping, input buffering, runtime rebinding capabilities, and seamless integration between global input state and entity-specific components.
The Bevy Input System implements a robust action-based input architecture that decouples physical input devices from logical game actions. This separation enables:
Hardware Agnosticism: Game logic references semantic actions (Jump, Interact) rather than physical inputs (Space, E), allowing seamless support for keyboard, mouse, gamepad, or future input devices without code changes.
Runtime Flexibility: Players can remap controls during gameplay without restarting the application or reloading assets, enhancing accessibility and user customization.
Temporal Buffering: Critical actions like jumps and interactions benefit from input buffering, capturing player intent even when inputs occur between physics frames or during animation transitions.
State Isolation: Global input state resources synchronize automatically with player entity components, maintaining clean separation between engine-level input processing and gameplay logic.
Extensibility: The system architecture supports straightforward extension to additional input devices (gamepads, touchscreens) through the binding abstraction layer.
The input system delivers production-ready features essential for modern game development:
Comprehensive Action Taxonomy: 35+ predefined logical actions spanning movement, combat, inventory management, camera control, and stealth mechanics.
Multi-Binding Support: Each logical action supports multiple physical bindings simultaneously (e.g., LockOn bound to both Tab key and mouse wheel button).
Buffered Input Processing: Time-sensitive actions receive special handling through a configurable buffer with automatic expiration management.
Runtime Rebinding: Players can dynamically reassign controls through an intuitive capture process without interrupting gameplay flow.
Automatic State Synchronization: Global input state resources automatically propagate to player entities, eliminating manual state copying boilerplate.
Input Enable/Disable Toggle: Comprehensive input disabling with automatic state reset prevents unintended inputs during cutscenes, menus, or dialogue sequences.
Axis Normalization: Movement vectors automatically normalize to prevent faster diagonal movement, with configurable sensitivity parameters.
Reflect Integration: Full Bevy Reflect trait implementation enables runtime inspection, serialization, and editor tooling support.
The input system operates within well-defined boundaries to maintain separation of concerns:
Input Collection Layer: Raw device input captured through Bevy’s ButtonInput<KeyCode> and ButtonInput<MouseButton> resources.
Binding Translation Layer: Physical inputs mapped to logical actions through the InputMap resource configuration.
State Management Layer: Translated actions populate the InputState resource with normalized values and temporal flags (just_pressed vs pressed).
Buffering Layer: Time-sensitive actions receive special handling through the InputBuffer resource with configurable expiration.
Entity Synchronization Layer: Global input state automatically propagates to player entities bearing the InputState component.
Gameplay Integration Boundary: The system intentionally stops at state population; actual gameplay effects (character movement, weapon firing) occur in separate systems that consume InputState.
This layered architecture ensures the input system remains focused on input processing without encroaching on gameplay logic responsibilities.
The input system employs a dual-state model combining global resources with entity-specific components:
InputMap: Central configuration resource defining action-to-binding mappings. Serves as the single source of truth for control schemes.
InputState (Resource): Global snapshot of current input state maintained by input processing systems. Updated every frame during the Update schedule.
InputBuffer: Temporal storage for recently pressed actions with automatic expiration based on configurable time-to-live.
InputConfig: System-wide configuration parameters including mouse sensitivity, axis inversion, and buffer expiration duration.
RebindState: Transient state resource tracking ongoing rebinding operations (which action is awaiting new binding assignment).
InputState (Component): Per-entity copy of input state, automatically synchronized from the global resource. Allows multiple controllable entities (e.g., player character, vehicles) to maintain independent input states.This dual-state model provides critical advantages:
Menu/UI Isolation: Global input state continues updating during pause menus, but player entity components can be selectively disabled without affecting UI navigation inputs.
Multi-Entity Control: Multiple player-controlled entities (e.g., in split-screen multiplayer) maintain independent input states through separate components.
AI/Player Differentiation: AI-controlled entities can have InputState components populated by AI decision systems rather than player input, enabling unified consumption patterns across control schemes.
Cutscene Safety: Disabling input on player entities during cutscenes prevents accidental player interference while maintaining global state for potential cutscene interaction points.
Input processing follows a strict unidirectional data flow through discrete processing stages:
Physical Input Devices
↓
[Bevy ButtonInput Resources]
↓
[update_input_state System]
├──→ Binding Translation (InputMap lookup)
├──→ Action Buffering (critical actions)
├──→ Axis Normalization (movement vectors)
└──→ State Population (InputState resource)
↓
[cleanup_input_buffer System]
↓
[player_input_sync_system]
↓
[Gameplay Systems Consuming InputState]
Each stage performs a single responsibility with explicit boundaries:
Binding Translation Stage: Raw device inputs (KeyCode::Space) mapped to logical actions (InputAction::Jump) through InputMap configuration. Multiple bindings per action evaluated with OR semantics (any binding triggers the action).
Buffering Stage: Time-critical actions (Jump, Interact, LockOn) captured on just_pressed events and stored in InputBuffer with timestamps. Enables “input forgiveness” for actions occurring between animation frames.
InputState fields with appropriate temporal semantics:
MoveForward) set boolean flags for duration of pressJump) set just_pressed flags only on initial press framemovement, look) normalized to consistent magnitudeBuffer Cleanup Stage: Expired buffered actions automatically removed based on configurable TTL (buffer_ttl in InputConfig).
InputState resource cloned to all entities with InputState components and Player marker component (excluding AI-controlled entities).This staged architecture ensures deterministic processing order and prevents race conditions between input collection and gameplay consumption.
The input system integrates with Bevy’s scheduling system through carefully ordered system execution:
.add_systems(Update, (
update_input_state,
handle_rebinding,
cleanup_input_buffer,
player_input_sync_system,
).chain())
.add_systems(Update, (
process_movement_input,
process_action_input,
));
Critical sequencing considerations:
update_input_state: Collects raw inputs and populates global statehandle_rebinding: Processes rebinding requests using current frame inputscleanup_input_buffer: Removes expired buffered actionsplayer_input_sync_system: Propagates global state to player entitiesParallel Execution: Movement and action processing systems run after the chain completes, ensuring they consume fully processed input state.
Frame Boundary Safety: All input processing completes before gameplay systems execute, preventing mid-frame state inconsistencies.
This scheduling guarantees that gameplay systems always observe a consistent, fully processed input state snapshot for the current frame.
Movement actions form the foundation of player locomotion with orthogonal directional controls:
MoveForward: Primary forward movement along character’s forward axis. Typically bound to W key. Combined with Sprint for accelerated movement.
MoveBackward: Reverse movement along character’s backward axis. Typically bound to S key. Often reduced speed compared to forward movement in gameplay implementation.
MoveLeft: Lateral movement left relative to character orientation. Typically bound to A key. Enables strafing mechanics essential for combat maneuvering.
MoveRight: Lateral movement right relative to character orientation. Typically bound to D key. Symmetric counterpart to MoveLeft.
Jump: Vertical propulsion action initiating airborne state. Buffer-sensitive action benefiting from input forgiveness during landing animations. Typically bound to Space key.
Sprint: Modifier action increasing movement speed while held. Continuous press detection enables variable-speed locomotion. Typically bound to Left Shift.
Crouch: Posture modifier reducing character height and potentially movement speed. Enables stealth mechanics and low-clearance navigation. Typically bound to Left Control.
LeanLeft / LeanRight: Partial cover mechanics allowing player to peek around obstacles while maintaining cover position. Implemented as continuous press actions rather than toggles for safety.
Combat actions support diverse engagement mechanics with weapon handling and defensive capabilities:
Attack: Primary offensive action triggering melee attacks or weapon firing depending on equipped item. Typically bound to left mouse button.
Block: Defensive action reducing incoming damage or deflecting attacks. Continuous press enables partial blocking mechanics. Typically bound to right mouse button (shared with Aim in some configurations).
Fire: Dedicated ranged weapon firing action. Separate from Attack to support hybrid combat systems with distinct melee/ranged inputs. Typically bound to left mouse button.
Aim: Precision aiming modifier activating iron sights or scoped view. Continuous press enables variable zoom levels in advanced implementations. Typically bound to right mouse button.
Reload: Weapon reloading sequence initiation. Momentary press triggers full reload animation sequence. Typically bound to R key.
NextWeapon / PrevWeapon: Cyclic weapon selection through inventory. Momentary presses advance or retreat through weapon slots. Typically bound to arrow keys.
SelectWeapon[0-9]: Direct weapon slot selection enabling instant weapon switching without cycling. Critical for tactical combat requiring rapid loadout changes. Bound to numeric keys 0-9 with 0 typically mapping to slot 10.
Interaction actions bridge player intent with environmental engagement:
Interact: Context-sensitive interaction with objects, NPCs, or environmental elements. Buffer-sensitive to prevent missed interactions during movement. Typically bound to E key.
ToggleInventory: User interface activation for inventory management, equipment configuration, and item usage. Momentary press toggles UI visibility state. Typically bound to I key.
SwitchCameraMode: Perspective or camera behavior modification (first-person ↔ third-person, free-look modes). Momentary press cycles through available camera configurations. Typically bound to C key.
ResetCamera: Camera reorientation to default position/angle. Essential for disorientation recovery after complex maneuvers. Typically bound to X key (changed from R to avoid reload conflict).
Specialized actions supporting advanced locomotion and stealth gameplay:
Hide: Stealth state activation reducing visibility to enemies and enabling silent movement. Momentary press toggles stealth state. Typically bound to H key.
Peek: Limited exposure from cover positions enabling target acquisition without full exposure. Continuous press maintains peek state. Typically bound to P key.
CornerLean: Advanced cover mechanic allowing partial exposure around corners while maintaining cover integrity. Distinct from basic lean actions with more complex animation requirements. Typically bound to C key (potential conflict with camera mode requires resolution in binding configuration).
LockOn: Target acquisition system locking camera and aiming reticle to valid targets within acquisition radius. Supports multiple bindings (keyboard Tab and mouse wheel button) for accessibility. Buffer-sensitive to prevent missed lock attempts during movement.
ZoomIn / ZoomOut: Camera field-of-view adjustment for tactical observation or situational awareness. Typically bound to numpad +/- keys.
SideSwitch: Cover side transition allowing player to switch cover positions without exposing full body. Critical for advanced cover combat systems. Typically bound to V key.
Camera manipulation actions (primarily handled through axis inputs rather than discrete actions):
look axis: Two-dimensional vector representing mouse movement delta for camera rotation. Processed separately from keyboard inputs with configurable sensitivity and axis inversion.
ResetCamera: Explicit camera reset action complementing continuous look controls.
Note: Continuous camera rotation typically handled through raw mouse delta processing rather than discrete actions to enable smooth, analog control. The input system provides the look vector field in InputState for this purpose.
The system supports two primary physical input types through the InputBinding enumeration:
InputBinding::Key(KeyCode))KeyCode enumeration.pressed() for continuous actions and just_pressed() for momentary actions.InputBinding::Mouse(MouseButton))MouseButton enumeration.ZoomIn/ZoomOut) rather than continuous axis due to event-based nature of scroll input.Each logical action supports multiple physical bindings evaluated with OR semantics:
Binding Evaluation Order: Bindings evaluated sequentially; first pressed binding triggers the action. Evaluation order typically irrelevant due to OR semantics but may impact rebinding UI display order.
Simultaneous Binding Presses: Pressing multiple bindings for the same action simultaneously treated identically to single binding press—no special handling or priority assignment.
The InputMap::default() implementation provides comprehensive out-of-the-box configuration:
ResetCamera on X instead of R to avoid reload conflict).LockOn on both Tab and mouse wheel) accommodating different player preferences.Default bindings represent a balanced starting point optimized for PC keyboard/mouse configurations. Console or alternative input schemes require custom InputMap initialization.
The InputState structure maintains comprehensive input snapshot with field categories:
movement: Vec2: Normalized 2D vector representing directional input magnitude and direction. Values range from (0.0, 0.0) (no input) to normalized vectors like (0.707, 0.707) for diagonal movement. Automatic normalization prevents faster diagonal movement artifacts.
look: Vec2: Raw mouse delta vector representing camera rotation input. Not normalized—magnitude represents physical mouse movement distance. Processed separately with configurable sensitivity multipliers.
Boolean flags indicating sustained press duration:
crouch_pressed, sprint_pressed, aim_pressed, lean_left, lean_right, block_pressed, fire_pressedThese fields remain true for entire duration of physical input press, enabling analog-like behavior for actions with variable duration effects (e.g., partial crouch depth based on press duration).
Boolean flags indicating frame-specific press events:
jump_pressed, interact_pressed, lock_on_pressed, attack_pressed, switch_camera_mode_pressed, fire_just_pressed, reload_pressed, reset_camera_pressed, next_weapon_pressed, prev_weapon_pressed, toggle_inventory_pressed, side_switch_pressed, hide_pressed, peek_pressed, corner_lean_pressed, zoom_in_pressed, zoom_out_pressedThese fields set true only on the exact frame when physical input transitions from released to pressed. Automatically reset to false on subsequent frames regardless of continued press duration. Critical for actions triggering discrete state transitions (jump initiation, weapon firing).
select_weapon: Option<usize>: Weapon slot selection result from numeric key presses. Some(n) indicates selection of slot n (0-indexed) on current frame; None otherwise. Resets to None each frame after consumption by weapon systems.
enabled: bool: Master input enable/disable flag. When false, all input fields automatically zeroed/reset and physical input ignored. Essential for cutscenes, menus, and dialogue sequences.
Critical distinction between continuous and momentary state fields:
| Field Type | Physical Input Duration | Field Value Duration | Typical Use Cases |
|---|---|---|---|
Continuous (*_pressed) |
Held for N frames | Remains true for all N frames |
Crouching, sprinting, aiming |
Momentary (*_just_pressed) |
Pressed on frame T | true only on frame T |
Jumping, firing, interacting |
Vector (movement, look) |
Continuous analog input | Updated every frame with current values | Locomotion, camera rotation |
This temporal distinction enables nuanced input handling:
Jump Buffering: jump_pressed field set from buffered action rather than direct input, allowing jump initiation up to 0.15 seconds after landing animation completes.
Double-Tap Detection: Gameplay systems can implement double-tap mechanics by tracking momentary press timestamps across frames.
Hold-to-Activate: Continuous press fields enable charge mechanics (e.g., hold fire to charge weapon) by measuring duration between initial press and release.
Input Chording: Simultaneous press detection through combined evaluation of multiple continuous press fields (e.g., crouch_pressed && sprint_pressed for sliding).
The set_input_enabled() method provides comprehensive input suppression:
pub fn set_input_enabled(&mut self, enabled: bool)
When disabling input (enabled = false):
enabled flag to falseVec2::ZEROfalseNone)Critical use cases:
Menu Systems: Disable player input when opening inventory/pause menus while maintaining UI navigation inputs through separate input contexts.
Cutscenes: Prevent player interference during narrative sequences while potentially allowing skip inputs through alternative input channels.
Dialogue Sequences: Suppress locomotion/combat inputs during NPC conversations while permitting dialogue choice selection.
Loading Transitions: Disable inputs during level transitions to prevent state corruption from inputs processed during asset loading.
Game State Transitions: Temporarily disable inputs during death sequences, respawn animations, or objective completion sequences.
Re-enabling input (enabled = true) does not restore previous state—player must provide fresh inputs. This prevents unintended actions from inputs buffered during disabled period.
Input buffering addresses critical temporal disconnects between player intent and game state readiness:
Animation Lockout Periods: During jump landing animations, attack recovery, or weapon reload sequences, raw input presses might occur when gameplay systems cannot process them. Buffering captures these inputs for processing when state becomes receptive.
Frame Rate Variance: At lower framerates, brief inputs might occur entirely between frames. Buffering with appropriate TTL captures these sub-frame inputs.
Network Latency Compensation: In networked games, buffering provides window for reconciling client inputs with server-validated state transitions.
Player Intent Preservation: Players pressing jump immediately upon landing expect jump initiation even if landing animation hasn’t fully completed. Buffering honors this expectation.
The BufferedAction structure captures temporal input data:
pub struct BufferedAction {
pub action: InputAction,
pub timestamp: f32,
}
action: Logical action that was pressed (Jump, Interact, etc.)timestamp: Game time in seconds when action was pressed (time.elapsed_seconds())Timestamp enables precise expiration management independent of frame rate. Buffer cleanup evaluates (current_time - timestamp) <= buffer_ttl rather than frame-counting.
Buffer behavior controlled through InputConfig resource:
pub struct InputConfig {
pub mouse_sensitivity: f32,
pub gamepad_sensitivity: f32,
pub invert_y_axis: bool,
pub buffer_ttl: f32, // Critical parameter
}
buffer_ttl (Time-To-Live): Maximum duration buffered actions remain valid. Default 0.15 seconds (150ms) represents balance between responsiveness and forgiveness:
Buffer TTL applies uniformly to all buffered actions. Advanced implementations might support action-specific TTLs through extended configuration.
Two consumption patterns supported through InputBuffer methods:
pub fn consume(&mut self, action: InputAction) -> bool
truefalseif input_buffer.consume(InputAction::Jump) { initiate_jump(); }Ensures each buffered action triggers at most one state transition, preventing duplicate processing.
pub fn is_buffered(&self, action: InputAction) -> bool
consume() call to actually trigger actionSupports advanced UX patterns like visual feedback for buffered inputs before state transition occurs.
System buffers three critical actions by default:
Jump: Most common buffering candidate due to frequent animation lockout during landing/recovery.Interact: Prevents missed interactions when approaching interactable objects during movement animations.LockOn: Ensures target acquisition succeeds even when pressing lock-on during weapon sway or movement animations.Gameplay systems may extend buffering to additional actions through custom buffer management outside core input system.
Automatic buffer management through dedicated cleanup system:
fn cleanup_input_buffer(
time: Res<Time>,
config: Res<InputConfig>,
mut input_buffer: ResMut<InputBuffer>,
)
(now - timestamp) <= config.buffer_ttlVec::retain() for performanceThis automatic cleanup prevents buffer accumulation and memory growth during extended play sessions.
The rebinding system enables dynamic control remapping through intuitive three-step process:
Initiation: Gameplay system sets RebindState.action = Some(target_action) when player enters rebinding UI for specific action.
Capture: System monitors all physical inputs (keyboard keys, mouse buttons) on subsequent frames. First detected input becomes new binding.
Assignment: Detected input automatically assigned as sole binding for target action in InputMap, replacing previous bindings. RebindState.action reset to None.
This workflow requires zero gameplay system involvement beyond initiation—capture and assignment handled entirely by handle_rebinding system.
pub struct RebindState {
pub action: Option<InputAction>,
}
None: Normal gameplay mode—inputs processed for action triggeringSome(action): Rebinding mode active for specified action—next physical input captured as new bindingState resource enables rebinding initiation from any system with mutable access to RebindState. Typical initiation pattern:
// In UI interaction system when player selects "Jump" for rebinding
rebind_state.action = Some(InputAction::Jump);
// Next physical input automatically becomes new jump binding
During rebinding mode (RebindState.action.is_some()):
get_just_pressed() firstThis priority ordering reflects typical rebinding expectations—keyboard rebinding more common than mouse button rebinding.
New bindings completely replace existing bindings for target action:
This simplification ensures rebinding predictability—players always know exactly which physical input triggers an action after rebinding.
Critical safety mechanisms prevent problematic rebinding scenarios:
InputMap at startupInputMap to disk (leveraging Reflect trait)fn update_input_state(
time: Res<Time>,
keyboard: Res<ButtonInput<KeyCode>>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
input_map: Res<InputMap>,
mut input_state: ResMut<InputState>,
mut input_buffer: ResMut<InputBuffer>,
)
input_state.enabled == false, skipping all processingClosure-based binding evaluation provides efficient action checking:
let check_action = |action: InputAction| -> bool {
if let Some(bindings) = input_map.bindings.get(&action) {
bindings.iter().any(|binding| match binding {
InputBinding::Key(code) => keyboard.pressed(code.clone()),
InputBinding::Mouse(button) => mouse_buttons.pressed(button.clone()),
})
} else {
false
}
};
false)Separate closure for just_pressed evaluation follows identical pattern with just_pressed() instead of pressed().
Buffered actions captured on exact frame of press:
for action in actions_to_buffer {
if check_action_just_pressed(action) {
input_buffer.actions.push(BufferedAction {
action,
timestamp: time.elapsed_seconds(),
});
}
}
just_pressed events buffered—not sustained pressesOrthogonal axis combination with normalization:
let mut movement = Vec2::ZERO;
if check_action(InputAction::MoveForward) { movement.y += 1.0; }
if check_action(InputAction::MoveBackward) { movement.y -= 1.0; }
if check_action(InputAction::MoveLeft) { movement.x -= 1.0; }
if check_action(InputAction::MoveRight) { movement.x += 1.0; }
input_state.movement = movement.normalize_or_zero();
(±1.0, ±1.0) before normalizationnormalize_or_zero() prevents division-by-zero on zero vectorMutually exclusive numeric selection with priority ordering:
input_state.select_weapon = None;
if check_action_just_pressed(InputAction::SelectWeapon1) { input_state.select_weapon = Some(0); }
else if check_action_just_pressed(InputAction::SelectWeapon2) { input_state.select_weapon = Some(1); }
// ... continues through SelectWeapon0
else if chain ensures only one weapon selected per frame even with simultaneous key pressesSelectWeapon0 maps to slot index 9 (tenth weapon slot) following common gaming conventionNone each frame—gameplay systems must consume selection immediately or lose itfn cleanup_input_buffer(
time: Res<Time>,
config: Res<InputConfig>,
mut input_buffer: ResMut<InputBuffer>,
)
Vec::retain() for allocation-free filteringinput_buffer.actions.retain(|ba| now - ba.timestamp <= config.buffer_ttl);
buffer_ttl settingfn player_input_sync_system(
input_state: Res<InputState>,
mut query: Query<&mut InputState, (With<Player>, Without<AiController>)>,
)
Critical query constraints:
With<Player>: Only entities marked as player-controlled receive synchronizationWithout<AiController>: Explicitly excludes AI-controlled entities that might coincidentally have Player markerSynchronization mechanism:
InputState resource to each matching entityInputState’s compact representation (~80 bytes)fn process_movement_input(_input: Res<InputState>) {}
fn process_action_input(_input: Res<InputState>) {}
These placeholder systems represent integration points for gameplay logic:
process_movement_input: Intended for character controller systems consuming movement vector and continuous press states (sprint_pressed, crouch_pressed)process_action_input: Intended for discrete action systems consuming momentary press states (jump_pressed, fire_just_pressed) and buffered actionsCurrent stub implementation enables:
Production implementations would replace stubs with systems containing actual character movement, weapon firing, and interaction logic.
pub struct InputConfig {
pub mouse_sensitivity: f32,
pub gamepad_sensitivity: f32,
pub invert_y_axis: bool,
pub buffer_ttl: f32,
}
mouse_sensitivity: Multiplier applied to raw mouse delta values before populating look vector. Default 0.15 represents moderate sensitivity:
gamepad_sensitivity: Analogous multiplier for gamepad thumbstick inputs. Default 1.0 represents direct mapping:
invert_y_axis: Boolean flag for vertical look inversion (common preference among flight simulator players):
false: Push mouse forward = look down (standard)true: Push mouse forward = look up (inverted)buffer_ttl: Time-to-live for buffered actions in seconds. Default 0.15 (150ms):
Default implementation provides balanced starting point:
impl Default for InputConfig {
fn default() -> Self {
Self {
mouse_sensitivity: 0.15,
gamepad_sensitivity: 1.0,
invert_y_axis: false,
buffer_ttl: 0.15,
}
}
}
Game-specific tuning recommended before production release:
InputConfig is a standard Bevy resource enabling runtime modification:
// In settings UI system when player adjusts slider
config.mouse_sensitivity = new_value;
Changes take effect immediately on next frame:
Persistence requires explicit serialization/deserialization as with InputMap.
Recommended integration pattern for character controllers:
// In character movement system
fn character_movement(
input: Res<InputState>,
mut query: Query<(&mut Transform, &mut CharacterController), With<Player>>,
) {
if !input.enabled {
return; // Respect global input disable state
}
for (mut transform, mut controller) in query.iter_mut() {
// Apply movement vector with sprint modifier
let speed = if input.sprint_pressed {
controller.sprint_speed
} else {
controller.walk_speed
};
let movement = input.movement * speed * time.delta_seconds();
// ... apply movement to transform
}
}
Critical integration considerations:
input.enabled before processing to respect global disable statemovement vector directly—already normalized and directionally correctsprint_pressed) enable analog-like speed variationLeveraging buffered jump input in character controller:
fn jump_handling(
mut input_buffer: ResMut<InputBuffer>,
mut query: Query<&mut CharacterController>,
) {
for mut controller in query.iter_mut() {
// Check both direct input and buffered input
let wants_to_jump = input.jump_pressed || input_buffer.consume(InputAction::Jump);
if wants_to_jump && controller.can_jump() {
controller.initiate_jump();
}
}
}
Buffer consumption pattern ensures:
Weapon selection and firing integration pattern:
fn weapon_system(
input: Res<InputState>,
mut weapons: ResMut<WeaponInventory>,
) {
// Handle weapon selection
if let Some(slot) = input.select_weapon {
weapons.equip_slot(slot);
}
// Handle firing with buffering consideration
if input.fire_just_pressed || input_buffer.consume(InputAction::Fire) {
weapons.active_weapon.fire();
}
// Handle reload
if input.reload_pressed {
weapons.active_weapon.reload();
}
}
Selection handling considerations:
select_weapon immediately—it resets to None next frame regardlessInput disable pattern for pause menus:
fn pause_menu_system(
mut input_state: ResMut<InputState>,
keyboard: Res<ButtonInput<KeyCode>>,
mut menu_state: ResMut<MenuState>,
) {
// Toggle pause on ESC press
if keyboard.just_pressed(KeyCode::Escape) {
menu_state.paused = !menu_state.paused;
// Disable player input when paused
input_state.set_input_enabled(!menu_state.paused);
}
// UI navigation uses separate input context not affected by disable
if menu_state.paused {
// Process UI navigation inputs here
// These would use direct ButtonInput checks rather than InputState
}
}
Critical separation:
InputState disable affects only gameplay systemsButtonInput resources for navigationCamera control integration leveraging look vector:
fn camera_control(
input: Res<InputState>,
config: Res<InputConfig>,
mut query: Query<&mut CameraController>,
) {
for mut controller in query.iter_mut() {
// Apply sensitivity multiplier to raw look input
let look_delta = input.look * config.mouse_sensitivity;
// Apply Y-axis inversion if configured
let look_delta = Vec2::new(look_delta.x,
if config.invert_y_axis { -look_delta.y } else { look_delta.y }
);
// Apply rotation to camera
controller.yaw += look_delta.x;
controller.pitch += look_delta.y;
// Clamp pitch to prevent gimbal lock
controller.pitch = controller.pitch.clamp(-1.5, 1.5);
}
}
Look vector processing responsibilities:
look vector preserved in InputState for alternative camera implementationsCrouch vs ControlPress)MouseRight vs Aim)SelectWeapon1 through SelectWeapon0)Action1, ButtonA) for configurable action slotsAim and Fire over combined AimAndFireJump action sufficient—no need for ShortJump/LongJump primitivesLeanRight and Interact both default to E—requires resolutionCornerLean and SwitchCameraMode both default to C—requires resolutionLockOn bound to both keyboard and mouse buttonCtrl+C for copy acceptable; Ctrl+Shift+Alt+Q for jump unacceptableSystematic approach to buffer configuration:
buffer_ttl to 75% of shortest critical animation:
Granular input disabling strategies:
input_state.set_input_enabled(false);
Use cases:
Core system doesn’t support partial disable—implement through wrapper:
// Custom resource tracking disabled action groups
#[derive(Resource)]
struct DisabledInputGroups {
movement: bool,
combat: bool,
camera: bool,
}
// System filtering actions based on disabled groups
fn filtered_input_update(
disabled: Res<DisabledInputGroups>,
raw_input: Res<InputState>,
mut filtered: ResMut<FilteredInputState>,
) {
filtered.movement = if disabled.movement { Vec2::ZERO } else { raw_input.movement };
filtered.jump_pressed = if disabled.movement { false } else { raw_input.jump_pressed };
// ... etc
}
Use cases:
Comprehensive input testing strategy:
#[test]
fn test_no_duplicate_bindings() {
let map = InputMap::default();
let mut binding_usage = HashMap::new();
for (action, bindings) in &map.bindings {
for binding in bindings {
let count = binding_usage.entry(binding.clone()).or_insert(0);
*count += 1;
}
}
// Allow up to 2 uses per binding (intentional conflicts)
for (binding, count) in &binding_usage {
assert!(count <= &2, "Binding {:?} used {} times", binding, count);
}
}
#[test]
fn test_jump_buffer_timing() {
let mut buffer = InputBuffer::default();
let config = InputConfig { buffer_ttl: 0.15, ..default() };
// Add jump at t=0.0
buffer.actions.push(BufferedAction {
action: InputAction::Jump,
timestamp: 0.0
});
// Should be present at t=0.14
buffer.actions.retain(|ba| 0.14 - ba.timestamp <= config.buffer_ttl);
assert_eq!(buffer.actions.len(), 1);
// Should be expired at t=0.16
buffer.actions.retain(|ba| 0.16 - ba.timestamp <= config.buffer_ttl);
assert_eq!(buffer.actions.len(), 0);
}
Manual testing scenarios:
Symptoms: Player presses keys but character doesn’t move/act.
Diagnosis Steps:
input_state.enabled == true (not disabled by menu/cutscene)InputMap contains bindings for expected actionsSolutions:
InputPlugin added to App before gameplay pluginsfn debug_input(input: Res<InputState>) {
println!("Movement: {:?}, Jump: {}", input.movement, input.jump_pressed);
}
set_input_enabled(false) calls without corresponding enableSymptoms: Single key press triggers action twice (double jump, double fire).
Diagnosis Steps:
consume() not is_buffered()Solutions:
consume() for buffered actions to ensure single consumptionRebindState.action after assignmentSymptoms: Character moves faster when pressing two movement keys simultaneously.
Diagnosis Steps:
movement.normalize_or_zero() called in input processingSolutions:
// Correct
let velocity = input.movement.normalize_or_zero() * speed;
// Incorrect - double normalization
let velocity = input.movement.normalize_or_zero().normalize() * speed;
Symptoms: Rebinding UI active but inputs not captured as new bindings.
Diagnosis Steps:
RebindState.action properly set to Some(action)Solutions:
if rebind_state.action.is_some() {
println!("Rebinding active for {:?}", rebind_state.action);
}
ButtonInput::reset() callsSymptoms: Memory usage increases steadily during extended play sessions.
Diagnosis Steps:
InputBuffer.actions vector size over timecleanup_input_buffer system executes every frameSolutions:
cleanup_input_buffer system registered in Update schedule// In buffer population
if input_buffer.actions.len() > 10 {
input_buffer.actions.remove(0); // Drop oldest
}
Debug overlay displaying current input state:
INPUT STATE (enabled: true)
Movement: (0.00, 1.00) [Forward]
Look: (2.34, -1.87)
Jump: ■ (buffered)
Sprint: □
Crouch: □
Fire: ■ (just pressed)
Weapon: None
Implementation approach:
System identifying problematic binding configurations:
fn detect_binding_conflicts(map: Res<InputMap>) {
let mut binding_to_actions: HashMap<InputBinding, Vec<InputAction>> = HashMap::new();
for (action, bindings) in &map.bindings {
for binding in bindings {
binding_to_actions.entry(binding.clone())
.or_insert_with(Vec::new)
.push(*action);
}
}
for (binding, actions) in &binding_to_actions {
if actions.len() > 1 {
warn!("Binding conflict: {:?} mapped to {:?}", binding, actions);
}
}
}
Run during development builds to catch configuration issues early.
Vec::retain() provides allocation-free filteringTotal typical footprint: <3KB—negligible for modern systems.
Current implementation performs hashmap lookup per action per frame:
if let Some(bindings) = input_map.bindings.get(&action) { ... }
Optimization potential:
Current Vec<BufferedAction> sufficient for typical use:
Current implementation reconstructs movement vector every frame:
Conclusion: System already highly optimized for typical workloads. Micro-optimizations unlikely to yield measurable gains in real-world scenarios.
System scales linearly with player count:
InputState componentInputMap, InputBuffer) remain single-instanceAdding actions scales linearly:
Adding gamepad support requires:
InputBinding variant (Gamepad(GamepadButton))ButtonInput<GamepadButton> resource accessCurrent architecture supports straightforward gamepad extension:
InputBinding enumeration:
pub enum InputBinding {
Key(KeyCode),
Mouse(MouseButton),
Gamepad(GamepadButton),
}
update_input_state:
gamepad_buttons: Res<ButtonInput<GamepadButton>>,
InputBinding::Gamepad(button) => gamepad_buttons.pressed(button.clone()),
// In movement construction
let gamepad_axis = Vec2::new(
gamepad_axes.axis(GamepadAxis::LEFT_STICK_X),
gamepad_axes.axis(GamepadAxis::LEFT_STICK_Y),
);
movement += gamepad_axis;
pub struct GamepadConfig {
pub left_stick_deadzone: f32,
pub right_stick_deadzone: f32,
}
pub struct HapticRequest {
pub gamepad: Gamepad,
pub intensity: f32,
pub duration: f32,
}
Current uniform TTL may not suit all actions:
Extension approach:
pub struct BufferConfig {
pub default_ttl: f32,
pub overrides: HashMap<InputAction, f32>,
}
// In buffer population
let ttl = config.overrides.get(&action).copied().unwrap_or(config.default_ttl);
Buffer combinations of simultaneous inputs:
Implementation strategy:
Replay system for:
Core requirements:
Extension architecture:
#[derive(Resource)]
pub struct InputRecorder {
pub recording: bool,
pub playback: bool,
pub events: Vec<InputEvent>,
}
pub struct InputEvent {
pub timestamp: f32,
pub action: InputAction,
pub pressed: bool,
}
Challenges:
Predefined control schemes for accessibility needs:
Implementation:
pub enum ControlProfile {
Standard,
OneHandedLeft,
OneHandedRight,
MouseOnly,
Simplified,
}
impl ControlProfile {
pub fn apply_to_map(&self, map: &mut InputMap) {
match self {
ControlProfile::OneHandedLeft => {
// Remap all actions to left-hand accessible keys
map.bindings.insert(InputAction::Jump, vec![InputBinding::Key(KeyCode::Space)]);
// ... etc
}
// ... other profiles
}
}
}
Tight integration required for responsive movement:
movement vector and continuous press statesInputBuffercan_jump() state for buffered jump validationCritical interface:
// CharacterController component provides state query methods
impl CharacterController {
pub fn can_jump(&self) -> bool {
self.is_grounded && !self.jump_cooldown.active()
}
}
Action consumption patterns:
select_weapon immediately (single-frame validity)State synchronization:
Look vector processing responsibilities:
InputConfigSeparation of concerns:
look vectorCritical separation patterns:
InputState resourceButtonInput resourcesMenu state machine:
GameState::Playing
→ ESC pressed
→ GameState::Paused (disable gameplay input, enable UI navigation)
→ Resume selected
→ GameState::Playing (enable gameplay input, disable UI navigation)
Input-driven animation selection:
Animation-state feedback:
The input system demonstrates production-ready characteristics:
Recommended for production use with minor configuration tuning for target game genre.
LeanRight/Interact conflict (both default to E)CornerLean/SwitchCameraMode conflict (both default to C)buffer_ttl to 75% of shortest critical animationThis input system provides a robust foundation for diverse game genres with minimal modification. Its action-based architecture, temporal buffering capabilities, and runtime rebinding support address the majority of input handling requirements for modern games. The clean separation between input processing and gameplay logic ensures long-term maintainability as game complexity grows.
Recommended adoption path:
The system’s thoughtful architecture provides solid footing for both immediate production needs and future evolution as player expectations and input technologies advance.