Add Binding::Scroll as an axis input source

The mouse wheel's per-frame delta is a Vec2, so it maps directly onto
axis actions alongside gamepad sticks. hardware_to_actions now reads
AccumulatedMouseScroll in the Axis branch (max-magnitude combine); the
digital branch ignores it, and Scalar is unchanged.

Since an axis is refreshed every frame and the scroll delta is zero when
idle, on_action fires exactly while the wheel turns and the piped value
is the delta — no persistence or edge-guarding needed. Enables e.g.
`.bind(Action::Zoom, [Binding::Scroll])`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main
Elijah Voigt 6 days ago
parent 09e6c66788
commit 2f59af15df

@ -137,6 +137,7 @@ use core::marker::PhantomData;
use bevy::{ use bevy::{
ecs::{schedule::ScheduleLabel, system::ScheduleSystem}, ecs::{schedule::ScheduleLabel, system::ScheduleSystem},
input::mouse::AccumulatedMouseScroll,
platform::collections::{HashMap, HashSet}, platform::collections::{HashMap, HashSet},
prelude::*, prelude::*,
ui::Checked, ui::Checked,
@ -258,8 +259,8 @@ pub trait GameAction: Copy + Eq + Hash + Send + Sync + 'static {
/// ///
/// [`Key`](Self::Key), [`Mouse`](Self::Mouse) and [`Gamepad`](Self::Gamepad) are /// [`Key`](Self::Key), [`Mouse`](Self::Mouse) and [`Gamepad`](Self::Gamepad) are
/// digital (for [`Instant`](ActionKind::Instant)/[`Toggle`](ActionKind::Toggle) /// digital (for [`Instant`](ActionKind::Instant)/[`Toggle`](ActionKind::Toggle)
/// actions); [`Stick`](Self::Stick) is analog (for [`Axis`](ActionKind::Axis) /// actions); [`Stick`](Self::Stick) and [`Scroll`](Self::Scroll) are analog (for
/// actions). /// [`Axis`](ActionKind::Axis) actions).
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Reflect)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Reflect)]
pub enum Binding { pub enum Binding {
/// A keyboard key. /// A keyboard key.
@ -271,6 +272,12 @@ pub enum Binding {
/// A gamepad analog source producing a [`Vec2`] (matched across all connected /// A gamepad analog source producing a [`Vec2`] (matched across all connected
/// gamepads; deadzone already applied by Bevy). /// gamepads; deadzone already applied by Bevy).
Stick(Stick), Stick(Stick),
/// The mouse wheel, producing this frame's scroll delta as a [`Vec2`]
/// (`x` = horizontal, `y` = vertical). Like a stick, it drives
/// [`Axis`](ActionKind::Axis) actions — but it is a *delta* (non-zero only
/// while scrolling), so the axis reads zero on idle frames and
/// [`on_action`] fires exactly while the wheel turns.
Scroll,
} }
/// An analog 2D source on a gamepad. /// An analog 2D source on a gamepad.
@ -557,28 +564,39 @@ fn hardware_to_actions<T: GameAction>(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>, mouse: Res<ButtonInput<MouseButton>>,
pads: Query<&Gamepad>, pads: Query<&Gamepad>,
scroll: Res<AccumulatedMouseScroll>,
map: Res<InputMap<T>>, map: Res<InputMap<T>>,
mut state: ResMut<ActionState<T>>, mut state: ResMut<ActionState<T>>,
) { ) {
for (&action, bindings) in &map.bindings { for (&action, bindings) in &map.bindings {
match action.kind() { match action.kind() {
// Analog: pick the most-deflected reading across sticks/gamepads and // Analog: pick the strongest reading across analog sources and store it
// store it every frame (zero when nothing is connected or centered). // every frame (zero when nothing is deflected / not scrolling).
ActionKind::Axis => { ActionKind::Axis => {
let mut value = Vec2::ZERO; let mut value = Vec2::ZERO;
for binding in bindings { for binding in bindings {
let Binding::Stick(stick) = binding else { let reading = match binding {
continue; Binding::Stick(stick) => {
}; // Best deflection across all connected gamepads.
for pad in &pads { let mut best = Vec2::ZERO;
let reading = match stick { for pad in &pads {
Stick::Left => pad.left_stick(), let r = match stick {
Stick::Right => pad.right_stick(), Stick::Left => pad.left_stick(),
Stick::DPad => pad.dpad(), Stick::Right => pad.right_stick(),
}; Stick::DPad => pad.dpad(),
if reading.length_squared() > value.length_squared() { };
value = reading; if r.length_squared() > best.length_squared() {
best = r;
}
}
best
} }
// This frame's wheel delta (zero when not scrolling).
Binding::Scroll => scroll.delta,
_ => continue,
};
if reading.length_squared() > value.length_squared() {
value = reading;
} }
} }
state.set_axis(action, value); state.set_axis(action, value);
@ -591,7 +609,8 @@ fn hardware_to_actions<T: GameAction>(
Binding::Key(k) => keys.just_pressed(*k), Binding::Key(k) => keys.just_pressed(*k),
Binding::Mouse(m) => mouse.just_pressed(*m), Binding::Mouse(m) => mouse.just_pressed(*m),
Binding::Gamepad(g) => pads.iter().any(|pad| pad.just_pressed(*g)), Binding::Gamepad(g) => pads.iter().any(|pad| pad.just_pressed(*g)),
Binding::Stick(_) => false, // not meaningful for digital actions // Analog sources are not meaningful for digital actions.
Binding::Stick(_) | Binding::Scroll => false,
}; };
if just_pressed { if just_pressed {
match kind { match kind {

Loading…
Cancel
Save