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::{
ecs::{schedule::ScheduleLabel, system::ScheduleSystem},
input::mouse::AccumulatedMouseScroll,
platform::collections::{HashMap, HashSet},
prelude::*,
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
/// digital (for [`Instant`](ActionKind::Instant)/[`Toggle`](ActionKind::Toggle)
/// actions); [`Stick`](Self::Stick) is analog (for [`Axis`](ActionKind::Axis)
/// actions).
/// actions); [`Stick`](Self::Stick) and [`Scroll`](Self::Scroll) are analog (for
/// [`Axis`](ActionKind::Axis) actions).
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Reflect)]
pub enum Binding {
/// A keyboard key.
@ -271,6 +272,12 @@ pub enum Binding {
/// A gamepad analog source producing a [`Vec2`] (matched across all connected
/// gamepads; deadzone already applied by Bevy).
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.
@ -557,28 +564,39 @@ fn hardware_to_actions<T: GameAction>(
keys: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>,
pads: Query<&Gamepad>,
scroll: Res<AccumulatedMouseScroll>,
map: Res<InputMap<T>>,
mut state: ResMut<ActionState<T>>,
) {
for (&action, bindings) in &map.bindings {
match action.kind() {
// Analog: pick the most-deflected reading across sticks/gamepads and
// store it every frame (zero when nothing is connected or centered).
// Analog: pick the strongest reading across analog sources and store it
// every frame (zero when nothing is deflected / not scrolling).
ActionKind::Axis => {
let mut value = Vec2::ZERO;
for binding in bindings {
let Binding::Stick(stick) = binding else {
continue;
};
for pad in &pads {
let reading = match stick {
Stick::Left => pad.left_stick(),
Stick::Right => pad.right_stick(),
Stick::DPad => pad.dpad(),
};
if reading.length_squared() > value.length_squared() {
value = reading;
let reading = match binding {
Binding::Stick(stick) => {
// Best deflection across all connected gamepads.
let mut best = Vec2::ZERO;
for pad in &pads {
let r = match stick {
Stick::Left => pad.left_stick(),
Stick::Right => pad.right_stick(),
Stick::DPad => pad.dpad(),
};
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);
@ -591,7 +609,8 @@ fn hardware_to_actions<T: GameAction>(
Binding::Key(k) => keys.just_pressed(*k),
Binding::Mouse(m) => mouse.just_pressed(*m),
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 {
match kind {

Loading…
Cancel
Save