Tainted\\Coders

Bevy Input

Bevy version: 0.16Last 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:

ResourceDescription
Axisstores the position data from certain input devices
ButtonInputa "press-able" input
GamepadAxisAn axis of a gamepad
GamepadButtonrepresents a single button of a gamepad just like a keyboard
Gamepadsrepresents a collection of connected game controllers
TouchInputrepresents touch based input events
Touchesa collection of Touches that have happened

There are some useful libraries for managing your inputs:

Bevy has upstreamed bevy_mod_picking and is working on upstreaming leafwing-input-manager eventually in a future version.

Keyboard

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

MethodDescription
pressedwill return true between a press and release event
just_pressedwill return true for one frame after a press event
just_releasedwill return true for one frame after a release event

Internally, Bevy has builtin systems like keyboard_input_system, mouse_button_input_system. These input systems are reacting to events to set the state of the resources.

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

/// Track keyboard inputs — useful for debugging or keybinding tools
fn log_keyboard_input(mut keyboard_events: EventReader<KeyboardInput>) {
    for event in keyboard_events.read() {
        println!(
            "Key pressed: {:?}, logical key: {:?}",
            event.key_code, event.logical_key
        );
    }
}

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

/// Handle player jump
fn jump_input_system(input: Res<ButtonInput<KeyCode>>) {
    if input.just_pressed(KeyCode::Space) {
        info!("Jump!");
    }
}

Physical vs logical keys

Keyboard input has two separate fields representing the key:

