Tainted \\ Coders

Bevy Input

Last updated:

There are two ways to handle input in Bevy:

  1. Reacting to the events emitted automatically by Bevy’s input systems
  2. Querying a resource like ButtonInput, Axis, Touches or Gamepads

Events are useful to handle many types of input at the same time, which can be useful for ordering the input relatively within the same frame.

Events help us create logic such as:

Run this system every time ANY key is pressed

Resources on the other hand, give us more control over the type of input state. Reading events can be too broad when we want to trigger state specific logic like:

Make my character jump anytime the SPACE key has just been pressed

Bevy has a different resource for each type of input:

  • Axis stores the position data from certain input devices
  • ButtonInput a “press-able” input
  • GamepadAxis An axis of a gamepad
  • GamepadButton represents a single button of a gamepad just like a keyboard
  • Gamepads represents a collection of connected game controllers
  • TouchInput represents touch based input events
  • Touches a collection of Touches that have happened

Keyboard

To handle keyboard input we use the ButtonInput<T> resource which has a set of convenient methods we can use to trigger behavior:

Method Description
pressed will return true between a press and release event
just_pressed will return true for one frame after a press event
just_released will return true for one frame after a release event

Internally, Bevy has builtin systems like keyboard_input_system, mouse_button_input_system, and others are calling ButtonInput<T>::press or ButtonInput<T>::release to trigger the events for that particular input type.

These get added to the ButtonInput<T> resource and are read to check if something was pressed or not.

We can read these events in general by listening to KeyboardInput events:

fn keyboard_system(mut keyboard_events: EventReader<KeyboardInput>) {
    for event in keyboard_events.read(){
        println!(
            "KeyCode: {:?} ScanCode {:?}",
            event.key_code,
            event.logical_key
        );
    }
}

Or we can use the resource to check for a more specific state:

fn jump_system(input: Res<ButtonInput<KeyCode>>) {
    if input.just_pressed(KeyCode::Space) {
        // Jumping!
    }
}

Physical vs logical keys

Keyboard input has two separate fields representing the key:

pub struct KeyboardInput {
    pub key_code: KeyCode,
    pub logical_key: Key,
    pub state: ButtonState,
    pub window: Entity,
}

The key_code represents the physical location of a key, while the logical_key is the mapping from this physical key to the winit key.

This is so we can have our physical keys mapped to alternative logical keys. For example rebinding ESC to CAPS. The physical key would remain the same but the logical key would be different.

One example of this difference can would be if you are creating a text editor. If you listened to KeyboardInput events like before, using the key code might output gibberish if they used a different layout.

Bevy has a convenient way of listening to the actual characters output by typing on the keyboard:

use bevy::window::ReceivedCharacter;

fn output_characters(mut events: EventReader<ReceivedCharacter>) {
    for event in events.read() {
        info!(
            "{:?}: '{}'",
            event,
            event.char
        );
    }
}

Modifiers

We can handle modifiers like shift or alt by checking for them before our key and set local variables to modify the input:

// This system prints when `Ctrl + Shift + A` is pressed
fn keyboard_input_system(input: Res<ButtonInput<KeyCode>>) {
    let shift = input.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
    let ctrl = input.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);

    if ctrl && shift && input.just_pressed(KeyCode::KeyA) {
        info!("Just pressed Ctrl + Shift + A!");
    }
}

Mouse

The mouse is handled in exactly the same way as your keyboard. Both the right and left clicks are combined in the ButtonInput<MouseButton> type:

fn shooting_system(mouse_button_input: Res<ButtonInput<MouseButton>>) {
    if mouse_button_input.just_pressed(MouseButton::Left) {
        // Start firing bullets
    }

    if mouse_button_input.pressed(MouseButton::Left) {
        // Continue firing bullets
    }

    if mouse_button_input.just_released(MouseButton::Left) {
        // Stop firing bullets
    }
}

For movement, mouse wheel and touchpad based inputs we don’t use a resource but listen to the events directly:

fn listen_to_all_mouse_based_events(
    mut mouse_button_input_events: EventReader<MouseButtonInput>,
    mut mouse_motion_events: EventReader<MouseMotion>,
    mut cursor_moved_events: EventReader<CursorMoved>,
    mut mouse_wheel_events: EventReader<MouseWheel>,
    mut touchpad_magnify_events: EventReader<TouchpadMagnify>,
    mut touchpad_rotate_events: EventReader<TouchpadRotate>,
) {
    for event in mouse_button_input_events.read() {
        info!("{:?}", event);
    }

    for event in mouse_motion_events.read() {
        info!("{:?}", event);
    }

    for event in cursor_moved_events.read() {
        info!("{:?}", event);
    }

    for event in mouse_wheel_events.read() {
        info!("{:?}", event);
    }

    // This event will only fire on macOS
    for event in touchpad_magnify_events.read() {
        info!("{:?}", event);
    }

    // This event will only fire on macOS
    for event in touchpad_rotate_events.read() {
        info!("{:?}", event);
    }
}

Touch

Touches represent a users interaction with a touchscreen enabled device.

When a user first touches their screen a TouchPhase::Started event is sent with a unique ID. When the user lifts their finger a matching TouchPhase::Ended event is sent with that same ID.

