--- name: bevy-input-handling description: "Bevy ECS skill for player input. Keywords: Bevy, ButtonInput, KeyCode, MouseButton, Gamepad." --- # Bevy input handling ## When to use Load this when a system should read keyboard, mouse, or gamepad state. The `bevy::input` module on Bevy 0.20 is the source for the names below. ## ECS shape - **Plugin:** `InputPlugin` adds input from keyboard, mouse, gamepad, and touch. It ships inside the usual app plugins. `InputSystems` labels the systems that refresh that data. - **Keyboard resource:** `Res>`. `KeyCode` is the physical key (layout-stable). `Res>` is the logical key. The keyboard example uses `pressed`, `just_pressed`, and `just_released`. - **Mouse:** `mouse_button_input_system` writes `ButtonInput` from `MouseButtonInput`. `AccumulatedMouseMotion` and `AccumulatedMouseScroll` are resources reset every frame. `MouseMotion` and `MouseWheel` are the underlying events. - **Gamepad:** `Gamepad` stores a connected pad's metadata (`GamepadButton`, `GamepadAxis`). Connection and axis changes arrive as `GamepadConnectionEvent`, `GamepadAxisChangedEvent`, and `GamepadButtonChangedEvent`. `GamepadRumbleRequest` is the force-feedback event. - **Schedule:** read these resources from a system on `Update` (or a schedule you already chain). Do not invent an input-map resource. ## Coming from Godot | Godot idea | Bevy idiom | | --- | --- | | Action just pressed | `ButtonInput::just_pressed` on `KeyCode` or `MouseButton` | | Action held | `ButtonInput::pressed` | | Physical WASD vs symbol keys | `KeyCode` for location, `Key` for the character | | Mouse look delta | `AccumulatedMouseMotion` (reset each frame) | | Controller | `Gamepad` plus the gamepad events listed above | ## Scope Keyboard, mouse, and gamepad state live on `ButtonInput` and `Gamepad`. Named actions, a jump buffer, a radial stick deadzone, and the last active device are resources you define. Glyph art is a UI concern once that device flag is set. ## Examples ```toml [dependencies] bevy = "0.20" ``` Read edges in `Update`. `FixedUpdate` can run zero or several times per frame, so a press sampled there is dropped or repeated. Store the edge on an intent component and let the fixed step consume it. ```rust #[derive(Component, Default)] struct MoveIntent { axis: Vec2, jump: bool, } fn read_keys(keys: Res>, mut intents: Query<&mut MoveIntent>) { let mut axis = Vec2::ZERO; if keys.pressed(KeyCode::KeyA) || keys.pressed(KeyCode::ArrowLeft) { axis.x -= 1.0; } if keys.pressed(KeyCode::KeyD) || keys.pressed(KeyCode::ArrowRight) { axis.x += 1.0; } if keys.pressed(KeyCode::KeyW) || keys.pressed(KeyCode::ArrowUp) { axis.y += 1.0; } if keys.pressed(KeyCode::KeyS) || keys.pressed(KeyCode::ArrowDown) { axis.y -= 1.0; } let jump = keys.just_pressed(KeyCode::Space); for mut intent in &mut intents { intent.axis = axis; if jump { intent.jump = true; } } } use bevy::input::mouse::AccumulatedMouseMotion; fn look(motion: Res, mut cameras: Query<&mut Transform, With>) { for mut tf in &mut cameras { tf.rotate_y(-motion.delta.x * 0.002); tf.rotate_local_x(-motion.delta.y * 0.002); } } #[derive(Component)] struct LookCam; ``` A radial deadzone drops stick drift, then rescales the remaining range so full deflection still reaches length 1. The jump buffer keeps a press that landed a few frames early. ```rust #[derive(Component, Default)] struct MoveIntent { axis: Vec2, jump: bool, } fn radial_deadzone(stick: Vec2, deadzone: f32) -> Vec2 { let mag = stick.length(); if mag <= deadzone || mag == 0.0 { return Vec2::ZERO; } let scaled = ((mag - deadzone) / (1.0 - deadzone)).clamp(0.0, 1.0); stick / mag * scaled } fn read_pad(pads: Query<&Gamepad>, mut intents: Query<&mut MoveIntent>) { let Some(pad) = pads.iter().next() else { return; }; let stick = Vec2::new( pad.get(GamepadAxis::LeftStickX).unwrap_or(0.0), pad.get(GamepadAxis::LeftStickY).unwrap_or(0.0), ); let axis = radial_deadzone(stick, 0.15); let jump = pad.just_pressed(GamepadButton::South); for mut intent in &mut intents { if axis.length_squared() > 0.0 { intent.axis = axis; } if jump { intent.jump = true; } } } #[derive(Resource)] struct JumpBuffer { remaining: f32, window: f32, } fn arm_jump_buffer(time: Res