diff --git a/Cargo.toml b/Cargo.toml index 567c6339..aa6a2fcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,10 +36,9 @@ egui = ['dep:bevy_egui'] [dependencies] leafwing_input_manager_macros = { path = "macros", version = "0.13" } -bevy = { version = "0.14.0-rc.2", default-features = false, features = [ +bevy = { version = "0.14.0-rc.3", default-features = false, features = [ "serialize", ] } -bevy_ecs = { version = "0.14.0-rc.2", features = ["serde"] } bevy_egui = { git = "https://github.com/Friz64/bevy_egui", branch = "bevy-0.14", optional = true } derive_more = { version = "0.99", default-features = false, features = [ @@ -55,8 +54,7 @@ dyn-hash = "0.2" once_cell = "1.19" [dev-dependencies] -bevy_ecs = { version = "0.14.0-rc.2", features = ["serde"] } -bevy = { version = "0.14.0-rc.2", default-features = false, features = [ +bevy = { version = "0.14.0-rc.3", default-features = false, features = [ "bevy_asset", "bevy_sprite", "bevy_text", diff --git a/RELEASES.md b/RELEASES.md index 3b843d76..f48484dc 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -155,6 +155,7 @@ Input processors allow you to create custom logic for axis-like input manipulati - fixed a bug where enabling a pressed action would read as `just_pressed`, and disabling a pressed action would read as `just_released`. - fixed a bug in `InputStreams::button_pressed()` where unrelated gamepads were not filtered out when an `associated_gamepad` is defined. - inputs are now handled correctly in the `FixedUpdate` schedule! Previously, the `ActionState`s were only updated in the `PreUpdate` schedule, so you could have situations where an action was marked as `just_pressed` multiple times in a row (if the `FixedUpdate` schedule ran multiple times in a frame) or was missed entirely (if the `FixedUpdate` schedule ran 0 times in a frame). +- Mouse motion and mouse scroll are now computed more efficiently and reliably, through the use of the new `AccumulatedMouseMovement` and `AccumulatedMouseScroll` resources. ### Tech debt diff --git a/src/action_state.rs b/src/action_state.rs index 3c43da8b..a7804215 100644 --- a/src/action_state.rs +++ b/src/action_state.rs @@ -745,6 +745,7 @@ mod tests { use crate::input_map::InputMap; use crate::input_mocking::MockInput; use crate::input_streams::InputStreams; + use crate::plugin::AccumulatorPlugin; use crate::prelude::InputChord; use bevy::input::InputPlugin; use bevy::prelude::*; @@ -754,7 +755,7 @@ mod tests { #[test] fn press_lifecycle() { let mut app = App::new(); - app.add_plugins(InputPlugin); + app.add_plugins(InputPlugin).add_plugins(AccumulatorPlugin); #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug, bevy::prelude::Reflect)] enum Action { @@ -840,7 +841,7 @@ mod tests { input_map.insert(Action::OneAndTwo, InputChord::new([Digit1, Digit2])); let mut app = App::new(); - app.add_plugins(InputPlugin); + app.add_plugins(InputPlugin).add_plugins(AccumulatorPlugin); // Action state let mut action_state = ActionState::::default(); diff --git a/src/axislike.rs b/src/axislike.rs index 0a1cc477..2151ba1f 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -236,7 +236,7 @@ impl DualAxisData { Dir2::new(self.xy).ok() } - /// The [`Rotation`] (measured clockwise from midnight) that this axis is pointing towards, if any + /// The [`Rot2`] that this axis is pointing towards, if any. /// /// If the axis is neutral (x,y) = (0,0), this will be `None` #[must_use] diff --git a/src/errors.rs b/src/errors.rs deleted file mode 100644 index 3d15721f..00000000 --- a/src/errors.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Errors that may occur when working with 2D coordinates - -use derive_more::{Display, Error}; - -/// The supplied vector-like struct was too close to zero to be converted into a rotation-like type -/// -/// This error is produced when attempting to convert into a rotation-like type -/// such as a [`Rotation`](crate::orientation::Rotation) or [`Quat`](bevy::math::Quat) from a vector-like type -/// such as a [`Vec2`](bevy::math::Vec2). -/// -/// In almost all cases, the correct way to handle this error is to simply not change the rotation. -#[derive(Debug, Clone, Copy, Error, Display, PartialEq, Eq)] -pub struct NearlySingularConversion; diff --git a/src/input_mocking.rs b/src/input_mocking.rs index 86fe942b..8eb7beab 100644 --- a/src/input_mocking.rs +++ b/src/input_mocking.rs @@ -190,12 +190,13 @@ pub trait MockInput { /// use bevy::prelude::*; /// use bevy::input::InputPlugin; /// use leafwing_input_manager::input_mocking::QueryInput; +/// use leafwing_input_manager::plugin::AccumulatorPlugin; /// use leafwing_input_manager::prelude::*; /// /// let mut app = App::new(); /// /// // This functionality requires Bevy's InputPlugin (included with DefaultPlugins) -/// app.add_plugins(InputPlugin); +/// app.add_plugins((InputPlugin, AccumulatorPlugin)); /// /// // Check if a key is currently pressed down. /// let pressed = app.pressed(KeyCode::KeyB); @@ -388,7 +389,7 @@ impl MockInput for MutableInputStreams<'_> { *self.gamepad_axes = Default::default(); *self.keycodes = Default::default(); *self.mouse_buttons = Default::default(); - *self.mouse_wheel = Default::default(); + *self.mouse_scroll = Default::default(); *self.mouse_motion = Default::default(); } } @@ -412,17 +413,20 @@ impl MutableInputStreams<'_> { } fn send_mouse_scroll(&mut self, delta: Vec2) { - self.mouse_wheel.send(MouseWheel { + let event = MouseWheel { + unit: MouseScrollUnit::Pixel, x: delta.x, y: delta.y, - // FIXME: MouseScrollUnit is not recorded and is always assumed to be Pixel - unit: MouseScrollUnit::Pixel, window: Entity::PLACEHOLDER, - }); + }; + + self.mouse_scroll_events.send(event); } fn send_mouse_move(&mut self, delta: Vec2) { - self.mouse_motion.send(MouseMotion { delta }); + let event = MouseMotion { delta }; + + self.mouse_motion_events.send(event); } fn send_gamepad_button_state( @@ -686,6 +690,7 @@ impl MockUIInteraction for App { #[cfg(test)] mod test { use crate::input_mocking::{MockInput, QueryInput}; + use crate::plugin::AccumulatorPlugin; use crate::user_input::*; use bevy::input::gamepad::{ GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadInfo, @@ -695,7 +700,7 @@ mod test { fn test_app() -> App { let mut app = App::new(); - app.add_plugins(InputPlugin); + app.add_plugins(InputPlugin).add_plugins(AccumulatorPlugin); let gamepad = Gamepad::new(0); let mut gamepad_events = app.world_mut().resource_mut::>(); @@ -786,7 +791,6 @@ mod test { } #[test] - #[ignore = "Mouse axis input clearing is buggy. Try again after https://github.com/bevyengine/bevy/pull/13762 is released."] fn mouse_inputs() { let mut app = test_app(); diff --git a/src/input_streams.rs b/src/input_streams.rs index 0d6536ea..7500a2ed 100644 --- a/src/input_streams.rs +++ b/src/input_streams.rs @@ -1,14 +1,17 @@ //! Unified input streams for working with [`bevy::input`] data. -use bevy::ecs::prelude::{Event, Events, ResMut, World}; +use bevy::ecs::prelude::{Events, ResMut, World}; use bevy::ecs::system::SystemState; +use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::input::{ gamepad::{Gamepad, GamepadAxis, GamepadButton, GamepadEvent, Gamepads}, keyboard::{KeyCode, KeyboardInput}, - mouse::{MouseButton, MouseButtonInput, MouseMotion, MouseWheel}, + mouse::{MouseButton, MouseButtonInput}, Axis, ButtonInput, }; +use crate::user_input::{AccumulatedMouseMovement, AccumulatedMouseScroll}; + /// A collection of [`ButtonInput`] structs, which can be used to update an [`InputMap`](crate::input_map::InputMap). /// /// These are typically collected via a system from the [`World`] as resources. @@ -26,10 +29,10 @@ pub struct InputStreams<'a> { pub keycodes: Option<&'a ButtonInput>, /// A [`MouseButton`] [`Input`](ButtonInput) stream pub mouse_buttons: Option<&'a ButtonInput>, - /// A [`MouseWheel`] event stream - pub mouse_wheel: Option>, - /// A [`MouseMotion`] event stream - pub mouse_motion: Vec, + /// The [`AccumulatedMouseScroll`] for the frame + pub mouse_scroll: &'a AccumulatedMouseScroll, + /// The [`AccumulatedMouseMovement`] for the frame + pub mouse_motion: &'a AccumulatedMouseMovement, /// The [`Gamepad`] that this struct will detect inputs from pub associated_gamepad: Option, } @@ -44,11 +47,8 @@ impl<'a> InputStreams<'a> { let gamepads = world.resource::(); let keycodes = world.get_resource::>(); let mouse_buttons = world.get_resource::>(); - let mouse_wheel = world.resource::>(); - let mouse_motion = world.resource::>(); - - let mouse_wheel: Vec = collect_events_cloned(mouse_wheel); - let mouse_motion: Vec = collect_events_cloned(mouse_motion); + let mouse_wheel = world.resource::(); + let mouse_motion = world.resource::(); InputStreams { gamepad_buttons, @@ -57,19 +57,13 @@ impl<'a> InputStreams<'a> { gamepads, keycodes, mouse_buttons, - mouse_wheel: Some(mouse_wheel), + mouse_scroll: mouse_wheel, mouse_motion, associated_gamepad: gamepad, } } } -// Clones and collects the received events into a `Vec`. -#[inline] -fn collect_events_cloned(events: &Events) -> Vec { - events.get_reader().read(events).cloned().collect() -} - /// A mutable collection of [`ButtonInput`] structs, which can be used for mocking user inputs. /// /// These are typically collected via a system from the [`World`] as resources. @@ -96,10 +90,14 @@ pub struct MutableInputStreams<'a> { pub mouse_buttons: &'a mut ButtonInput, /// Events used for mocking [`MouseButton`] inputs pub mouse_button_events: &'a mut Events, - /// A [`MouseWheel`] event stream - pub mouse_wheel: &'a mut Events, - /// A [`MouseMotion`] event stream - pub mouse_motion: &'a mut Events, + /// The [`AccumulatedMouseScroll`] for the frame + pub mouse_scroll: &'a mut AccumulatedMouseScroll, + /// The [`AccumulatedMouseMovement`] for the frame + pub mouse_motion: &'a mut AccumulatedMouseMovement, + /// Mouse scroll events used for mocking mouse scroll inputs + pub mouse_scroll_events: &'a mut Events, + /// Mouse motion events used for mocking mouse motion inputs + pub mouse_motion_events: &'a mut Events, /// The [`Gamepad`] that this struct will detect inputs from pub associated_gamepad: Option, @@ -108,6 +106,16 @@ pub struct MutableInputStreams<'a> { impl<'a> MutableInputStreams<'a> { /// Construct a [`MutableInputStreams`] from the [`World`] pub fn from_world(world: &'a mut World, gamepad: Option) -> Self { + // Initialize accumulated mouse movement and scroll resources if they don't exist + // These are special-cased because they are not initialized by the InputPlugin + if !world.contains_resource::() { + world.init_resource::(); + } + + if !world.contains_resource::() { + world.init_resource::(); + } + let mut input_system_state: SystemState<( ResMut>, ResMut>, @@ -118,6 +126,8 @@ impl<'a> MutableInputStreams<'a> { ResMut>, ResMut>, ResMut>, + ResMut, + ResMut, ResMut>, ResMut>, )> = SystemState::new(world); @@ -132,8 +142,10 @@ impl<'a> MutableInputStreams<'a> { keyboard_events, mouse_buttons, mouse_button_events, - mouse_wheel, + mouse_scroll, mouse_motion, + mouse_scroll_events, + mouse_motion_events, ) = input_system_state.get_mut(world); MutableInputStreams { @@ -146,8 +158,10 @@ impl<'a> MutableInputStreams<'a> { keyboard_events: keyboard_events.into_inner(), mouse_buttons: mouse_buttons.into_inner(), mouse_button_events: mouse_button_events.into_inner(), - mouse_wheel: mouse_wheel.into_inner(), + mouse_scroll: mouse_scroll.into_inner(), mouse_motion: mouse_motion.into_inner(), + mouse_scroll_events: mouse_scroll_events.into_inner(), + mouse_motion_events: mouse_motion_events.into_inner(), associated_gamepad: gamepad, } } @@ -171,8 +185,8 @@ impl<'a> From> for InputStreams<'a> { gamepads: mutable_streams.gamepads, keycodes: Some(mutable_streams.keycodes), mouse_buttons: Some(mutable_streams.mouse_buttons), - mouse_wheel: Some(collect_events_cloned(mutable_streams.mouse_wheel)), - mouse_motion: collect_events_cloned(mutable_streams.mouse_motion), + mouse_scroll: mutable_streams.mouse_scroll, + mouse_motion: mutable_streams.mouse_motion, associated_gamepad: mutable_streams.associated_gamepad, } } @@ -187,8 +201,8 @@ impl<'a> From<&'a MutableInputStreams<'a>> for InputStreams<'a> { gamepads: mutable_streams.gamepads, keycodes: Some(mutable_streams.keycodes), mouse_buttons: Some(mutable_streams.mouse_buttons), - mouse_wheel: Some(collect_events_cloned(mutable_streams.mouse_wheel)), - mouse_motion: collect_events_cloned(mutable_streams.mouse_motion), + mouse_scroll: mutable_streams.mouse_scroll, + mouse_motion: mutable_streams.mouse_motion, associated_gamepad: mutable_streams.associated_gamepad, } } diff --git a/src/lib.rs b/src/lib.rs index 453ddd0a..490dfb84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,6 @@ pub mod axislike; pub mod buttonlike; pub mod clashing_inputs; pub mod common_conditions; -pub mod errors; pub mod input_map; pub mod input_mocking; pub mod input_processing; diff --git a/src/plugin.rs b/src/plugin.rs index 1a43a943..e719e8ac 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -17,6 +17,7 @@ use crate::action_state::{ActionData, ActionState}; use crate::clashing_inputs::ClashStrategy; use crate::input_map::InputMap; use crate::input_processing::*; +use crate::systems::{accumulate_mouse_movement, accumulate_mouse_scroll}; #[cfg(feature = "timing")] use crate::timing::Timing; use crate::user_input::*; @@ -95,6 +96,11 @@ impl Plugin match self.machine { Machine::Client => { + // TODO: this should be part of `bevy_input` + if !app.is_plugin_added::() { + app.add_plugins(AccumulatorPlugin); + } + // Main schedule app.add_systems( PreUpdate, @@ -166,7 +172,9 @@ impl Plugin } }; - app.register_type::>() + app.register_type::() + .register_type::() + .register_type::>() .register_type::>() .register_type::() .register_type::>() @@ -218,6 +226,8 @@ pub enum InputManagerSystem { /// /// Cleans up the state of the input manager, clearing `just_pressed` and `just_released` Tick, + /// Accumulates various input event streams into a total delta for the frame. + Accumulate, /// Collects input data to update the [`ActionState`] Update, /// Manually control the [`ActionState`] @@ -225,3 +235,32 @@ pub enum InputManagerSystem { /// Must run after [`InputManagerSystem::Update`] or the action state will be overridden ManualControl, } + +/// A plugin to handle accumulating mouse movement and scroll events. +/// +/// This is a clearer, more reliable and more efficient approach to computing the total mouse movement and scroll for the frame. +/// +/// This plugin is public to allow it to be used in tests: users should always have this plugin implicitly added by [`InputManagerPlugin`]. +/// Ultimately, this should be included as part of [`InputPlugin`](bevy::input::InputPlugin): see [bevy#13915](https://github.com/bevyengine/bevy/issues/13915). +pub struct AccumulatorPlugin; + +impl Plugin for AccumulatorPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + app.init_resource::(); + + // TODO: these should be part of bevy_input + app.add_systems( + PreUpdate, + (accumulate_mouse_movement, accumulate_mouse_scroll) + .in_set(InputManagerSystem::Accumulate), + ); + + app.configure_sets( + PreUpdate, + InputManagerSystem::Accumulate + .after(InputSystem) + .before(InputManagerSystem::Update), + ); + } +} diff --git a/src/systems.rs b/src/systems.rs index e557e29d..097e52c8 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,5 +1,6 @@ //! The systems that power each [`InputManagerPlugin`](crate::plugin::InputManagerPlugin). +use crate::user_input::{AccumulatedMouseMovement, AccumulatedMouseScroll}; use crate::{ action_state::ActionState, clashing_inputs::ClashStrategy, input_map::InputMap, input_streams::InputStreams, Actionlike, @@ -88,6 +89,30 @@ pub fn tick_action_state( *stored_previous_instant = time.last_update(); } +/// Sums the[`MouseMotion`] events received since during this frame. +pub fn accumulate_mouse_movement( + mut mouse_motion: ResMut, + mut events: EventReader, +) { + mouse_motion.reset(); + + for event in events.read() { + mouse_motion.accumulate(event); + } +} + +/// Sums the [`MouseWheel`] events received since during this frame. +pub fn accumulate_mouse_scroll( + mut mouse_scroll: ResMut, + mut events: EventReader, +) { + mouse_scroll.reset(); + + for event in events.read() { + mouse_scroll.accumulate(event); + } +} + /// Fetches all the relevant [`ButtonInput`] resources /// to update [`ActionState`] according to the [`InputMap`]. /// @@ -100,8 +125,8 @@ pub fn update_action_state( gamepads: Res, keycodes: Option>>, mouse_buttons: Option>>, - mut mouse_wheel: EventReader, - mut mouse_motion: EventReader, + mouse_scroll: Res, + mouse_motion: Res, clash_strategy: Res, #[cfg(feature = "ui")] interactions: Query<&Interaction>, #[cfg(feature = "egui")] mut maybe_egui: Query<(Entity, &'static mut EguiContext)>, @@ -115,19 +140,18 @@ pub fn update_action_state( let gamepads = gamepads.into_inner(); let keycodes = keycodes.map(|keycodes| keycodes.into_inner()); let mouse_buttons = mouse_buttons.map(|mouse_buttons| mouse_buttons.into_inner()); - - let mouse_wheel: Option> = Some(mouse_wheel.read().cloned().collect()); - let mouse_motion: Vec = mouse_motion.read().cloned().collect(); + let mouse_scroll = mouse_scroll.into_inner(); + let mouse_motion = mouse_motion.into_inner(); // If the user clicks on a button, do not apply it to the game state #[cfg(feature = "ui")] - let (mouse_buttons, mouse_wheel) = if interactions + let mouse_buttons = if interactions .iter() .any(|&interaction| interaction != Interaction::None) { - (None, None) + None } else { - (mouse_buttons, mouse_wheel) + mouse_buttons }; // If egui wants to own inputs, don't also apply them to the game state @@ -141,12 +165,12 @@ pub fn update_action_state( // `wants_pointer_input` sometimes returns `false` after clicking or holding a button over a widget, // so `is_pointer_over_area` is also needed. #[cfg(feature = "egui")] - let (mouse_buttons, mouse_wheel) = if maybe_egui.iter_mut().any(|(_, mut ctx)| { + let mouse_buttons = if maybe_egui.iter_mut().any(|(_, mut ctx)| { ctx.get_mut().is_pointer_over_area() || ctx.get_mut().wants_pointer_input() }) { - (None, None) + None } else { - (mouse_buttons, mouse_wheel) + mouse_buttons }; let resources = input_map @@ -161,8 +185,8 @@ pub fn update_action_state( gamepads, keycodes, mouse_buttons, - mouse_wheel: mouse_wheel.clone(), - mouse_motion: mouse_motion.clone(), + mouse_scroll, + mouse_motion, associated_gamepad: input_map.gamepad(), }; diff --git a/src/user_input/chord.rs b/src/user_input/chord.rs index 353d9f98..685d059e 100644 --- a/src/user_input/chord.rs +++ b/src/user_input/chord.rs @@ -34,13 +34,14 @@ use crate::user_input::{DualAxisData, InputControlKind, UserInput}; /// - Nesting: Using an input chord within another multi-input element treats it as a single button, /// ignoring its individual functionalities (like single-axis values). /// -/// ```rust, ignore +/// ```rust /// use bevy::prelude::*; /// use bevy::input::InputPlugin; +/// use leafwing_input_manager::plugin::AccumulatorPlugin; /// use leafwing_input_manager::prelude::*; /// /// let mut app = App::new(); -/// app.add_plugins(InputPlugin); +/// app.add_plugins((InputPlugin, AccumulatorPlugin)); /// /// // Define a chord using A and B keys /// let input = InputChord::new([KeyCode::KeyA, KeyCode::KeyB]); @@ -239,11 +240,14 @@ mod tests { use bevy::prelude::*; use super::*; + use crate::plugin::AccumulatorPlugin; use crate::prelude::*; fn test_app() -> App { let mut app = App::new(); - app.add_plugins(MinimalPlugins).add_plugins(InputPlugin); + app.add_plugins(MinimalPlugins) + .add_plugins(InputPlugin) + .add_plugins(AccumulatorPlugin); // WARNING: you MUST register your gamepad during tests, // or all gamepad input mocking actions will fail @@ -346,7 +350,6 @@ mod tests { } #[test] - #[ignore = "https://github.com/Leafwing-Studios/leafwing-input-manager/issues/538"] fn test_chord_with_buttons_and_axes() { let chord = InputChord::new([KeyCode::KeyA, KeyCode::KeyB]) .with(MouseScrollAxis::X) diff --git a/src/user_input/gamepad.rs b/src/user_input/gamepad.rs index b5c77eda..e67482bd 100644 --- a/src/user_input/gamepad.rs +++ b/src/user_input/gamepad.rs @@ -931,6 +931,7 @@ impl WithDualAxisProcessingPipelineExt for GamepadVirtualDPad { mod tests { use super::*; use crate::input_mocking::MockInput; + use crate::plugin::AccumulatorPlugin; use bevy::input::gamepad::{ GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadInfo, }; @@ -939,7 +940,9 @@ mod tests { fn test_app() -> App { let mut app = App::new(); - app.add_plugins(MinimalPlugins).add_plugins(InputPlugin); + app.add_plugins(MinimalPlugins) + .add_plugins(InputPlugin) + .add_plugins(AccumulatorPlugin); // WARNING: you MUST register your gamepad during tests, // or all gamepad input mocking actions will fail diff --git a/src/user_input/keyboard.rs b/src/user_input/keyboard.rs index 0f193048..62ed2252 100644 --- a/src/user_input/keyboard.rs +++ b/src/user_input/keyboard.rs @@ -571,13 +571,14 @@ impl WithDualAxisProcessingPipelineExt for KeyboardVirtualDPad { mod tests { use super::*; use crate::input_mocking::MockInput; + use crate::plugin::AccumulatorPlugin; use crate::raw_inputs::RawInputs; use bevy::input::InputPlugin; use bevy::prelude::*; fn test_app() -> App { let mut app = App::new(); - app.add_plugins(InputPlugin); + app.add_plugins(InputPlugin).add_plugins(AccumulatorPlugin); app } diff --git a/src/user_input/mouse.rs b/src/user_input/mouse.rs index 5a7ae33a..c7d8abac 100644 --- a/src/user_input/mouse.rs +++ b/src/user_input/mouse.rs @@ -1,6 +1,7 @@ //! Mouse inputs -use bevy::prelude::{MouseButton, Reflect, Vec2}; +use bevy::input::mouse::{MouseMotion, MouseWheel}; +use bevy::prelude::{MouseButton, Reflect, Resource, Vec2}; use leafwing_input_manager_macros::serde_typetag; use serde::{Deserialize, Serialize}; @@ -61,20 +62,6 @@ impl UserInput for MouseButton { } } -/// Retrieves the total mouse displacement. -#[must_use] -#[inline] -fn accumulate_mouse_movement(input_streams: &InputStreams) -> Vec2 { - // PERF: this summing is computed for every individual input - // This should probably be computed once, and then cached / read - // Fix upstream! - input_streams - .mouse_motion - .iter() - .map(|event| event.delta) - .sum() -} - /// Provides button-like behavior for mouse movement in cardinal directions. /// /// # Behaviors @@ -84,13 +71,14 @@ fn accumulate_mouse_movement(input_streams: &InputStreams) -> Vec2 { /// - `1.0`: The input is currently active. /// - `0.0`: The input is inactive. /// -/// ```rust, ignore +/// ```rust /// use bevy::prelude::*; /// use bevy::input::InputPlugin; +/// use leafwing_input_manager::plugin::AccumulatorPlugin; /// use leafwing_input_manager::prelude::*; /// /// let mut app = App::new(); -/// app.add_plugins(InputPlugin); +/// app.add_plugins((InputPlugin, AccumulatorPlugin)); /// /// // Positive Y-axis movement /// let input = MouseMoveDirection::UP; @@ -135,8 +123,8 @@ impl UserInput for MouseMoveDirection { #[must_use] #[inline] fn pressed(&self, input_streams: &InputStreams) -> bool { - let movement = accumulate_mouse_movement(input_streams); - self.0.is_active(movement) + let mouse_movement = input_streams.mouse_motion.0; + self.0.is_active(mouse_movement) } /// Retrieves the amount of the mouse movement along the specified direction, @@ -179,10 +167,11 @@ impl UserInput for MouseMoveDirection { /// ```rust /// use bevy::prelude::*; /// use bevy::input::InputPlugin; +/// use leafwing_input_manager::plugin::AccumulatorPlugin; /// use leafwing_input_manager::prelude::*; /// /// let mut app = App::new(); -/// app.add_plugins(InputPlugin); +/// app.add_plugins((InputPlugin, AccumulatorPlugin)); /// /// // Y-axis movement /// let input = MouseMoveAxis::Y; @@ -240,7 +229,7 @@ impl UserInput for MouseMoveAxis { #[must_use] #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { - let movement = accumulate_mouse_movement(input_streams); + let movement = input_streams.mouse_motion.0; let value = self.axis.get_value(movement); self.processors .iter() @@ -306,10 +295,11 @@ impl WithAxisProcessingPipelineExt for MouseMoveAxis { /// ```rust /// use bevy::prelude::*; /// use bevy::input::InputPlugin; +/// use leafwing_input_manager::plugin::AccumulatorPlugin; /// use leafwing_input_manager::prelude::*; /// /// let mut app = App::new(); -/// app.add_plugins(InputPlugin); +/// app.add_plugins((InputPlugin, AccumulatorPlugin)); /// /// let input = MouseMove::default(); /// @@ -334,7 +324,7 @@ impl MouseMove { #[must_use] #[inline] fn processed_value(&self, input_streams: &InputStreams) -> Vec2 { - let movement = accumulate_mouse_movement(input_streams); + let movement = input_streams.mouse_motion.0; self.processors .iter() .fold(movement, |value, processor| processor.process(value)) @@ -411,20 +401,6 @@ impl WithDualAxisProcessingPipelineExt for MouseMove { } } -/// Accumulates the mouse wheel movement. -#[must_use] -#[inline] -fn accumulate_wheel_movement(input_streams: &InputStreams) -> Vec2 { - let Some(wheel) = &input_streams.mouse_wheel else { - return Vec2::ZERO; - }; - - // PERF: this summing is computed for every individual input - // This should probably be computed once, and then cached / read - // Fix upstream! - wheel.iter().map(|event| Vec2::new(event.x, event.y)).sum() -} - /// Provides button-like behavior for mouse wheel scrolling in cardinal directions. /// /// # Behaviors @@ -434,13 +410,14 @@ fn accumulate_wheel_movement(input_streams: &InputStreams) -> Vec2 { /// - `1.0`: The input is currently active. /// - `0.0`: The input is inactive. /// -/// ```rust, ignore +/// ```rust /// use bevy::prelude::*; /// use bevy::input::InputPlugin; +/// use leafwing_input_manager::plugin::AccumulatorPlugin; /// use leafwing_input_manager::prelude::*; /// /// let mut app = App::new(); -/// app.add_plugins(InputPlugin); +/// app.add_plugins((InputPlugin, AccumulatorPlugin)); /// /// // Positive Y-axis scrolling /// let input = MouseScrollDirection::UP; @@ -485,7 +462,7 @@ impl UserInput for MouseScrollDirection { #[must_use] #[inline] fn pressed(&self, input_streams: &InputStreams) -> bool { - let movement = accumulate_wheel_movement(input_streams); + let movement = input_streams.mouse_scroll.0; self.0.is_active(movement) } @@ -529,10 +506,11 @@ impl UserInput for MouseScrollDirection { /// ```rust /// use bevy::prelude::*; /// use bevy::input::InputPlugin; +/// use leafwing_input_manager::plugin::AccumulatorPlugin; /// use leafwing_input_manager::prelude::*; /// /// let mut app = App::new(); -/// app.add_plugins(InputPlugin); +/// app.add_plugins((InputPlugin, AccumulatorPlugin)); /// /// // Y-axis movement /// let input = MouseScrollAxis::Y; @@ -590,7 +568,7 @@ impl UserInput for MouseScrollAxis { #[must_use] #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { - let movement = accumulate_wheel_movement(input_streams); + let movement = input_streams.mouse_scroll.0; let value = self.axis.get_value(movement); self.processors .iter() @@ -655,10 +633,11 @@ impl WithAxisProcessingPipelineExt for MouseScrollAxis { /// ```rust /// use bevy::prelude::*; /// use bevy::input::InputPlugin; +/// use leafwing_input_manager::plugin::AccumulatorPlugin; /// use leafwing_input_manager::prelude::*; /// /// let mut app = App::new(); -/// app.add_plugins(InputPlugin); +/// app.add_plugins((InputPlugin, AccumulatorPlugin)); /// /// let input = MouseScroll::default(); /// @@ -683,7 +662,7 @@ impl MouseScroll { #[must_use] #[inline] fn processed_value(&self, input_streams: &InputStreams) -> Vec2 { - let movement = accumulate_wheel_movement(input_streams); + let movement = input_streams.mouse_scroll.0; self.processors .iter() .fold(movement, |value, processor| processor.process(value)) @@ -760,16 +739,69 @@ impl WithDualAxisProcessingPipelineExt for MouseScroll { } } +/// A resource that records the accumulated mouse movement for the frame. +/// +/// These values are computed by summing the [`MouseMotion`] events. +/// +/// This resource is automatically added by [`InputManagerPlugin`](crate::plugin::InputManagerPlugin). +/// Its value is updated during [`InputManagerSystem::Update`](crate::plugin::InputManagerSystem::Update). +#[derive(Debug, Default, Resource, Reflect, Serialize, Deserialize, Clone, PartialEq)] +pub struct AccumulatedMouseMovement(pub Vec2); + +impl AccumulatedMouseMovement { + /// Resets the accumulated mouse movement to zero. + #[inline] + pub fn reset(&mut self) { + self.0 = Vec2::ZERO; + } + + /// Accumulates the specified mouse movement. + #[inline] + pub fn accumulate(&mut self, event: &MouseMotion) { + self.0 += event.delta; + } +} + +/// A resource that records the accumulated mouse wheel (scrolling) movement for the frame. +/// +/// These values are computed by summing the [`MouseWheel`] events. +/// +/// This resource is automatically added by [`InputManagerPlugin`](crate::plugin::InputManagerPlugin). +/// Its value is updated during [`InputManagerSystem::Update`](crate::plugin::InputManagerSystem::Update). +#[derive(Debug, Default, Resource, Reflect, Serialize, Deserialize, Clone, PartialEq)] +pub struct AccumulatedMouseScroll(pub Vec2); + +impl AccumulatedMouseScroll { + /// Resets the accumulated mouse scroll to zero. + #[inline] + pub fn reset(&mut self) { + self.0 = Vec2::ZERO; + } + + /// Accumulates the specified mouse wheel movement. + /// + /// # Warning + /// + /// This ignores the mouse scroll unit: all values are treated as equal. + /// All scrolling, no matter what window it is on, is added to the same total. + #[inline] + pub fn accumulate(&mut self, event: &MouseWheel) { + self.0.x += event.x; + self.0.y += event.y; + } +} + #[cfg(test)] mod tests { use super::*; use crate::input_mocking::MockInput; + use crate::plugin::AccumulatorPlugin; use bevy::input::InputPlugin; use bevy::prelude::*; fn test_app() -> App { let mut app = App::new(); - app.add_plugins(InputPlugin); + app.add_plugins(InputPlugin).add_plugins(AccumulatorPlugin); app } @@ -986,4 +1018,64 @@ mod tests { check(&mouse_scroll_y, &inputs, true, data.y(), None); check(&mouse_scroll, &inputs, true, data.length(), Some(data)); } + + #[test] + fn one_frame_accumulate_mouse_movement() { + let mut app = test_app(); + app.send_axis_values(MouseMoveAxis::Y, [3.0]); + app.send_axis_values(MouseMoveAxis::Y, [2.0]); + + let mouse_motion_events = app.world().get_resource::>().unwrap(); + for event in mouse_motion_events.iter_current_update_events() { + dbg!("Event sent: {:?}", event); + } + + // The haven't been processed yet + let accumulated_mouse_movement = app.world().resource::(); + assert_eq!(accumulated_mouse_movement.0, Vec2::new(0.0, 0.0)); + + app.update(); + + // Now the events should be processed + let accumulated_mouse_movement = app.world().resource::(); + assert_eq!(accumulated_mouse_movement.0, Vec2::new(0.0, 5.0)); + + let inputs = InputStreams::from_world(app.world(), None); + assert_eq!(inputs.mouse_motion.0, Vec2::new(0.0, 5.0)); + } + + #[test] + fn multiple_frames_accumulate_mouse_movement() { + let mut app = test_app(); + let inputs = InputStreams::from_world(app.world(), None); + // Starts at 0 + assert_eq!( + inputs.mouse_motion.0, + Vec2::ZERO, + "Initial movement is not zero." + ); + + // Send some data + app.send_axis_values(MouseMoveAxis::Y, [3.0]); + // Wait for the events to be processed + app.update(); + + let inputs = InputStreams::from_world(app.world(), None); + // Data is read + assert_eq!( + inputs.mouse_motion.0, + Vec2::new(0.0, 3.0), + "Movement sent was not read correctly." + ); + + // Do nothing + app.update(); + let inputs = InputStreams::from_world(app.world(), None); + // Back to 0 for this frame + assert_eq!( + inputs.mouse_motion.0, + Vec2::ZERO, + "No movement was expected. Is the position in the event stream being cleared properly?" + ); + } }