During movement, many TouchPhase::Moved events are sent, or if the iOS screen changes a TouchPhase::Canceled is sent with the same ID matching system.

fn touch_system(touches: Res<Touches>) {
    for touch in touches.iter_just_pressed() {
        info!(
            "just pressed touch with id: {:?}, at: {:?}",
            touch.id(),
            touch.position()
        );
    }

    for touch in touches.iter_just_released() {
        info!(
            "just released touch with id: {:?}, at: {:?}",
            touch.id(),
            touch.position()
        );
    }

    for touch in touches.iter_just_canceled() {
        info!("canceled touch with id: {:?}", touch.id());
    }

    // you can also iterate all current touches and retrieve their state like this:
    for touch in touches.iter() {
        info!("active touch: {:?}", touch);
        info!("  just_pressed: {}", touches.just_pressed(touch.id()));
    }
}

Touches also have events representing any kind of touch input actions:

fn touch_event_system(mut touch_events: EventReader<TouchInput>) {
    for event in touch_events.read() {
        info!("{:?}", event);
    }
}

Gamepads

Each Gamepad is assigned a unique ID so we can associate them with the input from a specific player. We can list these IDs by asking the Gamepads resource:

fn gamepad_system(
    gamepads: Res<Gamepads>,
    button_inputs: Res<ButtonInput<GamepadButton>>
) {
    for gamepad in gamepads.iter() {
        if button_inputs.just_pressed(
            GamepadButton::new(gamepad, GamepadButtonType::South)
        ) {
            // The player is moving down on the joystick
        }
    }
}

Notice that gamepads also use a ButtonInput and have access to the same pressed, just_pressed and just_released methods.

We have gamepad specific events we can read such as when a player connects their controller or presses a button:

fn gamepad_events(
    mut gamepad_connection_events: EventReader<GamepadConnectionEvent>,
    mut gamepad_axis_events: EventReader<GamepadAxisChangedEvent>,
    mut gamepad_button_events: EventReader<GamepadButtonChangedEvent>,
) {
    for event in gamepad_connection_events.read() {
        // ...
    }

    for event in gamepad_axis_events.read() {
        // ...
    }

    for event in gamepad_button_events.read() {
        // ...
    }
}

The problem with reading events like this is that they will be potentially out of order. For example if the player A and then held down within the same frame, the above system would read them out of order.

To ensure in-frame ordering we can read from the more general GamepadEvent and switch on the type:

fn gamepad_ordered_events(mut gamepad_events: EventReader<GamepadEvent>) {
    for gamepad_event in gamepad_events.read() {
        match gamepad_event {
            GamepadEvent::Connection(event) => info!("{:?}", event),
            GamepadEvent::Button(event) => info!("{:?}", event),
            GamepadEvent::Axis(event) => info!("{:?}", event),
        }
    }
}

Gamepads use a slightly different system with an Axis<T> resource. Axis<GamepadAxis> represents the different types of axis we have available on our controllers:

pub enum GamepadAxisType {
    // The horizontal value of the left stick.
    LeftStickX,
    // The vertical value of the left stick.
    LeftStickY,
    // The value of the left `Z` button.
    LeftZ,
    // The horizontal value of the right stick.
    RightStickX,
    // The vertical value of the right stick.
    RightStickY,
    // The value of the right `Z` button.
    RightZ,
    // Non-standard support for other axis 
    // types (i.e. HOTAS sliders, potentiometers, etc).
    Other(u8),
}

You can add haptic feedback by writing events to request it:

fn gamepad_system_with_rumble(
    gamepads: Res<Gamepads>,
    button_inputs: Res<ButtonInput<GamepadButton>>,
    mut rumble_requests: EventWriter<GamepadRumbleRequest>,
) {
    for gamepad in gamepads.iter() {
        // ...
        rumble_requests.send(GamepadRumbleRequest::Add {
            gamepad,
            intensity: GamepadRumbleIntensity::strong_motor(0.1),
            duration: Duration::from_secs(5),
        });
    }
}

The GamepadRumbleRequest::Add event triggers a force-feedback motor, controlling how long the vibration should last, the motor to activate, and the vibration strength. GamepadRumbleRequest::Stop immediately stops all motors.

Rumble features are provided by the bevy_gltf crate.

Window

There are also events that come from the bevy_window that are related to input and may also be used to trigger your game logic:

// This system prints out all char events as they come in
// ReceivedCharacter comes from `bevy_window` not `bevy_input`
fn print_characters_pressed(
    mut events: EventReader<ReceivedCharacter>
) {
    for event in events.read() {
        info!(
            "{:?}: '{}'",
            event,
            event.char
        );
    }
}

The char field of ReceivedCharacter is a SmolStr which may contain multiple characters. An alternative to this that has better cross plaform support is to listen for KeyboardInput and use the logical_key:

fn keyboard_system(mut keyboard_events: EventReader<KeyboardInput>) {
    for event in keyboard_events.read(){
        info!(
            "{:?} {:?}",
            event,
            event.logical_key
        );
    }
}

Mouse events such as CursorMoved are also sent from the window itself:

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

Read more