// https://docs.rs/bevy/latest/bevy/input/keyboard/struct.KeyboardInput.html
pub struct KeyboardInput {
  pub key_code: KeyCode,
  pub logical_key: Key,
  pub state: ButtonState,
  pub text: Option<SmolStr>,
  pub repeat: bool,
  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.

Bevy encourages us to use physical keys over 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.

Modifiers

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

/// Handle combos or hotkeys, e.g., casting special ability
fn combo_key_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!("Special ability activated! (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:

/// Fire weapon using mouse input — standard FPS/twin-stick shooter logic
fn shoot_input_system(mouse: Res<ButtonInput<MouseButton>>) {
  if mouse.just_pressed(MouseButton::Left) {
    info!("Bang! Weapon fired.");
  }

  if mouse.pressed(MouseButton::Left) {
    info!("Holding trigger... continuing fire.");
  }

  if mouse.just_released(MouseButton::Left) {
    info!("Stopped firing.");
  }
}

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

/// Track all mouse interactions — could power a UI system or camera control
fn mouse_debug_system(
  mut button_events: EventReader<MouseButtonInput>,
  mut motion_events: EventReader<MouseMotion>,
  mut cursor_events: EventReader<CursorMoved>,
  mut wheel_events: EventReader<MouseWheel>,
  mut pinch_events: EventReader<PinchGesture>,
  mut rotation_events: EventReader<RotationGesture>,
) {
  for event in button_events.read() {
    info!("Mouse button event: {:?}", event);
  }
  for event in motion_events.read() {
    info!("Mouse moved: {:?}", event);
  }
  for event in cursor_events.read() {
    info!("Cursor moved: {:?}", event);
  }
  for event in wheel_events.read() {
    info!("Mouse wheel used: {:?}", event);
  }
  for event in pinch_events.read() {
    info!("Pinch gesture detected (macOS only): {:?}", event);
  }
  for event in rotation_events.read() {
    info!("Rotation gesture detected (macOS only): {:?}", event);
  }
}

For finer grained control over scrolling we can use check the MouseWheel event's unit type:

use bevy::input::mouse::MouseScrollUnit;

// Fine grained control over mouse wheel events
fn scroll_events(mut events: EventReader<MouseWheel>) {
  for event in events.read() {
    match event.unit {
      MouseScrollUnit::Line => {
        info!(
          "Scroll (line units): vertical: {}, horizontal: {}",
          event.y, event.x
        );
      }
      MouseScrollUnit::Pixel => {
        info!(
          "Scroll (pixel units): vertical: {}, horizontal: {}",
          event.y, event.x
        );
      }
    }
  }
}

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.

/// Handle touchscreen taps — perfect for mobile games
fn touch_input_system(touches: Res<Touches>) {
    for touch in touches.iter_just_pressed() {
        info!("Screen tapped at {:?}", touch.position());
    }
    for touch in touches.iter_just_released() {
        info!("Touch ended at {:?}", touch.position());
    }
    for touch in touches.iter_just_canceled() {
        info!("Touch canceled: {:?}", touch.id());
    }
    for touch in touches.iter() {
        info!("Ongoing touch at {:?}", touch.position());
    }
}

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

/// Lower-level touch events — for finer-grained control
fn raw_touch_event_system(mut touch_events: EventReader<TouchInput>) {
  for event in touch_events.read() {
    info!("Raw touch input: {:?}", event);
  }
}

Gamepads

Gamepads have changed in Bevy 0.15 to unify keyboard and gamepad APIs.

Bevy uses the gilrs crate to handle gamepads.

Each Gamepad is assigned a unique ID so we can associate them with the input from a specific player.

fn gamepad_system(
  gamepads: Query<&Gamepad>,
  button_inputs: Res<ButtonInput<GamepadButton>>,
) {
  for gamepad in &gamepads {
    if button_inputs.just_pressed(GamepadButton::South) {
      // ...
    }
  }
}

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

Gamepad events

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

/// Detect connection, axis, and button changes individually
fn separate_gamepad_event_readers(
  mut connect_events: EventReader<GamepadConnectionEvent>,
  mut axis_events: EventReader<GamepadAxisChangedEvent>,
  mut button_events: EventReader<GamepadButtonChangedEvent>,
) {
  for event in connect_events.read() {
    info!("Gamepad connection: {:?}", event);
  }
  for event in axis_events.read() {
    info!("Analog stick moved: {:?}", event);
  }
  for event in button_events.read() {
    info!("Button changed: {:?}", event);
  }
}

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.

If you care about ordering you would want to read the GamepadEvent and react to the event types. Bevy is going to use your GamepadSettings to filter events for you. Most of these settings hold thresholds and limits for button presses and axis changes.

/// Read all gamepad events in one go — ideal for analytics or debugging
fn unified_gamepad_event_reader(mut gamepad_events: EventReader<GamepadEvent>) {
  for event in gamepad_events.read() {
    match event {
      GamepadEvent::Connection(e) => info!("Gamepad connection: {:?}", e),
      GamepadEvent::Button(e) => info!("Gamepad button event: {:?}", e),
      GamepadEvent::Axis(e) => info!("Gamepad axis event: {:?}", e),
    }
  }
}

This solves whether we pressed A and then X as these will be read in the correct order now.

However, between these different event types this system won't fully respect in-frame relative ordering. Meaning that if you care about whether a GamepadEvent::Button happened before a GamepadEvent::Axis then you might not be able to rely on these events.

Instead you can use the equivalent RawGamepadEvent type and filter the events yourself.

Gamepad axis

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),
}

Gamepad haptics

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

/// Provide rumble feedback — simulate an explosion or impact
fn rumble_feedback_system(
  gamepads: Query<Entity, With<Gamepad>>,
  button_inputs: Res<ButtonInput<GamepadButton>>,
  mut rumble_writer: EventWriter<GamepadRumbleRequest>,
) {
  for gamepad in &gamepads {
    if button_inputs.just_pressed(GamepadButton::RightTrigger) {
      rumble_writer.write(GamepadRumbleRequest::Add {
        gamepad,
        intensity: GamepadRumbleIntensity::strong_motor(0.5),
        duration: Duration::from_millis(300),
      });

      info!("Rumble triggered!");
    }
  }
}

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.

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.

For example we can listen to KeyboardInput events to get the logical_key that was pressed:

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

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

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