name: bevy-input-and-interaction description: Use when the user asks about handling keyboard input, mouse input, gamepad/controller input, touch input, picking/raycasting, UI interaction, or input mapping in Bevy. Also triggers for questions about cursor position, mouse clicks on entities, or input abstraction. version: 1.0.0

Bevy Input & Interaction — Keyboard, Mouse, Gamepad & Picking

For system registration, queries, resources, and event fundamentals, see the bevy-ecs skill first. This skill builds on those concepts to cover all input handling and entity interaction in Bevy 0.15+.

Keyboard Input

Bevy exposes keyboard state through the ButtonInput<KeyCode> resource. Query it in any system:

#![allow(unused)]
fn main() {
fn keyboard_system(keys: Res<ButtonInput<KeyCode>>) {
    // Held down this frame
    if keys.pressed(KeyCode::KeyW) {
        // move forward
    }

    // Just pressed this frame (single-fire)
    if keys.just_pressed(KeyCode::Space) {
        // jump
    }

    // Just released this frame
    if keys.just_released(KeyCode::ShiftLeft) {
        // stop sprinting
    }
}
}

Common KeyCode Values

CategoryKeys
LettersKeyCode::KeyA .. KeyCode::KeyZ
DigitsKeyCode::Digit0 .. KeyCode::Digit9
ArrowsKeyCode::ArrowUp, ArrowDown, ArrowLeft, ArrowRight
ModifiersKeyCode::ShiftLeft, ShiftRight, ControlLeft, AltLeft, SuperLeft
CommonKeyCode::Space, Enter, Escape, Tab, Backspace
FunctionKeyCode::F1 .. KeyCode::F12

Text Input

For actual character input (respecting keyboard layout, IME, etc.), use KeyboardInput events rather than ButtonInput:

#![allow(unused)]
fn main() {
fn text_input_system(mut events: EventReader<KeyboardInput>) {
    for event in events.read() {
        if event.state.is_pressed() {
            if let Key::Character(ref char) = event.logical_key {
                info!("Character typed: {char}");
            }
        }
    }
}
}

Mouse Input

Buttons

Mouse buttons work identically to keyboard keys:

#![allow(unused)]
fn main() {
fn mouse_button_system(buttons: Res<ButtonInput<MouseButton>>) {
    if buttons.just_pressed(MouseButton::Left) {
        // primary click
    }
    if buttons.pressed(MouseButton::Right) {
        // holding secondary
    }
    if buttons.just_pressed(MouseButton::Middle) {
        // middle click
    }
}
}

Cursor Position

Read the cursor position from the Window component:

#![allow(unused)]
fn main() {
fn cursor_position_system(windows: Query<&Window>) {
    let window = windows.single();
    if let Some(position) = window.cursor_position() {
        // position is in window/logical pixels, origin at top-left
        info!("Cursor at: {position}");
    }
}
}

Screen-to-World Conversion

To get the world-space position of the cursor (essential for clicking on game objects):

#![allow(unused)]
fn main() {
fn cursor_world_position(
    windows: Query<&Window>,
    camera_q: Query<(&Camera, &GlobalTransform)>,
) {
    let window = windows.single();
    let (camera, camera_transform) = camera_q.single();

    if let Some(cursor_pos) = window.cursor_position() {
        if let Ok(world_pos) = camera.viewport_to_world_2d(camera_transform, cursor_pos) {
            info!("World cursor: {world_pos}");
        }
    }
}
}

For 3D, use viewport_to_world which returns a Ray3d:

#![allow(unused)]
fn main() {
fn cursor_ray_3d(
    windows: Query<&Window>,
    camera_q: Query<(&Camera, &GlobalTransform)>,
) {
    let window = windows.single();
    let (camera, camera_transform) = camera_q.single();

    if let Some(cursor_pos) = window.cursor_position() {
        if let Ok(ray) = camera.viewport_to_world(camera_transform, cursor_pos) {
            // ray.origin and ray.direction for raycasting
            info!("Ray origin: {}, direction: {}", ray.origin, *ray.direction);
        }
    }
}
}

Mouse Motion and Scroll

Use events for relative mouse movement and scroll wheel:

#![allow(unused)]
fn main() {
fn mouse_motion_system(mut motion: EventReader<MouseMotion>) {
    for event in motion.read() {
        // event.delta is Vec2 — relative movement in pixels
        info!("Mouse moved: {:?}", event.delta);
    }
}

fn mouse_scroll_system(mut scroll: EventReader<MouseWheel>) {
    for event in scroll.read() {
        // event.x, event.y — scroll amounts
        // event.unit — Lines or Pixels
        info!("Scroll: x={} y={}", event.x, event.y);
    }
}
}

