Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

KeyCode
LettersKeyCode::KeyA through KeyCode::KeyZ
NumbersKeyCode::Digit0 through KeyCode::Digit9
Arrow keysKeyCode::ArrowUp, ArrowDown, ArrowLeft, ArrowRight
SpaceKeyCode::Space
ShiftKeyCode::ShiftLeft, KeyCode::ShiftRight
ControlKeyCode::ControlLeft, KeyCode::ControlRight
AltKeyCode::AltLeft, KeyCode::AltRight
EscapeKeyCode::Escape
EnterKeyCode::Enter
TabKeyCode::Tab
F keysKeyCode::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
}
}