Keyboard & Mouse
Keyboard and mouse state live on world.resources.input.keyboard and world.resources.input.mouse. Both can be polled directly inside run_systems or consumed as events through the AppEvent queue.
Keyboard
Polling
is_key_pressed returns whether a key is currently held. It is the right call for continuous actions like movement or sprint.
#![allow(unused)] fn main() { fn run_systems(&mut self, world: &mut World) { let keyboard = &world.resources.input.keyboard; if keyboard.is_key_pressed(KeyCode::KeyW) { move_forward(); } if keyboard.is_key_pressed(KeyCode::Space) { jump(); } if keyboard.is_key_pressed(KeyCode::ShiftLeft) { sprint(); } } }
Common Key Codes
| Key | Code |
|---|---|
| Letters | KeyCode::KeyA through KeyCode::KeyZ |
| Numbers | KeyCode::Digit0 through KeyCode::Digit9 |
| Arrow keys | KeyCode::ArrowUp, ArrowDown, ArrowLeft, ArrowRight |
| Space | KeyCode::Space |
| Shift | KeyCode::ShiftLeft, KeyCode::ShiftRight |
| Control | KeyCode::ControlLeft, KeyCode::ControlRight |
| Alt | KeyCode::AltLeft, KeyCode::AltRight |
| Escape | KeyCode::Escape |
| Enter | KeyCode::Enter |
| Tab | KeyCode::Tab |
| F keys | KeyCode::F1 through KeyCode::F12 |
Event Handling
For discrete actions where the press is more important than the held state, match on the keyboard AppEvent. The example below pauses on Escape, toggles fullscreen on F11, and selects a weapon slot on the digit keys.
#![allow(unused)] fn main() { fn on_keyboard_input(&mut self, world: &mut World, key: KeyCode, state: ElementState) { if state == ElementState::Pressed { match key { KeyCode::Escape => self.paused = !self.paused, KeyCode::F11 => toggle_fullscreen(world), KeyCode::Digit1 => self.select_weapon(0), KeyCode::Digit2 => self.select_weapon(1), _ => {} } } } }
Mouse
Position
position is the cursor location in screen coordinates. Origin is the top-left of the window.
#![allow(unused)] fn main() { let mouse = &world.resources.input.mouse; let position = mouse.position; }
Movement Delta
position_delta is the per-frame change in cursor position, the right value for mouse-look or anything that wants relative motion rather than absolute coordinates.
#![allow(unused)] fn main() { let delta = world.resources.input.mouse.position_delta; camera_yaw += delta.x * sensitivity; camera_pitch += delta.y * sensitivity; }
Buttons
Mouse buttons expose three states each. CLICKED is held, JUST_PRESSED fires on the transition into pressed, JUST_RELEASED fires on the transition out. The JUST_* variants only see one frame of true.
#![allow(unused)] fn main() { let mouse = &world.resources.input.mouse; if mouse.state.contains(MouseState::LEFT_CLICKED) { fire_weapon(); } if mouse.state.contains(MouseState::LEFT_JUST_PRESSED) { start_drag(); } if mouse.state.contains(MouseState::LEFT_JUST_RELEASED) { end_drag(); } if mouse.state.contains(MouseState::RIGHT_CLICKED) { aim_down_sights(); } if mouse.state.contains(MouseState::MIDDLE_CLICKED) { pan_camera(); } }
Scroll Wheel
wheel_delta is the scroll delta since the previous frame. y is vertical scroll, x is horizontal scroll on mice that support it.
#![allow(unused)] fn main() { let scroll = world.resources.input.mouse.wheel_delta; if scroll.y != 0.0 { zoom_camera(scroll.y); } }
Event Handling
Match on the mouse AppEvent when the press and release transitions both matter, such as aim-down-sights that holds while the button is down.
#![allow(unused)] fn main() { fn on_mouse_input(&mut self, world: &mut World, state: ElementState, button: MouseButton) { match (button, state) { (MouseButton::Left, ElementState::Pressed) => self.shoot(), (MouseButton::Right, ElementState::Pressed) => self.aim(), (MouseButton::Right, ElementState::Released) => self.stop_aim(), _ => {} } } }
WASD Movement
The standard WASD pattern. Each direction key sets a component of a movement vector, then the vector is normalized so diagonals are not 1.41x faster than cardinals.
#![allow(unused)] fn main() { fn get_movement_input(world: &World) -> Vec3 { let keyboard = &world.resources.input.keyboard; let mut direction = Vec3::zeros(); if keyboard.is_key_pressed(KeyCode::KeyW) { direction.z -= 1.0; } if keyboard.is_key_pressed(KeyCode::KeyS) { direction.z += 1.0; } if keyboard.is_key_pressed(KeyCode::KeyA) { direction.x -= 1.0; } if keyboard.is_key_pressed(KeyCode::KeyD) { direction.x += 1.0; } if direction.magnitude() > 0.0 { direction.normalize_mut(); } direction } }
Mouse Look
First-person camera control. Mouse delta drives yaw on the world Y axis and pitch on the local X axis. Composing them as yaw * rotation * pitch keeps yaw global and pitch relative to the camera's current orientation, which is the convention that produces the expected behavior. A real implementation also clamps pitch so the camera does not flip over.
#![allow(unused)] fn main() { fn mouse_look_system(world: &mut World, sensitivity: f32) { let delta = world.resources.input.mouse.position_delta; if let Some(camera) = world.resources.active_camera { if let Some(transform) = world.core.get_local_transform_mut(camera) { let yaw = nalgebra_glm::quat_angle_axis( -delta.x * sensitivity, &Vec3::y(), ); let pitch = nalgebra_glm::quat_angle_axis( -delta.y * sensitivity, &Vec3::x(), ); transform.rotation = yaw * transform.rotation * pitch; } } } }
Cursor Visibility
For first-person games, lock the cursor to the window and hide it so the system pointer does not interfere with mouse-look.
#![allow(unused)] fn main() { fn initialize(&mut self, world: &mut World) { world.set_cursor_locked(true); world.set_cursor_visible(false); } }
Rebindable Controls
A KeyBindings struct decouples action names from physical keys. The struct holds one KeyCode per action and the input system reads from those fields rather than from hard-coded constants. The user can then write to the struct to remap.
#![allow(unused)] fn main() { struct KeyBindings { move_forward: KeyCode, move_back: KeyCode, move_left: KeyCode, move_right: KeyCode, jump: KeyCode, sprint: KeyCode, } impl Default for KeyBindings { fn default() -> Self { Self { move_forward: KeyCode::KeyW, move_back: KeyCode::KeyS, move_left: KeyCode::KeyA, move_right: KeyCode::KeyD, jump: KeyCode::Space, sprint: KeyCode::ShiftLeft, } } } }
Input Buffering
An input buffer accepts an input slightly before the action becomes available and replays it when the action becomes valid. The most common use is jump. A player who presses jump a few frames before landing still gets the jump, which feels more responsive than rejecting the press outright. The buffer is a countdown timer that decays each frame and is refreshed on press.
#![allow(unused)] fn main() { struct InputBuffer { jump_buffer: f32, } fn update_input_buffer(buffer: &mut InputBuffer, world: &World, dt: f32) { buffer.jump_buffer = (buffer.jump_buffer - dt).max(0.0); if world.resources.input.keyboard.is_key_pressed(KeyCode::Space) { buffer.jump_buffer = 0.15; } } fn try_jump(buffer: &mut InputBuffer, grounded: bool) -> bool { if grounded && buffer.jump_buffer > 0.0 { buffer.jump_buffer = 0.0; return true; } false } }