Gamepad Input

In Bevy 0.15+, gamepads are entities with a Gamepad component. Buttons and axes are accessed through the Gamepad component directly.

Detecting Connected Gamepads

#![allow(unused)]
fn main() {
fn gamepad_connection_system(
    gamepads: Query<(Entity, &Gamepad), Added<Gamepad>>,
) {
    for (entity, _gamepad) in &gamepads {
        info!("Gamepad connected: {entity}");
    }
}
}

Reading Gamepad Input

#![allow(unused)]
fn main() {
fn gamepad_input_system(gamepads: Query<&Gamepad>) {
    for gamepad in &gamepads {
        // Buttons
        if gamepad.just_pressed(GamepadButton::South) {
            info!("A / Cross pressed");
        }
        if gamepad.pressed(GamepadButton::RightTrigger2) {
            info!("Right trigger held");
        }

        // Axes — returns f32 in [-1.0, 1.0]
        let left_stick_x = gamepad.get(GamepadAxis::LeftStickX).unwrap_or(0.0);
        let left_stick_y = gamepad.get(GamepadAxis::LeftStickY).unwrap_or(0.0);

        // Apply dead zone manually
        let dead_zone = 0.15;
        if left_stick_x.abs() > dead_zone || left_stick_y.abs() > dead_zone {
            info!("Left stick: ({left_stick_x}, {left_stick_y})");
        }
    }
}
}

Common Gamepad Buttons

GamepadButtonXboxPlayStation
SouthACross
EastBCircle
WestXSquare
NorthYTriangle
LeftTriggerLBL1
RightTriggerRBR1
LeftTrigger2LTL2
RightTrigger2RTR2
LeftThumbL3L3
RightThumbR3R3
DPadUp/Down/Left/RightD-PadD-Pad
StartMenuOptions
SelectViewShare

Common Gamepad Axes

GamepadAxisDescription
LeftStickXLeft stick horizontal
LeftStickYLeft stick vertical
RightStickXRight stick horizontal
RightStickYRight stick vertical

Touch Input

The Touches resource tracks all active touch points:

#![allow(unused)]
fn main() {
fn touch_system(touches: Res<Touches>) {
    // New touches this frame
    for touch in touches.iter_just_pressed() {
        info!(
            "Touch started: id={}, position={}",
            touch.id(),
            touch.position()  // Vec2 in window coordinates
        );
    }

    // Currently held touches (includes position and start_position)
    for touch in touches.iter() {
        let delta = touch.position() - touch.start_position();
        info!("Finger {} moved by {delta}", touch.id());
    }

    // Touches released this frame
    for touch in touches.iter_just_released() {
        info!("Touch ended: id={}", touch.id());
    }

    // Touches cancelled (e.g., interrupted by OS)
    for touch in touches.iter_just_cancelled() {
        info!("Touch cancelled: id={}", touch.id());
    }
}
}

Multi-touch finger tracking uses the touch.id() to correlate touches across frames. Each finger gets a stable ID for its entire press-move-release lifecycle.

Picking (0.15+)

Bevy 0.15 ships a built-in picking system for detecting pointer interactions with entities. No third-party crate needed.

Making Entities Pickable

Entities with meshes are pickable by default when bevy_picking is enabled. To explicitly control picking, add or remove the Pickable component:

#![allow(unused)]
fn main() {
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    // Pickable by default (has a mesh)
    commands.spawn((
        Mesh3d(asset_server.load("models/button.glb")),
        MeshMaterial3d(/* ... */),
    ));

    // Explicitly disable picking on an entity
    commands.spawn((
        Mesh3d(asset_server.load("models/background.glb")),
        MeshMaterial3d(/* ... */),
        Pickable::IGNORE,
    ));
}
}

Pointer Events with Observers

The picking system fires events that you handle with observers. This is the recommended pattern — events are targeted to specific entities:

