Input Handling

Click here to download the example code.

This is a complete example that you can run. It will print all input activity to the console.


Bevy supports the following inputs:

  • Keyboard
  • Mouse (relative motion, buttons, scrolling)
  • Cursor (absolute pointer position)
  • Touchscreen (with multi-touch)
  • Most controllers/gamepads/joysticks (via the gilrs library)

Sensors like accelerometers, gyroscopes, VR head tracking, are not supported yet.

For most input types (where it makes sense), Bevy provides two ways of dealing with them:

Some inputs are only provided as events.

Checking state is done using resources such as Input (for binary inputs like keys or buttons), Axis (for analog inputs), Touches (for fingers on a touchscreen), etc. This way of handling input is very convenient for implementing game logic. In these scenarios, you typically only care about the specific inputs mapped to actions in your game. You can check specific buttons/keys to see when they get pressed/released, or what their current state is.

Events are a lower-level, more all-encompassing approach. Use them if you want to get all activity from that class of input device, rather than only checking for specific inputs. For example, if you are implementing text input / typing, you probably want to receive all keyboard activity.

Since bevy resources and events are global and accessible from any system, you don't need to centralize your input handling code in one place. You can check any inputs you care about, from any system.

Input Mapping

Bevy does not yet offer a built-in way to do input mapping (configure key bindings, etc). You need to come up with your own way of translating the inputs into logical actions in your game/app.

There are some community-made plugins that may help with that: see the awesome-bevy list.

It may be a good idea to build your own abstractions specific to your game. For example, if you need to handle player movement, you might want to have a system for reading inputs and converting them to your own internal "movement intent/action events", and then another system acting on those internal events, to actually move the player. Make sure to use explicit system ordering to avoid lag / frame delays.

How to work with different input devices

Keyboard

Keyboard keys can be identified by Scan Code or Key Code.

Scan Codes represent the physical key on the keyboard, regardless of the system layout. Key Codes represent the symbol/letter on each key and are dependent on the keyboard layout.

Unfortunately, support for using Scan Codes in Bevy is limited. This can be annoying for people with non-QWERTY keyboard layouts.

Checking the state of specific keys can currently only be done by Key Code, using the Input<KeyCode> resource:

fn keyboard_input(
    keys: Res<Input<KeyCode>>,
) {
    if keys.just_pressed(KeyCode::Space) {
        // Space was pressed
    }
    if keys.just_released(KeyCode::LControl) {
        // Left Ctrl was released
    }
    if keys.pressed(KeyCode::W) {
        // W is being held down
    }
}

To get all keyboard activity, you can use KeyboardInput events:

fn keyboard_events(
    mut key_evr: EventReader<KeyboardInput>,
) {
    use bevy::input::ElementState;

    for ev in key_evr.iter() {
        match ev.state {
            ElementState::Pressed => {
                println!("Key press: {:?} ({})", ev.key_code, ev.scan_code);
            }
            ElementState::Released => {
                println!("Key release: {:?} ({})", ev.key_code, ev.scan_code);
            }
        }
    }
}

These events give you both the Key Code and Scan Code, but unfortunately, the Scan Code is just represented as an arbitrary u32 integer ID (which means it is difficult to figure out what actual physical key it represents).

Mouse Buttons

Similar to keyboard input, mouse buttons are available as an Input state resource, as well as events.

You can check the state of specific mouse buttons using Input<MouseButton>:

fn mouse_button_input(
    buttons: Res<Input<MouseButton>>,
) {
    if buttons.just_pressed(MouseButton::Left) {
        // Left button was pressed
    }
    if buttons.just_released(MouseButton::Left) {
        // Left Button was released
    }
    if buttons.pressed(MouseButton::Right) {
        // Right Button is being held down
    }
}

To get all press/release activity, use MouseButtonInput events:

fn mouse_button_events(
    mut mousebtn_evr: EventReader<MouseButtonInput>,
) {
    use bevy::input::ElementState;

    for ev in mousebtn_evr.iter() {
        match ev.state {
            ElementState::Pressed => {
                println!("Mouse button press: {:?}", ev.button);
            }
            ElementState::Released => {
                println!("Mouse button release: {:?}", ev.button);
            }
        }
    }
}

Mouse Motion

If you need to detect relative mouse motion, you can use MouseMotion events. Whenever the mouse is moved, you will get an event with the delta.

fn mouse_motion(
    mut motion_evr: EventReader<MouseMotion>,
) {
    for ev in motion_evr.iter() {
        println!("Mouse moved: X: {} px, Y: {} px", ev.delta.x, ev.delta.y);
    }
}

You might want to grab/lock the mouse inside the game window.

Mouse Cursor

You can get the current coordinates of the mouse pointer, from the respective Window:

fn cursor_position(
    windows: Res<Windows>,
) {
    // Games typically only have one window (the primary window).
    // For multi-window applications, you need to use a specific window ID here.
    let window = windows.get_primary().unwrap();

    if let Some(_position) = window.cursor_position() {
        // cursor is inside the window, position given
    } else {
        // cursor is not inside the window
    }
}

To detect when the pointer is moved, use CursorMoved events to get the updated coordinates:

fn cursor_events(
    mut cursor_evr: EventReader<CursorMoved>,
) {
    for ev in cursor_evr.iter() {
        println!(
            "New cursor position: X: {}, Y: {}, in Window ID: {:?}",
            ev.position.x, ev.position.y, ev.id
        );
    }
}

