Tainted\\Coders

Bevy Input

Bevy version: 0.19Last updated:

Bevy provides two main ways to handle input:

  1. Reading messages emitted automatically by Bevy's input systems
  2. Polling resources like ButtonInput or Touches, or querying components like Gamepad

However, in practice most people would reach for something like bevy_enhanced_input which provides a 3rd, better way of handling input through observers.

Broadly, resources can be more useful when combining many inputs rather than reacting to a single one. You can accomplish anything with these 3 input styles.

Keyboard

To handle keyboard input we can 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 on the frame the press was processed
just_releasedwill return true on the frame the release was processed

We can listen to keyboard events in general by listening to KeyboardInput messages:

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

Or we can poll 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, independent of the keyboard layout, while the logical_key is the layout-dependent key value that the physical key produces.

The choice between them depends on what you are building:

For example, if you are creating a text editor, using the key_code would output gibberish on a keyboard with a different layout. The logical_key gives you the character the user actually typed.

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 and mouse wheel based inputs we can listen to the messages directly:

/// Track all mouse interactions — could power a UI system or camera control
fn mouse_debug_system(
  mut button_events: MessageReader<MouseButtonInput>,
  mut motion_events: MessageReader<MouseMotion>,
  mut cursor_events: MessageReader<CursorMoved>,
  mut wheel_events: MessageReader<MouseWheel>,
  mut pinch_events: MessageReader<PinchGesture>,
  mut rotation_events: MessageReader<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);
  }
}

Bevy also provides the AccumulatedMouseMotion and AccumulatedMouseScroll resources, which sum the events received during the current frame and reset to zero at the start of the next one. These are convenient when you only need the total delta for the frame.

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

use bevy::input::mouse::MouseScrollUnit;
// Fine grained control over mouse wheel events
fn scroll_events(mut events: MessageReader<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 user's interaction with a touchscreen enabled device.

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

During movement, many TouchPhase::Moved messages are sent. A TouchPhase::Canceled message is sent with the same ID when the system cancels tracking of the touch, such as when the window loses focus or, on iOS, when the device is held against the user's face.

/// 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());
  }
}

If you need the lower-level per-event data, you can also read TouchInput messages directly:

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

Gamepads

Bevy uses the gilrs crate to handle gamepads.

Each connected gamepad is spawned as an entity with a Gamepad component. The Gamepad component exposes the same pressed, just_pressed and just_released methods as ButtonInput, but for that gamepad only:

fn gamepad_system(gamepads: Query<(Entity, &Gamepad)>) {
  for (entity, gamepad) in &gamepads {
    if gamepad.just_pressed(GamepadButton::South) {
      info!("{entity} just pressed South");
    }
  }
}

There is no global ButtonInput<GamepadButton> resource. Each Gamepad component owns its own button and axis state.

Gamepad events

We have gamepad specific messages 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: MessageReader<GamepadConnectionEvent>,
  mut axis_events: MessageReader<GamepadAxisChangedEvent>,
  mut button_events: MessageReader<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);
  }
}

These are separate message streams, so the relative order between an axis change and a button change within the same frame is not preserved when reading them individually.

If you care about in-frame ordering you can read the single GamepadEvent stream instead and react to the message types. Bevy uses the GamepadSettings component (required by Gamepad) to filter messages 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: MessageReader<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),
    }
  }
}

Because GamepadEvent is a single stream, a GamepadEvent::Button and a GamepadEvent::Axis are read in the order the raw events arrived. If you need the unfiltered events, before GamepadSettings are applied, use the equivalent RawGamepadEvent type and filter the messages yourself.

Gamepad axis

Axes live on the Gamepad component as an Axis<GamepadInput> field. The GamepadAxis enum describes the different types of axis available on our controllers:

pub enum GamepadAxis {
  // The horizontal value of the left stick.
  LeftStickX,
  // The vertical value of the left stick.
  LeftStickY,
  // Generally the throttle axis of a HOTAS setup.
  LeftZ,
  // The horizontal value of the right stick.
  RightStickX,
  // The vertical value of the right stick.
  RightStickY,
  // The yaw of the main joystick, not supported on common gamepads.
  RightZ,
  // Non-standard support for other axis
  // types (i.e. HOTAS sliders, potentiometers, etc).
  Other(u8),
}

You can read an axis with Gamepad::get (clamped) or Gamepad::get_unclamped:

/// Read an analog axis from each connected gamepad
fn gamepad_axis_system(gamepads: Query<&Gamepad>) {
  for gamepad in &gamepads {
    if let Some(left_stick_x) = gamepad.get(GamepadAxis::LeftStickX) {
      info!("Left stick X: {}", left_stick_x);
    }
  }
}

Gamepad haptics

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

/// Provide rumble feedback — simulate an explosion or impact
fn rumble_feedback_system(
  gamepads: Query<(Entity, &Gamepad)>,
  mut rumble_writer: MessageWriter<GamepadRumbleRequest>,
) {
  for (entity, gamepad) in &gamepads {
    if gamepad.just_pressed(GamepadButton::RightTrigger2) {
      let event = GamepadRumbleRequest::Add {
        gamepad: entity,
        intensity: GamepadRumbleIntensity::strong_motor(0.5),
        duration: Duration::from_millis(300),
      };

      rumble_writer.write(event);

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

The GamepadRumbleRequest::Add variant 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 input-related messages from bevy_window that may be used to trigger your game logic.

Window-specific input messages include:

For example, CursorMoved is sent from the window itself. Unlike MouseMotion, which reports raw device motion, it reports the cursor position inside a window and includes an optional delta:

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

Bevy enhanced input

There are some gotchas when it comes to input handling that can be troublesome for those starting out.

If you use avian and you run your input handling in an Update schedule, you will run into some frame issues because avian is running in FixedUpdate schedule which runs before that.

There are other common issues that can be eliminated by making your inputs observers which trigger their behavior right away on certain events instead of in a specific schedule and so are less susceptible to scheduling issues.

There is an awesome library that will do this for you and may eventually be upstreamed into Bevy: bevy_enhanced_input.

It allows us to define a set of actions that will produce events when their bindings are fired:

pub fn add_car_controls(event: On<Add, Car>, mut commands: Commands) {
  commands.entity(event.event_target()).insert(actions!(Car[
    (
      Action::<AirRollLeftAction>::new(),
      bindings![KeyCode::KeyQ, GamepadButton::East],
    ),
    (
      Action::<AirRollRightAction>::new(),
      bindings![KeyCode::KeyE, GamepadButton::West],
    ),
  ]));
}

Then we can use observers to react to different parts of the lifecycle of a button press:

pub fn on_air_roll_left(
  event: On<Fire<AirRollLeftAction>>,
  mut cars: Query<
    (&Car, &Transform, &mut AngularVelocity),
    (Without<Dodging>, With<Jumping>),
  >,
) {
  let Ok((car, transform, mut angular_velocity)) = cars.get_mut(event.context) else {
    return;
  };

  let forward = *transform.forward();
  let current_roll = angular_velocity.0.dot(forward);
  let target_roll = -car.air_roll_rate;

  angular_velocity.0 += forward * (target_roll - current_roll);
}