#![allow(unused)]
fn main() {
use bevy::picking::pointer::PointerInteraction;

fn setup(mut commands: Commands) {
    // Spawn a clickable entity with an observer
    commands.spawn((
        Mesh3d(/* ... */),
        MeshMaterial3d(/* ... */),
    ))
    .observe(on_click)
    .observe(on_pointer_over)
    .observe(on_pointer_out);
}

fn on_click(trigger: Trigger<Pointer<Click>>, mut commands: Commands) {
    let entity = trigger.target();
    let event = trigger.event();
    info!("Clicked entity {entity:?} at {}", event.pointer_location.position);
}

fn on_pointer_over(
    trigger: Trigger<Pointer<Over>>,
    mut materials: Query<&mut MeshMaterial3d<StandardMaterial>>,
) {
    // Highlight on hover
    let entity = trigger.target();
    if let Ok(mut material) = materials.get_mut(entity) {
        // swap to highlight material
    }
}

fn on_pointer_out(
    trigger: Trigger<Pointer<Out>>,
    mut materials: Query<&mut MeshMaterial3d<StandardMaterial>>,
) {
    // Remove highlight
    let entity = trigger.target();
    if let Ok(mut material) = materials.get_mut(entity) {
        // restore original material
    }
}
}

Available Pointer Events

EventFires when
Pointer<Over>Pointer enters the entity's bounds
Pointer<Out>Pointer leaves the entity's bounds
Pointer<Down>Pointer button pressed while over entity
Pointer<Up>Pointer button released while over entity
Pointer<Click>Full press-and-release cycle on the entity
Pointer<Move>Pointer moves while over entity
Pointer<DragStart>Drag begins on entity
Pointer<Drag>Entity is being dragged
Pointer<DragEnd>Drag ends
Pointer<DragEnter>Dragged entity enters another entity's bounds
Pointer<DragOver>Dragged entity hovers over another entity
Pointer<DragDrop>Dragged entity dropped onto another entity
Pointer<DragLeave>Dragged entity leaves another entity's bounds

Picking works with both 2D sprites and 3D meshes, and also with Bevy UI nodes.

leafwing-input-manager (Third-Party Input Abstraction)

For games that need unified input mapping across keyboard, gamepad, and mouse, leafwing-input-manager is the recommended community crate. It lets you define logical actions and bind them to physical inputs.

See the bevy-ecosystem skill for setup details and version compatibility.

#![allow(unused)]
fn main() {
use leafwing_input_manager::prelude::*;

// 1. Define actions
#[derive(Actionlike, PartialEq, Eq, Hash, Clone, Copy, Debug, Reflect)]
pub enum PlayerAction {
    Move,   // Axis-pair action
    Jump,
    Attack,
}

// 2. Build an input map and spawn it on the player
fn spawn_player(mut commands: Commands) {
    let input_map = InputMap::default()
        .with_dual_axis(PlayerAction::Move, KeyboardVirtualDPad::WASD)
        .with_dual_axis(PlayerAction::Move, GamepadStick::LEFT)
        .with(PlayerAction::Jump, KeyCode::Space)
        .with(PlayerAction::Jump, GamepadButton::South)
        .with(PlayerAction::Attack, MouseButton::Left)
        .with(PlayerAction::Attack, GamepadButton::West);

    commands.spawn((
        // ... player components
        InputManagerBundle::with_map(input_map),
    ));
}

// 3. Query action state in gameplay systems
fn player_movement(query: Query<&ActionState<PlayerAction>, With<Player>>) {
    let action_state = query.single();

    if action_state.pressed(&PlayerAction::Move) {
        let axis_pair = action_state.clamped_axis_pair(&PlayerAction::Move);
        let movement = Vec2::new(axis_pair.x, axis_pair.y);
        // apply movement * speed * time.delta_secs()
    }

    if action_state.just_pressed(&PlayerAction::Jump) {
        // jump
    }
}

// 4. Register the plugin
// app.add_plugins(InputManagerPlugin::<PlayerAction>::default())
}

Common Patterns

Player Movement (WASD + Arrow Keys)

#![allow(unused)]
fn main() {
fn player_movement(
    keys: Res<ButtonInput<KeyCode>>,
    mut query: Query<&mut Transform, With<Player>>,
    time: Res<Time>,
) {
    let mut direction = Vec2::ZERO;

    if keys.pressed(KeyCode::KeyW) || keys.pressed(KeyCode::ArrowUp) {
        direction.y += 1.0;
    }
    if keys.pressed(KeyCode::KeyS) || keys.pressed(KeyCode::ArrowDown) {
        direction.y -= 1.0;
    }
    if keys.pressed(KeyCode::KeyA) || keys.pressed(KeyCode::ArrowLeft) {
        direction.x -= 1.0;
    }
    if keys.pressed(KeyCode::KeyD) || keys.pressed(KeyCode::ArrowRight) {
        direction.x += 1.0;
    }

    // Normalize to prevent diagonal speed boost
    let direction = direction.normalize_or_zero();
    let speed = 200.0;

    for mut transform in &mut query {
        transform.translation.x += direction.x * speed * time.delta_secs();
        transform.translation.y += direction.y * speed * time.delta_secs();
    }
}
}