Note that you can only get the position of the mouse inside a window; you cannot get the global position of the mouse in the whole OS Desktop / on the screen as a whole.

Scrolling / Mouse Wheel

To detect scrolling input, use MouseWheel events:

fn scroll_events(
    mut scroll_evr: EventReader<MouseWheel>,
) {
    use bevy::input::mouse::MouseScrollUnit;
    for ev in scroll_evr.iter() {
        match ev.unit {
            MouseScrollUnit::Line => {
                println!("Scroll (line units): vertical: {}, horizontal: {}", ev.y, ev.x);
            }
            MouseScrollUnit::Pixel => {
                println!("Scroll (pixel units): vertical: {}, horizontal: {}", ev.y, ev.x);
            }
        }
    }
}

The MouseScrollUnit enum is important: it tells you the type of scroll input. Line is for hardware with fixed steps, like the wheel on desktop mice. Pixel is for hardware with smooth (fine-grained) scrolling, like laptop touchpads.

You should probably handle each of these differently (with different sensitivity settings), to provide a good experience on both types of hardware.

Controller / Gamepad / Joystick

You can detect when controllers/gamepads are connected or disconnected using GamepadEvent. Do this to get the ID of the device and assign it to a "player" in your game. You can easily support local multiplayer.

/// Simple resource to store the ID of the connected gamepad.
/// We need to know which gamepad to use for player input.
struct MyGamepad(Gamepad);

fn gamepad_connections(
    mut commands: Commands,
    my_gamepad: Option<Res<MyGamepad>>,
    mut gamepad_evr: EventReader<GamepadEvent>,
) {
    for GamepadEvent(id, kind) in gamepad_evr.iter() {
        match kind {
            GamepadEventType::Connected => {
                println!("New gamepad connected with ID: {:?}", id);

                // if we don't have any gamepad yet, use this one
                if my_gamepad.is_none() {
                    commands.insert_resource(MyGamepad(*id));
                }
            }
            GamepadEventType::Disconnected => {
                println!("Lost gamepad connection with ID: {:?}", id);

                // if it's the one we previously associated with the player,
                // disassociate it:
                if let Some(MyGamepad(old_id)) = my_gamepad.as_deref() {
                    if old_id == id {
                        commands.remove_resource::<MyGamepad>();
                    }
                }
            }
            // other events are irrelevant
            _ => {}
        }
    }
}

The other event types are for changes to axes and buttons. They are not shown above, because handling gamepad input via events is inconvenient.

You can handle the analog sticks with Axis<GamepadAxis>. Buttons can be handled with Input<GamepadButton> (similar to mouse buttons or keyboard keys).

fn gamepad_input(
    axes: Res<Axis<GamepadAxis>>,
    buttons: Res<Input<GamepadButton>>,
    my_gamepad: Option<Res<MyGamepad>>,
) {
    let gamepad = if let Some(gp) = my_gamepad {
        // a gamepad is connected, we have the id
        gp.0
    } else {
        // no gamepad is connected
        return;
    };

    // The joysticks are represented using a separate axis for X and Y

    let axis_lx = GamepadAxis(gamepad, GamepadAxisType::LeftStickX);
    let axis_ly = GamepadAxis(gamepad, GamepadAxisType::LeftStickY);

    if let (Some(x), Some(y)) = (axes.get(axis_lx), axes.get(axis_ly)) {
        // combine X and Y into one vector
        let left_stick_pos = Vec2::new(x, y);

        // implement a dead-zone to ignore small inputs
        if left_stick_pos.length() > 0.1 {
            // do something with the position of the left stick
        }
    }

    let jump_button = GamepadButton(gamepad, GamepadButtonType::South);
    let heal_button = GamepadButton(gamepad, GamepadButtonType::East);

    if buttons.just_pressed(jump_button) {
        // button pressed: make the player jump
    }

    if buttons.pressed(heal_button) {
        // button being held down: heal the player
    }
}

Notice that the names of buttons in the enum are vendor-neutral (like South and East instead of X/O or A/B). Many different kinds of hardware should work, but if your device is not supported, you should file an issue with the gilrs project.

Touchscreen

Multi-touch touchscreens are supported. You can track multiple fingers on the screen, with position and pressure/force information. Bevy does not offer gesture recognition.

The Touches resource allows you to track any fingers currently on the screen:

fn touches(
    touches: Res<Touches>,
) {
    // There is a lot more information available, see the API docs.
    // This example only shows some very basic things.

    for finger in touches.iter() {
        if touches.just_pressed(finger.id()) {
            println!("A new touch with ID {} just began.", finger.id());
        }
        println!(
            "Finger {} is at position ({},{}), started from ({},{}).",
            finger.id(),
            finger.position().x,
            finger.position().y,
            finger.start_position().x,
            finger.start_position().y,
        );
    }
}

Alternatively, you can use TouchInput events, but this is often harder to use:

fn touch_events(
    mut touch_evr: EventReader<TouchInput>,
) {
    use bevy::input::touch::TouchPhase;
    for ev in touch_evr.iter() {
        match ev.phase {
            TouchPhase::Started => {
                println!("Touch {} started at: {:?}", ev.id, ev.position);
            }
            TouchPhase::Moved => {
                println!("Touch {} moved to: {:?}", ev.id, ev.position);
            }
            TouchPhase::Ended => {
                println!("Touch {} ended at: {:?}", ev.id, ev.position);
            }
            TouchPhase::Cancelled => {
                println!("Touch {} cancelled at: {:?}", ev.id, ev.position);
            }
        }
    }
}