FPS Camera Control (Mouse Look)

#![allow(unused)]
fn main() {
#[derive(Component)]
struct FpsCamera {
    sensitivity: f32,
    pitch: f32,
    yaw: f32,
}

fn fps_camera_look(
    mut motion: EventReader<MouseMotion>,
    mut camera: Query<(&mut Transform, &mut FpsCamera)>,
) {
    let (mut transform, mut fps) = camera.single_mut();

    for event in motion.read() {
        fps.yaw -= event.delta.x * fps.sensitivity;
        fps.pitch -= event.delta.y * fps.sensitivity;
        fps.pitch = fps.pitch.clamp(-89.0_f32.to_radians(), 89.0_f32.to_radians());
    }

    transform.rotation =
        Quat::from_rotation_y(fps.yaw) * Quat::from_rotation_x(fps.pitch);
}
}

Lock the cursor for FPS controls:

#![allow(unused)]
fn main() {
fn grab_cursor(mut windows: Query<&mut Window>) {
    let mut window = windows.single_mut();
    window.cursor_options.grab_mode = CursorGrabMode::Locked;
    window.cursor_options.visible = false;
}
}

Orbit Camera

#![allow(unused)]
fn main() {
#[derive(Component)]
struct OrbitCamera {
    focus: Vec3,
    radius: f32,
    pitch: f32,
    yaw: f32,
}

fn orbit_camera_system(
    mut scroll: EventReader<MouseWheel>,
    mut motion: EventReader<MouseMotion>,
    buttons: Res<ButtonInput<MouseButton>>,
    mut camera: Query<(&mut Transform, &mut OrbitCamera)>,
) {
    let (mut transform, mut orbit) = camera.single_mut();

    // Zoom with scroll wheel
    for event in scroll.read() {
        orbit.radius -= event.y * 0.5;
        orbit.radius = orbit.radius.clamp(2.0, 50.0);
    }

    // Rotate with middle mouse button
    if buttons.pressed(MouseButton::Middle) {
        for event in motion.read() {
            orbit.yaw -= event.delta.x * 0.005;
            orbit.pitch -= event.delta.y * 0.005;
            orbit.pitch = orbit.pitch.clamp(-1.5, 1.5);
        }
    }

    // Update camera transform
    let rotation = Quat::from_rotation_y(orbit.yaw) * Quat::from_rotation_x(orbit.pitch);
    transform.translation = orbit.focus + rotation * Vec3::new(0.0, 0.0, orbit.radius);
    transform.look_at(orbit.focus, Vec3::Y);
}
}

Drag and Drop with Picking

#![allow(unused)]
fn main() {
#[derive(Component)]
struct Draggable;

#[derive(Component)]
struct Dragging {
    offset: Vec2,
}

fn setup_draggable(mut commands: Commands) {
    commands.spawn((
        Sprite {
            custom_size: Some(Vec2::new(64.0, 64.0)),
            ..default()
        },
        Draggable,
    ))
    .observe(on_drag_start)
    .observe(on_drag)
    .observe(on_drag_end);
}

fn on_drag_start(
    trigger: Trigger<Pointer<DragStart>>,
    mut commands: Commands,
    transforms: Query<&Transform>,
) {
    let entity = trigger.target();
    let pointer_pos = trigger.event().pointer_location.position;
    if let Ok(transform) = transforms.get(entity) {
        let offset = Vec2::new(transform.translation.x, transform.translation.y) - pointer_pos;
        commands.entity(entity).insert(Dragging { offset });
    }
}

fn on_drag(
    trigger: Trigger<Pointer<Drag>>,
    mut transforms: Query<(&mut Transform, &Dragging)>,
) {
    let entity = trigger.target();
    let pointer_pos = trigger.event().pointer_location.position;
    if let Ok((mut transform, dragging)) = transforms.get_mut(entity) {
        let new_pos = pointer_pos + dragging.offset;
        transform.translation.x = new_pos.x;
        transform.translation.y = new_pos.y;
    }
}

fn on_drag_end(trigger: Trigger<Pointer<DragEnd>>, mut commands: Commands) {
    commands.entity(trigger.target()).remove::<Dragging>();
}
}