From 04cb6682b434eb5385a3a0f90661a851300d0eb4 Mon Sep 17 00:00:00 2001 From: 100-TomatoJuice Date: Sun, 30 Jul 2023 10:22:19 -0700 Subject: [PATCH 01/12] Added DeadzoneShape --- src/axislike.rs | 44 ++++++++++++++++++++++++++---- src/input_map.rs | 2 +- src/input_streams.rs | 63 ++++++++++++++++++++++++++++--------------- tests/gamepad_axis.rs | 1 + tests/mouse_motion.rs | 1 + tests/mouse_wheel.rs | 1 + 6 files changed, 85 insertions(+), 27 deletions(-) diff --git a/src/axislike.rs b/src/axislike.rs index 4b07ab95..1e74bcfe 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -186,6 +186,8 @@ pub struct DualAxis { pub x: SingleAxis, /// The axis representing vertical movement. pub y: SingleAxis, + /// The shape of the deadzone + pub deadzone_shape: DeadzoneShape, } impl DualAxis { @@ -194,16 +196,23 @@ impl DualAxis { /// This cannot be changed, but the struct can be easily manually constructed. pub const DEFAULT_DEADZONE: f32 = 0.1; - /// Creates a [`DualAxis`] with both `positive_low` and `negative_low` in both axes set to `threshold`. + /// The default shape of the deadzone used by constructor methods. + /// + /// This cannot be changed, but the struct can be easily manually constructed. + pub const DEFAULT_DEADZONE_SHAPE: DeadzoneShape = DeadzoneShape::Cross; + + /// Creates a [`DualAxis`] with both `positive_low` and `negative_low` in both axes set to `threshold` with a `deadzone_shape`. #[must_use] pub fn symmetric( x_axis_type: impl Into, y_axis_type: impl Into, threshold: f32, + deadzone_shape: DeadzoneShape, ) -> DualAxis { DualAxis { x: SingleAxis::symmetric(x_axis_type, threshold), y: SingleAxis::symmetric(y_axis_type, threshold), + deadzone_shape, } } @@ -221,6 +230,7 @@ impl DualAxis { DualAxis { x: SingleAxis::from_value(x_axis_type, x_value), y: SingleAxis::from_value(y_axis_type, y_value), + deadzone_shape: Self::DEFAULT_DEADZONE_SHAPE, } } @@ -231,6 +241,7 @@ impl DualAxis { GamepadAxisType::LeftStickX, GamepadAxisType::LeftStickY, Self::DEFAULT_DEADZONE, + Self::DEFAULT_DEADZONE_SHAPE, ) } @@ -241,6 +252,7 @@ impl DualAxis { GamepadAxisType::RightStickX, GamepadAxisType::RightStickY, Self::DEFAULT_DEADZONE, + Self::DEFAULT_DEADZONE_SHAPE, ) } @@ -249,6 +261,7 @@ impl DualAxis { DualAxis { x: SingleAxis::mouse_wheel_x(), y: SingleAxis::mouse_wheel_y(), + deadzone_shape: Self::DEFAULT_DEADZONE_SHAPE, } } @@ -257,14 +270,16 @@ impl DualAxis { DualAxis { x: SingleAxis::mouse_motion_x(), y: SingleAxis::mouse_motion_y(), + deadzone_shape: Self::DEFAULT_DEADZONE_SHAPE, } } - /// Returns this [`DualAxis`] with the deadzone set to the specified value + /// Returns this [`DualAxis`] with the deadzone set to the specified values and shape #[must_use] - pub fn with_deadzone(mut self, deadzone: f32) -> DualAxis { - self.x = self.x.with_deadzone(deadzone); - self.y = self.y.with_deadzone(deadzone); + pub fn with_deadzone(mut self, deadzone_x: f32, deadzone_y: f32, deadzone: DeadzoneShape) -> DualAxis { + self.x = self.x.with_deadzone(deadzone_x); + self.y = self.y.with_deadzone(deadzone_y); + self.deadzone_shape = deadzone; self } @@ -688,3 +703,22 @@ impl From for Vec2 { data.xy } } + +/// The shape of the deadzone for a [`DualAxis`] input. +/// +/// Uses the x and y thresholds on [`DualAxis`] to set the shape diamensions +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum DeadzoneShape{ + /// Deadzone with a cross + /// + /// Uses the x threshold as the horizontal width and y threshold as the vertical width. + Cross, + /// Deadzone with a rectangle. + /// + /// Uses the x threshold as the width and y threshold as the length. + Rect, + /// Deadzone with a circle. + /// + /// Uses the larger of the x or y threshold as the radius + Circle, +} \ No newline at end of file diff --git a/src/input_map.rs b/src/input_map.rs index c505b6e0..862235ff 100644 --- a/src/input_map.rs +++ b/src/input_map.rs @@ -388,7 +388,7 @@ impl InputMap { if input_streams.input_pressed(input) { inputs.push(input.clone()); - action.value += input_streams.input_value(input); + action.value += input_streams.input_value(input, true); } } diff --git a/src/input_streams.rs b/src/input_streams.rs index e4d32a48..d4ccf8fb 100644 --- a/src/input_streams.rs +++ b/src/input_streams.rs @@ -13,7 +13,7 @@ use bevy::ecs::system::SystemState; use crate::axislike::{ AxisType, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, SingleAxis, VirtualAxis, - VirtualDPad, + VirtualDPad, DeadzoneShape, }; use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; use crate::prelude::DualAxis; @@ -133,7 +133,7 @@ impl<'a> InputStreams<'a> { || self.button_pressed(InputKind::SingleAxis(axis.y)) } InputKind::SingleAxis(axis) => { - let value = self.input_value(&UserInput::Single(button)); + let value = self.input_value(&UserInput::Single(button), true); value < axis.negative_low || value > axis.positive_low } @@ -247,7 +247,7 @@ impl<'a> InputStreams<'a> { /// /// If you need to ensure that this value is always in the range `[-1., 1.]`, /// be sure to clamp the returned data. - pub fn input_value(&self, input: &UserInput) -> f32 { + pub fn input_value(&self, input: &UserInput, include_deadzone: bool) -> f32 { let use_button_value = || -> f32 { if self.input_pressed(input) { 1.0 @@ -259,7 +259,7 @@ impl<'a> InputStreams<'a> { // Helper that takes the value returned by an axis and returns 0.0 if it is not within the // triggering range. let value_in_axis_range = |axis: &SingleAxis, value: f32| -> f32 { - if value >= axis.negative_low && value <= axis.positive_low { + if value >= axis.negative_low && value <= axis.positive_low && include_deadzone{ 0.0 } else if axis.inverted { -value @@ -317,8 +317,8 @@ impl<'a> InputStreams<'a> { } } UserInput::VirtualAxis(VirtualAxis { negative, positive }) => { - self.input_value(&UserInput::Single(*positive)).abs() - - self.input_value(&UserInput::Single(*negative)).abs() + self.input_value(&UserInput::Single(*positive), true).abs() + - self.input_value(&UserInput::Single(*negative), true).abs() } UserInput::Single(InputKind::DualAxis(_)) => { self.input_axis_pair(input).unwrap_or_default().length() @@ -375,10 +375,10 @@ impl<'a> InputStreams<'a> { left, right, }) => { - let x = self.input_value(&UserInput::Single(*right)).abs() - - self.input_value(&UserInput::Single(*left)).abs(); - let y = self.input_value(&UserInput::Single(*up)).abs() - - self.input_value(&UserInput::Single(*down)).abs(); + let x = self.input_value(&UserInput::Single(*right), true).abs() + - self.input_value(&UserInput::Single(*left), true).abs(); + let y = self.input_value(&UserInput::Single(*up), true).abs() + - self.input_value(&UserInput::Single(*down), true).abs(); Some(DualAxisData::new(x, y)) } _ => None, @@ -386,17 +386,38 @@ impl<'a> InputStreams<'a> { } fn extract_dual_axis_data(&self, dual_axis: &DualAxis) -> DualAxisData { - let x = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.x))); - let y = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.y))); - - if x > dual_axis.x.positive_low - || x < dual_axis.x.negative_low - || y > dual_axis.y.positive_low - || y < dual_axis.y.negative_low - { - DualAxisData::new(x, y) - } else { - DualAxisData::new(0.0, 0.0) + match dual_axis.deadzone_shape { + DeadzoneShape::Cross => { + let x = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.x)), true); + let y = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.y)), true); + + DualAxisData::new(x, y) + }, + DeadzoneShape::Rect => { + let x = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.x)), false); + let y = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.y)), false); + if x > dual_axis.x.positive_low + || x < dual_axis.x.negative_low + || y > dual_axis.y.positive_low + || y < dual_axis.y.negative_low + { + DualAxisData::new(x, y) + } else { + DualAxisData::new(0.0, 0.0) + } + }, + DeadzoneShape::Circle => { + let x = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.x)), false); + let y = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.y)), false); + let radius = dual_axis.x.positive_low.max(dual_axis.y.positive_low); + + if (x.powi(2) + y.powi(2)).sqrt() >= radius + { + DualAxisData::new(x, y) + } else { + DualAxisData::new(0.0, 0.0) + } + }, } } } diff --git a/tests/gamepad_axis.rs b/tests/gamepad_axis.rs index 666197d3..add7a24f 100644 --- a/tests/gamepad_axis.rs +++ b/tests/gamepad_axis.rs @@ -108,6 +108,7 @@ fn game_pad_dual_axis_mocking() { negative_low: 0.0, inverted: false, }, + deadzone_shape: DualAxis::DEFAULT_DEADZONE_SHAPE, }; app.send_input(input); let mut events = app.world.resource_mut::>(); diff --git a/tests/mouse_motion.rs b/tests/mouse_motion.rs index 82b2c798..6908e2de 100644 --- a/tests/mouse_motion.rs +++ b/tests/mouse_motion.rs @@ -103,6 +103,7 @@ fn mouse_motion_dual_axis_mocking() { negative_low: 0.0, inverted: false, }, + deadzone_shape: DualAxis::DEFAULT_DEADZONE_SHAPE, }; app.send_input(input); let mut events = app.world.resource_mut::>(); diff --git a/tests/mouse_wheel.rs b/tests/mouse_wheel.rs index a63c45f8..3ef6f89c 100644 --- a/tests/mouse_wheel.rs +++ b/tests/mouse_wheel.rs @@ -104,6 +104,7 @@ fn mouse_wheel_dual_axis_mocking() { negative_low: 0.0, inverted: false, }, + deadzone_shape: DualAxis::DEFAULT_DEADZONE_SHAPE, }; app.send_input(input); let mut events = app.world.resource_mut::>(); From df53db5ace1705ccaf2e13ed52ce413dfa89207e Mon Sep 17 00:00:00 2001 From: 100-TomatoJuice Date: Sun, 30 Jul 2023 14:28:38 -0700 Subject: [PATCH 02/12] Moved DeadZoneShape processing to an impl --- src/axislike.rs | 121 ++++- src/input_streams.rs | 1152 ++++++++++++++++++++---------------------- 2 files changed, 663 insertions(+), 610 deletions(-) diff --git a/src/axislike.rs b/src/axislike.rs index 1e74bcfe..615e108e 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -1,9 +1,10 @@ //! Tools for working with directional axis-like user inputs (gamesticks, D-Pads and emulated equivalents) use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; +use crate::input_streams::InputStreams; use crate::orientation::{Direction, Rotation}; use crate::prelude::QwertyScanCode; -use crate::user_input::InputKind; +use crate::user_input::{InputKind, UserInput}; use bevy::input::{ gamepad::{GamepadAxisType, GamepadButtonType}, keyboard::KeyCode, @@ -187,7 +188,7 @@ pub struct DualAxis { /// The axis representing vertical movement. pub y: SingleAxis, /// The shape of the deadzone - pub deadzone_shape: DeadzoneShape, + pub deadzone_shape: DeadZoneShape, } impl DualAxis { @@ -197,9 +198,12 @@ impl DualAxis { pub const DEFAULT_DEADZONE: f32 = 0.1; /// The default shape of the deadzone used by constructor methods. - /// - /// This cannot be changed, but the struct can be easily manually constructed. - pub const DEFAULT_DEADZONE_SHAPE: DeadzoneShape = DeadzoneShape::Cross; + /// + /// This cannot be changed, but the struct can be easily manually constructed. + pub const DEFAULT_DEADZONE_SHAPE: DeadZoneShape = DeadZoneShape::Cross { + horizontal_width: Self::DEFAULT_DEADZONE, + vertical_width: Self::DEFAULT_DEADZONE, + }; /// Creates a [`DualAxis`] with both `positive_low` and `negative_low` in both axes set to `threshold` with a `deadzone_shape`. #[must_use] @@ -207,7 +211,7 @@ impl DualAxis { x_axis_type: impl Into, y_axis_type: impl Into, threshold: f32, - deadzone_shape: DeadzoneShape, + deadzone_shape: DeadZoneShape, ) -> DualAxis { DualAxis { x: SingleAxis::symmetric(x_axis_type, threshold), @@ -276,9 +280,7 @@ impl DualAxis { /// Returns this [`DualAxis`] with the deadzone set to the specified values and shape #[must_use] - pub fn with_deadzone(mut self, deadzone_x: f32, deadzone_y: f32, deadzone: DeadzoneShape) -> DualAxis { - self.x = self.x.with_deadzone(deadzone_x); - self.y = self.y.with_deadzone(deadzone_y); + pub fn with_deadzone(mut self, deadzone: DeadZoneShape) -> DualAxis { self.deadzone_shape = deadzone; self } @@ -705,20 +707,103 @@ impl From for Vec2 { } /// The shape of the deadzone for a [`DualAxis`] input. -/// +/// /// Uses the x and y thresholds on [`DualAxis`] to set the shape diamensions -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub enum DeadzoneShape{ +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +pub enum DeadZoneShape { /// Deadzone with a cross - /// + /// /// Uses the x threshold as the horizontal width and y threshold as the vertical width. - Cross, + Cross { + /// The width of the horizontal rectangle. + horizontal_width: f32, + /// The width of the vertical rectangle. + vertical_width: f32, + }, /// Deadzone with a rectangle. - /// + /// /// Uses the x threshold as the width and y threshold as the length. - Rect, + Rect { + /// The width of the rectangle. + width: f32, + /// The height of the rectangle. + height: f32, + }, /// Deadzone with a circle. - /// + /// /// Uses the larger of the x or y threshold as the radius - Circle, + Ellipse { + /// The horizontal radius + radius_x: f32, + /// The vertical radius + radius_y: f32, + }, +} + +impl Eq for DeadZoneShape {} +impl std::hash::Hash for DeadZoneShape { + fn hash(&self, state: &mut H) {} +} + +impl DeadZoneShape { + /// + pub fn get_dual_axis_data( + &self, + dual_axis: &DualAxis, + input_stream: &InputStreams, + ) -> DualAxisData { + match self { + DeadZoneShape::Cross { + horizontal_width, + vertical_width, + } => { + let x = input_stream + .input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.x)), true); + let y = input_stream + .input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.y)), true); + + DualAxisData::new(x, y) + } + DeadZoneShape::Rect { width, height } => { + let x = input_stream.input_value( + &UserInput::Single(InputKind::SingleAxis(dual_axis.x)), + false, + ); + let y = input_stream.input_value( + &UserInput::Single(InputKind::SingleAxis(dual_axis.y)), + false, + ); + + if DeadZoneShape::outside_rectangle(x, y, *width, *height) { + DualAxisData::new(x, y) + } else { + DualAxisData::new(0.0, 0.0) + } + } + DeadZoneShape::Ellipse { radius_x, radius_y } => { + let x = input_stream.input_value( + &UserInput::Single(InputKind::SingleAxis(dual_axis.x)), + false, + ); + let y = input_stream.input_value( + &UserInput::Single(InputKind::SingleAxis(dual_axis.y)), + false, + ); + + if DeadZoneShape::outside_ellipse(x, y, *radius_x, *radius_y) { + DualAxisData::new(x, y) + } else { + DualAxisData::new(0.0, 0.0) + } + } + } + } + + fn outside_ellipse(x: f32, y: f32, radius_x: f32, radius_y: f32) -> bool { + (x.powi(2) / radius_x.powi(2) + y.powi(2) / radius_y.powi(2)) >= 1.0 + } + + fn outside_rectangle(x: f32, y: f32, width: f32, height: f32) -> bool { + x > width || x < -width || y > height || y < -height + } } \ No newline at end of file diff --git a/src/input_streams.rs b/src/input_streams.rs index d4ccf8fb..5b6cadb5 100644 --- a/src/input_streams.rs +++ b/src/input_streams.rs @@ -1,592 +1,560 @@ -//! Unified input streams for working with [`bevy::input`] data. - -use bevy::input::{ - gamepad::{Gamepad, GamepadAxis, GamepadButton, GamepadEvent, Gamepads}, - keyboard::{KeyCode, KeyboardInput, ScanCode}, - mouse::{MouseButton, MouseButtonInput, MouseMotion, MouseWheel}, - Axis, Input, -}; -use petitset::PetitSet; - -use bevy::ecs::prelude::{Events, ResMut, World}; -use bevy::ecs::system::SystemState; - -use crate::axislike::{ - AxisType, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, SingleAxis, VirtualAxis, - VirtualDPad, DeadzoneShape, -}; -use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; -use crate::prelude::DualAxis; -use crate::user_input::{InputKind, UserInput}; - -/// A collection of [`Input`] structs, which can be used to update an [`InputMap`](crate::input_map::InputMap). -/// -/// These are typically collected via a system from the [`World`](bevy::prelude::World) as resources. -#[derive(Debug, Clone)] -pub struct InputStreams<'a> { - /// A [`GamepadButton`] [`Input`] stream - pub gamepad_buttons: &'a Input, - /// A [`GamepadButton`] [`Axis`] stream - pub gamepad_button_axes: &'a Axis, - /// A [`GamepadAxis`] [`Axis`] stream - pub gamepad_axes: &'a Axis, - /// A list of registered gamepads - pub gamepads: &'a Gamepads, - /// A [`KeyCode`] [`Input`] stream - pub keycodes: Option<&'a Input>, - /// A [`ScanCode`] [`Input`] stream - pub scan_codes: Option<&'a Input>, - /// A [`MouseButton`] [`Input`] stream - pub mouse_buttons: Option<&'a Input>, - /// A [`MouseWheel`] event stream - pub mouse_wheel: Option<&'a Events>, - /// A [`MouseMotion`] event stream - pub mouse_motion: &'a Events, - /// The [`Gamepad`] that this struct will detect inputs from - pub associated_gamepad: Option, -} - -// Constructors -impl<'a> InputStreams<'a> { - /// Construct an [`InputStreams`] from a [`World`] - pub fn from_world(world: &'a World, gamepad: Option) -> Self { - let gamepad_buttons = world.resource::>(); - let gamepad_button_axes = world.resource::>(); - let gamepad_axes = world.resource::>(); - let gamepads = world.resource::(); - let keycodes = world.get_resource::>(); - let scan_codes = world.get_resource::>(); - let mouse_buttons = world.get_resource::>(); - let mouse_wheel = world.get_resource::>(); - let mouse_motion = world.resource::>(); - - InputStreams { - gamepad_buttons, - gamepad_button_axes, - gamepad_axes, - gamepads, - keycodes, - scan_codes, - mouse_buttons, - mouse_wheel, - mouse_motion, - associated_gamepad: gamepad, - } - } -} - -// Input checking -impl<'a> InputStreams<'a> { - /// Guess which registered [`Gamepad`] should be used. - /// - /// If an associated gamepad is set, use that. - /// Otherwise use the first registered gamepad, if any. - pub fn guess_gamepad(&self) -> Option { - match self.associated_gamepad { - Some(gamepad) => Some(gamepad), - None => self.gamepads.iter().next(), - } - } - - /// Is the `input` matched by the [`InputStreams`]? - pub fn input_pressed(&self, input: &UserInput) -> bool { - match input { - UserInput::Single(button) => self.button_pressed(*button), - UserInput::Chord(buttons) => self.all_buttons_pressed(buttons), - UserInput::VirtualDPad(VirtualDPad { - up, - down, - left, - right, - }) => { - for button in [up, down, left, right] { - if self.button_pressed(*button) { - return true; - } - } - false - } - UserInput::VirtualAxis(VirtualAxis { negative, positive }) => { - self.button_pressed(*negative) || self.button_pressed(*positive) - } - } - } - - /// Is at least one of the `inputs` pressed? - #[must_use] - pub fn any_pressed(&self, inputs: &PetitSet) -> bool { - for input in inputs.iter() { - if self.input_pressed(input) { - return true; - } - } - // If none of the inputs matched, return false - false - } - - /// Is the `button` pressed? - #[must_use] - pub fn button_pressed(&self, button: InputKind) -> bool { - match button { - InputKind::DualAxis(axis) => { - self.button_pressed(InputKind::SingleAxis(axis.x)) - || self.button_pressed(InputKind::SingleAxis(axis.y)) - } - InputKind::SingleAxis(axis) => { - let value = self.input_value(&UserInput::Single(button), true); - - value < axis.negative_low || value > axis.positive_low - } - InputKind::GamepadButton(gamepad_button) => { - if let Some(gamepad) = self.guess_gamepad() { - self.gamepad_buttons.pressed(GamepadButton { - gamepad, - button_type: gamepad_button, - }) - } else { - false - } - } - InputKind::Keyboard(keycode) => { - matches!(self.keycodes, Some(keycodes) if keycodes.pressed(keycode)) - } - InputKind::KeyLocation(scan_code) => { - matches!(self.scan_codes, Some(scan_codes) if scan_codes.pressed(scan_code)) - } - InputKind::Modifier(modifier) => { - let key_codes = modifier.key_codes(); - // Short circuiting is probably not worth the branch here - matches!(self.keycodes, Some(keycodes) if keycodes.pressed(key_codes[0]) | keycodes.pressed(key_codes[1])) - } - InputKind::Mouse(mouse_button) => { - matches!(self.mouse_buttons, Some(mouse_buttons) if mouse_buttons.pressed(mouse_button)) - } - InputKind::MouseWheel(mouse_wheel_direction) => { - let Some(mouse_wheel) = self.mouse_wheel else { - return false; - }; - - let mut total_mouse_wheel_movement = 0.0; - - // FIXME: verify that this works and doesn't double count events - let mut event_reader = mouse_wheel.get_reader(); - - // PERF: this summing is computed for every individual input - // This should probably be computed once, and then cached / read - // Fix upstream! - for mouse_wheel_event in event_reader.iter(mouse_wheel) { - total_mouse_wheel_movement += match mouse_wheel_direction { - MouseWheelDirection::Up | MouseWheelDirection::Down => mouse_wheel_event.y, - MouseWheelDirection::Left | MouseWheelDirection::Right => { - mouse_wheel_event.x - } - } - } - - match mouse_wheel_direction { - MouseWheelDirection::Up | MouseWheelDirection::Right => { - total_mouse_wheel_movement > 0.0 - } - MouseWheelDirection::Down | MouseWheelDirection::Left => { - total_mouse_wheel_movement < 0.0 - } - } - } - // CLEANUP: refactor to share code with MouseWheel - InputKind::MouseMotion(mouse_motion_direction) => { - let mut total_mouse_movement = 0.0; - - // FIXME: verify that this works and doesn't double count events - let mut event_reader = self.mouse_motion.get_reader(); - - for mouse_motion_event in event_reader.iter(self.mouse_motion) { - total_mouse_movement += match mouse_motion_direction { - MouseMotionDirection::Up | MouseMotionDirection::Down => { - mouse_motion_event.delta.y - } - MouseMotionDirection::Left | MouseMotionDirection::Right => { - mouse_motion_event.delta.x - } - } - } - - match mouse_motion_direction { - MouseMotionDirection::Up | MouseMotionDirection::Right => { - total_mouse_movement > 0.0 - } - MouseMotionDirection::Down | MouseMotionDirection::Left => { - total_mouse_movement < 0.0 - } - } - } - } - } - - /// Are all of the `buttons` pressed? - #[must_use] - pub fn all_buttons_pressed(&self, buttons: &PetitSet) -> bool { - for &button in buttons.iter() { - // If any of the appropriate inputs failed to match, the action is considered pressed - if !self.button_pressed(button) { - return false; - } - } - // If none of the inputs failed to match, return true - true - } - - /// Get the "value" of the input. - /// - /// For binary inputs such as buttons, this will always be either `0.0` or `1.0`. For analog - /// inputs such as axes, this will be the axis value. - /// - /// [`UserInput::Chord`] inputs are also considered binary and will return `0.0` or `1.0` based - /// on whether the chord has been pressed. - /// - /// # Warning - /// - /// If you need to ensure that this value is always in the range `[-1., 1.]`, - /// be sure to clamp the returned data. - pub fn input_value(&self, input: &UserInput, include_deadzone: bool) -> f32 { - let use_button_value = || -> f32 { - if self.input_pressed(input) { - 1.0 - } else { - 0.0 - } - }; - - // Helper that takes the value returned by an axis and returns 0.0 if it is not within the - // triggering range. - let value_in_axis_range = |axis: &SingleAxis, value: f32| -> f32 { - if value >= axis.negative_low && value <= axis.positive_low && include_deadzone{ - 0.0 - } else if axis.inverted { - -value - } else { - value - } - }; - - match input { - UserInput::Single(InputKind::SingleAxis(single_axis)) => { - match single_axis.axis_type { - AxisType::Gamepad(axis_type) => { - if let Some(gamepad) = self.guess_gamepad() { - let value = self - .gamepad_axes - .get(GamepadAxis { gamepad, axis_type }) - .unwrap_or_default(); - - value_in_axis_range(single_axis, value) - } else { - 0.0 - } - } - AxisType::MouseWheel(axis_type) => { - let Some(mouse_wheel) = self.mouse_wheel else { - return 0.0; - }; - - let mut total_mouse_wheel_movement = 0.0; - // FIXME: verify that this works and doesn't double count events - let mut event_reader = mouse_wheel.get_reader(); - - for mouse_wheel_event in event_reader.iter(mouse_wheel) { - total_mouse_wheel_movement += match axis_type { - MouseWheelAxisType::X => mouse_wheel_event.x, - MouseWheelAxisType::Y => mouse_wheel_event.y, - } - } - value_in_axis_range(single_axis, total_mouse_wheel_movement) - } - // CLEANUP: deduplicate code with MouseWheel - AxisType::MouseMotion(axis_type) => { - let mut total_mouse_motion_movement = 0.0; - // FIXME: verify that this works and doesn't double count events - let mut event_reader = self.mouse_motion.get_reader(); - - for mouse_wheel_event in event_reader.iter(self.mouse_motion) { - total_mouse_motion_movement += match axis_type { - MouseMotionAxisType::X => mouse_wheel_event.delta.x, - MouseMotionAxisType::Y => mouse_wheel_event.delta.y, - } - } - value_in_axis_range(single_axis, total_mouse_motion_movement) - } - } - } - UserInput::VirtualAxis(VirtualAxis { negative, positive }) => { - self.input_value(&UserInput::Single(*positive), true).abs() - - self.input_value(&UserInput::Single(*negative), true).abs() - } - UserInput::Single(InputKind::DualAxis(_)) => { - self.input_axis_pair(input).unwrap_or_default().length() - } - UserInput::VirtualDPad { .. } => { - self.input_axis_pair(input).unwrap_or_default().length() - } - // This is required because upstream bevy::input still waffles about whether triggers are buttons or axes - UserInput::Single(InputKind::GamepadButton(button_type)) => { - if let Some(gamepad) = self.guess_gamepad() { - // Get the value from the registered gamepad - self.gamepad_button_axes - .get(GamepadButton { - gamepad, - button_type: *button_type, - }) - .unwrap_or_else(use_button_value) - } else { - 0.0 - } - } - _ => use_button_value(), - } - } - - /// Get the axis pair associated to the user input. - /// - /// If `input` is a chord, returns result of the first dual axis in the chord. - - /// If `input` is not a [`DualAxis`](crate::axislike::DualAxis) or [`VirtualDPad`], returns [`None`]. - /// - /// # Warning - /// - /// If you need to ensure that this value is always in the range `[-1., 1.]`, - /// be sure to clamp the returned data. - pub fn input_axis_pair(&self, input: &UserInput) -> Option { - match input { - UserInput::Chord(inputs) => inputs - .iter() - .flat_map(|input_kind| { - if let InputKind::DualAxis(dual_axis) = input_kind { - Some(self.extract_dual_axis_data(dual_axis)) - } else { - None - } - }) - .next(), - UserInput::Single(InputKind::DualAxis(dual_axis)) => { - Some(self.extract_dual_axis_data(dual_axis)) - } - UserInput::VirtualDPad(VirtualDPad { - up, - down, - left, - right, - }) => { - let x = self.input_value(&UserInput::Single(*right), true).abs() - - self.input_value(&UserInput::Single(*left), true).abs(); - let y = self.input_value(&UserInput::Single(*up), true).abs() - - self.input_value(&UserInput::Single(*down), true).abs(); - Some(DualAxisData::new(x, y)) - } - _ => None, - } - } - - fn extract_dual_axis_data(&self, dual_axis: &DualAxis) -> DualAxisData { - match dual_axis.deadzone_shape { - DeadzoneShape::Cross => { - let x = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.x)), true); - let y = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.y)), true); - - DualAxisData::new(x, y) - }, - DeadzoneShape::Rect => { - let x = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.x)), false); - let y = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.y)), false); - if x > dual_axis.x.positive_low - || x < dual_axis.x.negative_low - || y > dual_axis.y.positive_low - || y < dual_axis.y.negative_low - { - DualAxisData::new(x, y) - } else { - DualAxisData::new(0.0, 0.0) - } - }, - DeadzoneShape::Circle => { - let x = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.x)), false); - let y = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.y)), false); - let radius = dual_axis.x.positive_low.max(dual_axis.y.positive_low); - - if (x.powi(2) + y.powi(2)).sqrt() >= radius - { - DualAxisData::new(x, y) - } else { - DualAxisData::new(0.0, 0.0) - } - }, - } - } -} - -/// A mutable collection of [`Input`] structs, which can be used for mocking user inputs. -/// -/// These are typically collected via a system from the [`World`](bevy::prelude::World) as resources. -// WARNING: If you update the fields of this type, you must also remember to update `InputMocking::reset_inputs`. -#[derive(Debug)] -pub struct MutableInputStreams<'a> { - /// A [`GamepadButton`] [`Input`] stream - pub gamepad_buttons: &'a mut Input, - /// A [`GamepadButton`] [`Axis`] stream - pub gamepad_button_axes: &'a mut Axis, - /// A [`GamepadAxis`] [`Axis`] stream - pub gamepad_axes: &'a mut Axis, - /// A list of registered [`Gamepads`] - pub gamepads: &'a mut Gamepads, - /// Events used for mocking gamepad-related inputs - pub gamepad_events: &'a mut Events, - - /// A [`KeyCode`] [`Input`] stream - pub keycodes: &'a mut Input, - /// A [`ScanCode`] [`Input`] stream - pub scan_codes: &'a mut Input, - /// Events used for mocking keyboard-related inputs - pub keyboard_events: &'a mut Events, - - /// A [`MouseButton`] [`Input`] stream - pub mouse_buttons: &'a mut Input, - /// 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 [`Gamepad`] that this struct will detect inputs from - pub associated_gamepad: Option, -} - -impl<'a> MutableInputStreams<'a> { - /// Construct a [`MutableInputStreams`] from the [`World`] - pub fn from_world(world: &'a mut World, gamepad: Option) -> Self { - let mut input_system_state: SystemState<( - ResMut>, - ResMut>, - ResMut>, - ResMut, - ResMut>, - ResMut>, - ResMut>, - ResMut>, - ResMut>, - ResMut>, - ResMut>, - ResMut>, - )> = SystemState::new(world); - - let ( - gamepad_buttons, - gamepad_button_axes, - gamepad_axes, - gamepads, - gamepad_events, - keycodes, - scan_codes, - keyboard_events, - mouse_buttons, - mouse_button_events, - mouse_wheel, - mouse_motion, - ) = input_system_state.get_mut(world); - - MutableInputStreams { - gamepad_buttons: gamepad_buttons.into_inner(), - gamepad_button_axes: gamepad_button_axes.into_inner(), - gamepad_axes: gamepad_axes.into_inner(), - gamepads: gamepads.into_inner(), - gamepad_events: gamepad_events.into_inner(), - keycodes: keycodes.into_inner(), - scan_codes: scan_codes.into_inner(), - 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_motion: mouse_motion.into_inner(), - associated_gamepad: gamepad, - } - } - - /// Guess which registered [`Gamepad`] should be used. - /// - /// If an associated gamepad is set, use that. - /// Otherwise use the first registered gamepad, if any. - pub fn guess_gamepad(&self) -> Option { - match self.associated_gamepad { - Some(gamepad) => Some(gamepad), - None => self.gamepads.iter().next(), - } - } -} - -impl<'a> From> for InputStreams<'a> { - fn from(mutable_streams: MutableInputStreams<'a>) -> Self { - InputStreams { - gamepad_buttons: mutable_streams.gamepad_buttons, - gamepad_button_axes: mutable_streams.gamepad_button_axes, - gamepad_axes: mutable_streams.gamepad_axes, - gamepads: mutable_streams.gamepads, - keycodes: Some(mutable_streams.keycodes), - scan_codes: Some(mutable_streams.scan_codes), - mouse_buttons: Some(mutable_streams.mouse_buttons), - mouse_wheel: Some(mutable_streams.mouse_wheel), - mouse_motion: mutable_streams.mouse_motion, - associated_gamepad: mutable_streams.associated_gamepad, - } - } -} - -impl<'a> From<&'a MutableInputStreams<'a>> for InputStreams<'a> { - fn from(mutable_streams: &'a MutableInputStreams<'a>) -> Self { - InputStreams { - gamepad_buttons: mutable_streams.gamepad_buttons, - gamepad_button_axes: mutable_streams.gamepad_button_axes, - gamepad_axes: mutable_streams.gamepad_axes, - gamepads: mutable_streams.gamepads, - keycodes: Some(mutable_streams.keycodes), - scan_codes: Some(mutable_streams.scan_codes), - mouse_buttons: Some(mutable_streams.mouse_buttons), - mouse_wheel: Some(mutable_streams.mouse_wheel), - mouse_motion: mutable_streams.mouse_motion, - associated_gamepad: mutable_streams.associated_gamepad, - } - } -} - -#[cfg(test)] -mod tests { - use super::MutableInputStreams; - use crate::prelude::MockInput; - use bevy::input::InputPlugin; - use bevy::prelude::*; - - #[test] - fn modifier_key_triggered_by_either_input() { - use crate::user_input::Modifier; - let mut app = App::new(); - app.add_plugins(InputPlugin); - - let mut input_streams = MutableInputStreams::from_world(&mut app.world, None); - assert!(!input_streams.pressed(Modifier::Control)); - - input_streams.send_input(KeyCode::ControlLeft); - app.update(); - - let mut input_streams = MutableInputStreams::from_world(&mut app.world, None); - assert!(input_streams.pressed(Modifier::Control)); - - input_streams.reset_inputs(); - app.update(); - - let mut input_streams = MutableInputStreams::from_world(&mut app.world, None); - assert!(!input_streams.pressed(Modifier::Control)); - - input_streams.send_input(KeyCode::ControlRight); - app.update(); - - let input_streams = MutableInputStreams::from_world(&mut app.world, None); - assert!(input_streams.pressed(Modifier::Control)); - } -} +//! Unified input streams for working with [`bevy::input`] data. + +use bevy::input::{ + gamepad::{Gamepad, GamepadAxis, GamepadButton, GamepadEvent, Gamepads}, + keyboard::{KeyCode, KeyboardInput, ScanCode}, + mouse::{MouseButton, MouseButtonInput, MouseMotion, MouseWheel}, + Axis, Input, +}; +use petitset::PetitSet; + +use bevy::ecs::prelude::{Events, ResMut, World}; +use bevy::ecs::system::SystemState; + +use crate::axislike::{ + AxisType, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, SingleAxis, VirtualAxis, + VirtualDPad, +}; +use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; +use crate::prelude::DualAxis; +use crate::user_input::{InputKind, UserInput}; + +/// A collection of [`Input`] structs, which can be used to update an [`InputMap`](crate::input_map::InputMap). +/// +/// These are typically collected via a system from the [`World`](bevy::prelude::World) as resources. +#[derive(Debug, Clone)] +pub struct InputStreams<'a> { + /// A [`GamepadButton`] [`Input`] stream + pub gamepad_buttons: &'a Input, + /// A [`GamepadButton`] [`Axis`] stream + pub gamepad_button_axes: &'a Axis, + /// A [`GamepadAxis`] [`Axis`] stream + pub gamepad_axes: &'a Axis, + /// A list of registered gamepads + pub gamepads: &'a Gamepads, + /// A [`KeyCode`] [`Input`] stream + pub keycodes: Option<&'a Input>, + /// A [`ScanCode`] [`Input`] stream + pub scan_codes: Option<&'a Input>, + /// A [`MouseButton`] [`Input`] stream + pub mouse_buttons: Option<&'a Input>, + /// A [`MouseWheel`] event stream + pub mouse_wheel: Option<&'a Events>, + /// A [`MouseMotion`] event stream + pub mouse_motion: &'a Events, + /// The [`Gamepad`] that this struct will detect inputs from + pub associated_gamepad: Option, +} + +// Constructors +impl<'a> InputStreams<'a> { + /// Construct an [`InputStreams`] from a [`World`] + pub fn from_world(world: &'a World, gamepad: Option) -> Self { + let gamepad_buttons = world.resource::>(); + let gamepad_button_axes = world.resource::>(); + let gamepad_axes = world.resource::>(); + let gamepads = world.resource::(); + let keycodes = world.get_resource::>(); + let scan_codes = world.get_resource::>(); + let mouse_buttons = world.get_resource::>(); + let mouse_wheel = world.get_resource::>(); + let mouse_motion = world.resource::>(); + + InputStreams { + gamepad_buttons, + gamepad_button_axes, + gamepad_axes, + gamepads, + keycodes, + scan_codes, + mouse_buttons, + mouse_wheel, + mouse_motion, + associated_gamepad: gamepad, + } + } +} + +// Input checking +impl<'a> InputStreams<'a> { + /// Guess which registered [`Gamepad`] should be used. + /// + /// If an associated gamepad is set, use that. + /// Otherwise use the first registered gamepad, if any. + pub fn guess_gamepad(&self) -> Option { + match self.associated_gamepad { + Some(gamepad) => Some(gamepad), + None => self.gamepads.iter().next(), + } + } + + /// Is the `input` matched by the [`InputStreams`]? + pub fn input_pressed(&self, input: &UserInput) -> bool { + match input { + UserInput::Single(button) => self.button_pressed(*button), + UserInput::Chord(buttons) => self.all_buttons_pressed(buttons), + UserInput::VirtualDPad(VirtualDPad { + up, + down, + left, + right, + }) => { + for button in [up, down, left, right] { + if self.button_pressed(*button) { + return true; + } + } + false + } + UserInput::VirtualAxis(VirtualAxis { negative, positive }) => { + self.button_pressed(*negative) || self.button_pressed(*positive) + } + } + } + + /// Is at least one of the `inputs` pressed? + #[must_use] + pub fn any_pressed(&self, inputs: &PetitSet) -> bool { + for input in inputs.iter() { + if self.input_pressed(input) { + return true; + } + } + // If none of the inputs matched, return false + false + } + + /// Is the `button` pressed? + #[must_use] + pub fn button_pressed(&self, button: InputKind) -> bool { + match button { + InputKind::DualAxis(axis) => { + self.button_pressed(InputKind::SingleAxis(axis.x)) + || self.button_pressed(InputKind::SingleAxis(axis.y)) + } + InputKind::SingleAxis(axis) => { + let value = self.input_value(&UserInput::Single(button), true); + + value < axis.negative_low || value > axis.positive_low + } + InputKind::GamepadButton(gamepad_button) => { + if let Some(gamepad) = self.guess_gamepad() { + self.gamepad_buttons.pressed(GamepadButton { + gamepad, + button_type: gamepad_button, + }) + } else { + false + } + } + InputKind::Keyboard(keycode) => { + matches!(self.keycodes, Some(keycodes) if keycodes.pressed(keycode)) + } + InputKind::KeyLocation(scan_code) => { + matches!(self.scan_codes, Some(scan_codes) if scan_codes.pressed(scan_code)) + } + InputKind::Modifier(modifier) => { + let key_codes = modifier.key_codes(); + // Short circuiting is probably not worth the branch here + matches!(self.keycodes, Some(keycodes) if keycodes.pressed(key_codes[0]) | keycodes.pressed(key_codes[1])) + } + InputKind::Mouse(mouse_button) => { + matches!(self.mouse_buttons, Some(mouse_buttons) if mouse_buttons.pressed(mouse_button)) + } + InputKind::MouseWheel(mouse_wheel_direction) => { + let Some(mouse_wheel) = self.mouse_wheel else { + return false; + }; + + let mut total_mouse_wheel_movement = 0.0; + + // FIXME: verify that this works and doesn't double count events + let mut event_reader = mouse_wheel.get_reader(); + + // PERF: this summing is computed for every individual input + // This should probably be computed once, and then cached / read + // Fix upstream! + for mouse_wheel_event in event_reader.iter(mouse_wheel) { + total_mouse_wheel_movement += match mouse_wheel_direction { + MouseWheelDirection::Up | MouseWheelDirection::Down => mouse_wheel_event.y, + MouseWheelDirection::Left | MouseWheelDirection::Right => { + mouse_wheel_event.x + } + } + } + + match mouse_wheel_direction { + MouseWheelDirection::Up | MouseWheelDirection::Right => { + total_mouse_wheel_movement > 0.0 + } + MouseWheelDirection::Down | MouseWheelDirection::Left => { + total_mouse_wheel_movement < 0.0 + } + } + } + // CLEANUP: refactor to share code with MouseWheel + InputKind::MouseMotion(mouse_motion_direction) => { + let mut total_mouse_movement = 0.0; + + // FIXME: verify that this works and doesn't double count events + let mut event_reader = self.mouse_motion.get_reader(); + + for mouse_motion_event in event_reader.iter(self.mouse_motion) { + total_mouse_movement += match mouse_motion_direction { + MouseMotionDirection::Up | MouseMotionDirection::Down => { + mouse_motion_event.delta.y + } + MouseMotionDirection::Left | MouseMotionDirection::Right => { + mouse_motion_event.delta.x + } + } + } + + match mouse_motion_direction { + MouseMotionDirection::Up | MouseMotionDirection::Right => { + total_mouse_movement > 0.0 + } + MouseMotionDirection::Down | MouseMotionDirection::Left => { + total_mouse_movement < 0.0 + } + } + } + } + } + + /// Are all of the `buttons` pressed? + #[must_use] + pub fn all_buttons_pressed(&self, buttons: &PetitSet) -> bool { + for &button in buttons.iter() { + // If any of the appropriate inputs failed to match, the action is considered pressed + if !self.button_pressed(button) { + return false; + } + } + // If none of the inputs failed to match, return true + true + } + + /// Get the "value" of the input. + /// + /// For binary inputs such as buttons, this will always be either `0.0` or `1.0`. For analog + /// inputs such as axes, this will be the axis value. + /// + /// [`UserInput::Chord`] inputs are also considered binary and will return `0.0` or `1.0` based + /// on whether the chord has been pressed. + /// + /// # Warning + /// + /// If you need to ensure that this value is always in the range `[-1., 1.]`, + /// be sure to clamp the returned data. + pub fn input_value(&self, input: &UserInput, include_deadzone: bool) -> f32 { + let use_button_value = || -> f32 { + if self.input_pressed(input) { + 1.0 + } else { + 0.0 + } + }; + + // Helper that takes the value returned by an axis and returns 0.0 if it is not within the + // triggering range. + let value_in_axis_range = |axis: &SingleAxis, value: f32| -> f32 { + if value >= axis.negative_low && value <= axis.positive_low && include_deadzone { + 0.0 + } else if axis.inverted { + -value + } else { + value + } + }; + + match input { + UserInput::Single(InputKind::SingleAxis(single_axis)) => { + match single_axis.axis_type { + AxisType::Gamepad(axis_type) => { + if let Some(gamepad) = self.guess_gamepad() { + let value = self + .gamepad_axes + .get(GamepadAxis { gamepad, axis_type }) + .unwrap_or_default(); + + value_in_axis_range(single_axis, value) + } else { + 0.0 + } + } + AxisType::MouseWheel(axis_type) => { + let Some(mouse_wheel) = self.mouse_wheel else { + return 0.0; + }; + + let mut total_mouse_wheel_movement = 0.0; + // FIXME: verify that this works and doesn't double count events + let mut event_reader = mouse_wheel.get_reader(); + + for mouse_wheel_event in event_reader.iter(mouse_wheel) { + total_mouse_wheel_movement += match axis_type { + MouseWheelAxisType::X => mouse_wheel_event.x, + MouseWheelAxisType::Y => mouse_wheel_event.y, + } + } + value_in_axis_range(single_axis, total_mouse_wheel_movement) + } + // CLEANUP: deduplicate code with MouseWheel + AxisType::MouseMotion(axis_type) => { + let mut total_mouse_motion_movement = 0.0; + // FIXME: verify that this works and doesn't double count events + let mut event_reader = self.mouse_motion.get_reader(); + + for mouse_wheel_event in event_reader.iter(self.mouse_motion) { + total_mouse_motion_movement += match axis_type { + MouseMotionAxisType::X => mouse_wheel_event.delta.x, + MouseMotionAxisType::Y => mouse_wheel_event.delta.y, + } + } + value_in_axis_range(single_axis, total_mouse_motion_movement) + } + } + } + UserInput::VirtualAxis(VirtualAxis { negative, positive }) => { + self.input_value(&UserInput::Single(*positive), true).abs() + - self.input_value(&UserInput::Single(*negative), true).abs() + } + UserInput::Single(InputKind::DualAxis(_)) => { + self.input_axis_pair(input).unwrap_or_default().length() + } + UserInput::VirtualDPad { .. } => { + self.input_axis_pair(input).unwrap_or_default().length() + } + // This is required because upstream bevy::input still waffles about whether triggers are buttons or axes + UserInput::Single(InputKind::GamepadButton(button_type)) => { + if let Some(gamepad) = self.guess_gamepad() { + // Get the value from the registered gamepad + self.gamepad_button_axes + .get(GamepadButton { + gamepad, + button_type: *button_type, + }) + .unwrap_or_else(use_button_value) + } else { + 0.0 + } + } + _ => use_button_value(), + } + } + + /// Get the axis pair associated to the user input. + /// + /// If `input` is a chord, returns result of the first dual axis in the chord. + + /// If `input` is not a [`DualAxis`](crate::axislike::DualAxis) or [`VirtualDPad`], returns [`None`]. + /// + /// # Warning + /// + /// If you need to ensure that this value is always in the range `[-1., 1.]`, + /// be sure to clamp the returned data. + pub fn input_axis_pair(&self, input: &UserInput) -> Option { + match input { + UserInput::Chord(inputs) => inputs + .iter() + .flat_map(|input_kind| { + if let InputKind::DualAxis(dual_axis) = input_kind { + Some(self.extract_dual_axis_data(dual_axis)) + } else { + None + } + }) + .next(), + UserInput::Single(InputKind::DualAxis(dual_axis)) => { + Some(self.extract_dual_axis_data(dual_axis)) + } + UserInput::VirtualDPad(VirtualDPad { + up, + down, + left, + right, + }) => { + let x = self.input_value(&UserInput::Single(*right), true).abs() + - self.input_value(&UserInput::Single(*left), true).abs(); + let y = self.input_value(&UserInput::Single(*up), true).abs() + - self.input_value(&UserInput::Single(*down), true).abs(); + Some(DualAxisData::new(x, y)) + } + _ => None, + } + } + + fn extract_dual_axis_data(&self, dual_axis: &DualAxis) -> DualAxisData { + dual_axis.deadzone_shape.get_dual_axis_data(dual_axis, self) + } +} + +/// A mutable collection of [`Input`] structs, which can be used for mocking user inputs. +/// +/// These are typically collected via a system from the [`World`](bevy::prelude::World) as resources. +// WARNING: If you update the fields of this type, you must also remember to update `InputMocking::reset_inputs`. +#[derive(Debug)] +pub struct MutableInputStreams<'a> { + /// A [`GamepadButton`] [`Input`] stream + pub gamepad_buttons: &'a mut Input, + /// A [`GamepadButton`] [`Axis`] stream + pub gamepad_button_axes: &'a mut Axis, + /// A [`GamepadAxis`] [`Axis`] stream + pub gamepad_axes: &'a mut Axis, + /// A list of registered [`Gamepads`] + pub gamepads: &'a mut Gamepads, + /// Events used for mocking gamepad-related inputs + pub gamepad_events: &'a mut Events, + + /// A [`KeyCode`] [`Input`] stream + pub keycodes: &'a mut Input, + /// A [`ScanCode`] [`Input`] stream + pub scan_codes: &'a mut Input, + /// Events used for mocking keyboard-related inputs + pub keyboard_events: &'a mut Events, + + /// A [`MouseButton`] [`Input`] stream + pub mouse_buttons: &'a mut Input, + /// 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 [`Gamepad`] that this struct will detect inputs from + pub associated_gamepad: Option, +} + +impl<'a> MutableInputStreams<'a> { + /// Construct a [`MutableInputStreams`] from the [`World`] + pub fn from_world(world: &'a mut World, gamepad: Option) -> Self { + let mut input_system_state: SystemState<( + ResMut>, + ResMut>, + ResMut>, + ResMut, + ResMut>, + ResMut>, + ResMut>, + ResMut>, + ResMut>, + ResMut>, + ResMut>, + ResMut>, + )> = SystemState::new(world); + + let ( + gamepad_buttons, + gamepad_button_axes, + gamepad_axes, + gamepads, + gamepad_events, + keycodes, + scan_codes, + keyboard_events, + mouse_buttons, + mouse_button_events, + mouse_wheel, + mouse_motion, + ) = input_system_state.get_mut(world); + + MutableInputStreams { + gamepad_buttons: gamepad_buttons.into_inner(), + gamepad_button_axes: gamepad_button_axes.into_inner(), + gamepad_axes: gamepad_axes.into_inner(), + gamepads: gamepads.into_inner(), + gamepad_events: gamepad_events.into_inner(), + keycodes: keycodes.into_inner(), + scan_codes: scan_codes.into_inner(), + 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_motion: mouse_motion.into_inner(), + associated_gamepad: gamepad, + } + } + + /// Guess which registered [`Gamepad`] should be used. + /// + /// If an associated gamepad is set, use that. + /// Otherwise use the first registered gamepad, if any. + pub fn guess_gamepad(&self) -> Option { + match self.associated_gamepad { + Some(gamepad) => Some(gamepad), + None => self.gamepads.iter().next(), + } + } +} + +impl<'a> From> for InputStreams<'a> { + fn from(mutable_streams: MutableInputStreams<'a>) -> Self { + InputStreams { + gamepad_buttons: mutable_streams.gamepad_buttons, + gamepad_button_axes: mutable_streams.gamepad_button_axes, + gamepad_axes: mutable_streams.gamepad_axes, + gamepads: mutable_streams.gamepads, + keycodes: Some(mutable_streams.keycodes), + scan_codes: Some(mutable_streams.scan_codes), + mouse_buttons: Some(mutable_streams.mouse_buttons), + mouse_wheel: Some(mutable_streams.mouse_wheel), + mouse_motion: mutable_streams.mouse_motion, + associated_gamepad: mutable_streams.associated_gamepad, + } + } +} + +impl<'a> From<&'a MutableInputStreams<'a>> for InputStreams<'a> { + fn from(mutable_streams: &'a MutableInputStreams<'a>) -> Self { + InputStreams { + gamepad_buttons: mutable_streams.gamepad_buttons, + gamepad_button_axes: mutable_streams.gamepad_button_axes, + gamepad_axes: mutable_streams.gamepad_axes, + gamepads: mutable_streams.gamepads, + keycodes: Some(mutable_streams.keycodes), + scan_codes: Some(mutable_streams.scan_codes), + mouse_buttons: Some(mutable_streams.mouse_buttons), + mouse_wheel: Some(mutable_streams.mouse_wheel), + mouse_motion: mutable_streams.mouse_motion, + associated_gamepad: mutable_streams.associated_gamepad, + } + } +} + +#[cfg(test)] +mod tests { + use super::MutableInputStreams; + use crate::prelude::MockInput; + use bevy::input::InputPlugin; + use bevy::prelude::*; + + #[test] + fn modifier_key_triggered_by_either_input() { + use crate::user_input::Modifier; + let mut app = App::new(); + app.add_plugins(InputPlugin); + + let mut input_streams = MutableInputStreams::from_world(&mut app.world, None); + assert!(!input_streams.pressed(Modifier::Control)); + + input_streams.send_input(KeyCode::ControlLeft); + app.update(); + + let mut input_streams = MutableInputStreams::from_world(&mut app.world, None); + assert!(input_streams.pressed(Modifier::Control)); + + input_streams.reset_inputs(); + app.update(); + + let mut input_streams = MutableInputStreams::from_world(&mut app.world, None); + assert!(!input_streams.pressed(Modifier::Control)); + + input_streams.send_input(KeyCode::ControlRight); + app.update(); + + let input_streams = MutableInputStreams::from_world(&mut app.world, None); + assert!(input_streams.pressed(Modifier::Control)); + } +} From a30811a1c50d8c32d3a8862634617cf8437b94ac Mon Sep 17 00:00:00 2001 From: 100-TomatoJuice Date: Mon, 31 Jul 2023 09:30:36 -0700 Subject: [PATCH 03/12] Cleaned up `Cross` --- src/axislike.rs | 137 ++++++++++++++++++------------------------ src/input_streams.rs | 15 ++++- tests/gamepad_axis.rs | 2 +- tests/mouse_motion.rs | 2 +- tests/mouse_wheel.rs | 2 +- 5 files changed, 77 insertions(+), 81 deletions(-) diff --git a/src/axislike.rs b/src/axislike.rs index 615e108e..d4b79a03 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -1,10 +1,9 @@ //! Tools for working with directional axis-like user inputs (gamesticks, D-Pads and emulated equivalents) use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; -use crate::input_streams::InputStreams; use crate::orientation::{Direction, Rotation}; use crate::prelude::QwertyScanCode; -use crate::user_input::{InputKind, UserInput}; +use crate::user_input::InputKind; use bevy::input::{ gamepad::{GamepadAxisType, GamepadButtonType}, keyboard::KeyCode, @@ -188,7 +187,7 @@ pub struct DualAxis { /// The axis representing vertical movement. pub y: SingleAxis, /// The shape of the deadzone - pub deadzone_shape: DeadZoneShape, + pub deadzone: DeadZoneShape, } impl DualAxis { @@ -201,8 +200,10 @@ impl DualAxis { /// /// This cannot be changed, but the struct can be easily manually constructed. pub const DEFAULT_DEADZONE_SHAPE: DeadZoneShape = DeadZoneShape::Cross { - horizontal_width: Self::DEFAULT_DEADZONE, - vertical_width: Self::DEFAULT_DEADZONE, + rect_1_width: Self::DEFAULT_DEADZONE, + rect_1_height: Self::DEFAULT_DEADZONE, + rect_2_width: Self::DEFAULT_DEADZONE, + rect_2_height: Self::DEFAULT_DEADZONE, }; /// Creates a [`DualAxis`] with both `positive_low` and `negative_low` in both axes set to `threshold` with a `deadzone_shape`. @@ -216,7 +217,7 @@ impl DualAxis { DualAxis { x: SingleAxis::symmetric(x_axis_type, threshold), y: SingleAxis::symmetric(y_axis_type, threshold), - deadzone_shape, + deadzone: deadzone_shape, } } @@ -234,7 +235,7 @@ impl DualAxis { DualAxis { x: SingleAxis::from_value(x_axis_type, x_value), y: SingleAxis::from_value(y_axis_type, y_value), - deadzone_shape: Self::DEFAULT_DEADZONE_SHAPE, + deadzone: Self::DEFAULT_DEADZONE_SHAPE, } } @@ -265,7 +266,7 @@ impl DualAxis { DualAxis { x: SingleAxis::mouse_wheel_x(), y: SingleAxis::mouse_wheel_y(), - deadzone_shape: Self::DEFAULT_DEADZONE_SHAPE, + deadzone: Self::DEFAULT_DEADZONE_SHAPE, } } @@ -274,14 +275,14 @@ impl DualAxis { DualAxis { x: SingleAxis::mouse_motion_x(), y: SingleAxis::mouse_motion_y(), - deadzone_shape: Self::DEFAULT_DEADZONE_SHAPE, + deadzone: Self::DEFAULT_DEADZONE_SHAPE, } } /// Returns this [`DualAxis`] with the deadzone set to the specified values and shape #[must_use] pub fn with_deadzone(mut self, deadzone: DeadZoneShape) -> DualAxis { - self.deadzone_shape = deadzone; + self.deadzone = deadzone; self } @@ -707,35 +708,31 @@ impl From for Vec2 { } /// The shape of the deadzone for a [`DualAxis`] input. -/// -/// Uses the x and y thresholds on [`DualAxis`] to set the shape diamensions #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] pub enum DeadZoneShape { - /// Deadzone with a cross - /// - /// Uses the x threshold as the horizontal width and y threshold as the vertical width. + /// Deadzone with the shape of a cross. The cross is represented by two perpendicular rectangles. Cross { - /// The width of the horizontal rectangle. - horizontal_width: f32, - /// The width of the vertical rectangle. - vertical_width: f32, + /// The width of the first rectangle. + rect_1_width: f32, + /// The height of the first rectangle. + rect_1_height: f32, + /// The width of the second rectangle. + rect_2_width: f32, + /// The height of the second rectangle. + rect_2_height: f32, }, - /// Deadzone with a rectangle. - /// - /// Uses the x threshold as the width and y threshold as the length. + /// Deadzone with the shape of a rectangle. Rect { /// The width of the rectangle. width: f32, /// The height of the rectangle. height: f32, }, - /// Deadzone with a circle. - /// - /// Uses the larger of the x or y threshold as the radius + /// Deadzone with the shpae of an ellipse. Ellipse { - /// The horizontal radius + /// The horizontal radius of the ellipse. radius_x: f32, - /// The vertical radius + /// The vertical radius of the ellipse. radius_y: f32, }, } @@ -746,64 +743,50 @@ impl std::hash::Hash for DeadZoneShape { } impl DeadZoneShape { - /// - pub fn get_dual_axis_data( - &self, - dual_axis: &DualAxis, - input_stream: &InputStreams, - ) -> DualAxisData { + /// Returns whether the (x, y) input is outside the deadzone. + pub fn input_outside_deadzone(&self, x: f32, y: f32) -> bool { match self { DeadZoneShape::Cross { - horizontal_width, - vertical_width, - } => { - let x = input_stream - .input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.x)), true); - let y = input_stream - .input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.y)), true); - - DualAxisData::new(x, y) - } - DeadZoneShape::Rect { width, height } => { - let x = input_stream.input_value( - &UserInput::Single(InputKind::SingleAxis(dual_axis.x)), - false, - ); - let y = input_stream.input_value( - &UserInput::Single(InputKind::SingleAxis(dual_axis.y)), - false, - ); - - if DeadZoneShape::outside_rectangle(x, y, *width, *height) { - DualAxisData::new(x, y) - } else { - DualAxisData::new(0.0, 0.0) - } - } + rect_1_width, + rect_1_height, + rect_2_width, + rect_2_height, + } => self.outside_cross( + x, + y, + *rect_1_width, + *rect_1_height, + *rect_2_width, + *rect_2_height, + ), + DeadZoneShape::Rect { width, height } => self.outside_rectangle(x, y, *width, *height), DeadZoneShape::Ellipse { radius_x, radius_y } => { - let x = input_stream.input_value( - &UserInput::Single(InputKind::SingleAxis(dual_axis.x)), - false, - ); - let y = input_stream.input_value( - &UserInput::Single(InputKind::SingleAxis(dual_axis.y)), - false, - ); - - if DeadZoneShape::outside_ellipse(x, y, *radius_x, *radius_y) { - DualAxisData::new(x, y) - } else { - DualAxisData::new(0.0, 0.0) - } + self.outside_ellipse(x, y, *radius_x, *radius_y) } } } - fn outside_ellipse(x: f32, y: f32, radius_x: f32, radius_y: f32) -> bool { - (x.powi(2) / radius_x.powi(2) + y.powi(2) / radius_y.powi(2)) >= 1.0 + /// Returns whether the (x, y) input is outside a cross. + fn outside_cross( + &self, + x: f32, + y: f32, + horizontal_width: f32, + horizontal_height: f32, + vertical_width: f32, + vertical_height: f32, + ) -> bool { + self.outside_rectangle(x, y, horizontal_width, horizontal_height) + && self.outside_rectangle(x, y, vertical_width, vertical_height) } - fn outside_rectangle(x: f32, y: f32, width: f32, height: f32) -> bool { + /// Returns whether the (x, y) input is outside a rectangle. + fn outside_rectangle(&self, x: f32, y: f32, width: f32, height: f32) -> bool { x > width || x < -width || y > height || y < -height } -} \ No newline at end of file + + /// Returns whether the (x, y) input is outside an ellipse. + fn outside_ellipse(&self, x: f32, y: f32, radius_x: f32, radius_y: f32) -> bool { + (x.powi(2) / radius_x.powi(2) + y.powi(2) / radius_y.powi(2)) >= 1.0 + } +} diff --git a/src/input_streams.rs b/src/input_streams.rs index 5b6cadb5..b32c93fc 100644 --- a/src/input_streams.rs +++ b/src/input_streams.rs @@ -386,7 +386,20 @@ impl<'a> InputStreams<'a> { } fn extract_dual_axis_data(&self, dual_axis: &DualAxis) -> DualAxisData { - dual_axis.deadzone_shape.get_dual_axis_data(dual_axis, self) + let x = self.input_value( + &UserInput::Single(InputKind::SingleAxis(dual_axis.x)), + false, + ); + let y = self.input_value( + &UserInput::Single(InputKind::SingleAxis(dual_axis.y)), + false, + ); + + if dual_axis.deadzone.input_outside_deadzone(x, y) { + DualAxisData::new(x, y) + } else { + DualAxisData::new(0.0, 0.0) + } } } diff --git a/tests/gamepad_axis.rs b/tests/gamepad_axis.rs index add7a24f..368bf5ca 100644 --- a/tests/gamepad_axis.rs +++ b/tests/gamepad_axis.rs @@ -108,7 +108,7 @@ fn game_pad_dual_axis_mocking() { negative_low: 0.0, inverted: false, }, - deadzone_shape: DualAxis::DEFAULT_DEADZONE_SHAPE, + deadzone: DualAxis::DEFAULT_DEADZONE_SHAPE, }; app.send_input(input); let mut events = app.world.resource_mut::>(); diff --git a/tests/mouse_motion.rs b/tests/mouse_motion.rs index 6908e2de..9a5ccc51 100644 --- a/tests/mouse_motion.rs +++ b/tests/mouse_motion.rs @@ -103,7 +103,7 @@ fn mouse_motion_dual_axis_mocking() { negative_low: 0.0, inverted: false, }, - deadzone_shape: DualAxis::DEFAULT_DEADZONE_SHAPE, + deadzone: DualAxis::DEFAULT_DEADZONE_SHAPE, }; app.send_input(input); let mut events = app.world.resource_mut::>(); diff --git a/tests/mouse_wheel.rs b/tests/mouse_wheel.rs index 3ef6f89c..e43aa568 100644 --- a/tests/mouse_wheel.rs +++ b/tests/mouse_wheel.rs @@ -104,7 +104,7 @@ fn mouse_wheel_dual_axis_mocking() { negative_low: 0.0, inverted: false, }, - deadzone_shape: DualAxis::DEFAULT_DEADZONE_SHAPE, + deadzone: DualAxis::DEFAULT_DEADZONE_SHAPE, }; app.send_input(input); let mut events = app.world.resource_mut::>(); From 02e7806eaf87d3786e031a996faaa61ffbc480fa Mon Sep 17 00:00:00 2001 From: 100-TomatoJuice Date: Tue, 1 Aug 2023 10:10:46 -0700 Subject: [PATCH 04/12] Add tests + fixes --- src/axislike.rs | 33 ++++++-------- src/input_streams.rs | 6 ++- tests/gamepad_axis.rs | 102 +++++++++++++++++++++++++++++++++++------- 3 files changed, 105 insertions(+), 36 deletions(-) diff --git a/src/axislike.rs b/src/axislike.rs index d4b79a03..51fa233c 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -200,10 +200,10 @@ impl DualAxis { /// /// This cannot be changed, but the struct can be easily manually constructed. pub const DEFAULT_DEADZONE_SHAPE: DeadZoneShape = DeadZoneShape::Cross { - rect_1_width: Self::DEFAULT_DEADZONE, + rect_1_width: Self::DEFAULT_DEADZONE / 2.0, rect_1_height: Self::DEFAULT_DEADZONE, rect_2_width: Self::DEFAULT_DEADZONE, - rect_2_height: Self::DEFAULT_DEADZONE, + rect_2_height: Self::DEFAULT_DEADZONE / 2.0, }; /// Creates a [`DualAxis`] with both `positive_low` and `negative_low` in both axes set to `threshold` with a `deadzone_shape`. @@ -211,12 +211,11 @@ impl DualAxis { pub fn symmetric( x_axis_type: impl Into, y_axis_type: impl Into, - threshold: f32, deadzone_shape: DeadZoneShape, ) -> DualAxis { DualAxis { - x: SingleAxis::symmetric(x_axis_type, threshold), - y: SingleAxis::symmetric(y_axis_type, threshold), + x: SingleAxis::symmetric(x_axis_type, 0.0), + y: SingleAxis::symmetric(y_axis_type, 0.0), deadzone: deadzone_shape, } } @@ -245,7 +244,6 @@ impl DualAxis { DualAxis::symmetric( GamepadAxisType::LeftStickX, GamepadAxisType::LeftStickY, - Self::DEFAULT_DEADZONE, Self::DEFAULT_DEADZONE_SHAPE, ) } @@ -256,7 +254,6 @@ impl DualAxis { DualAxis::symmetric( GamepadAxisType::RightStickX, GamepadAxisType::RightStickY, - Self::DEFAULT_DEADZONE, Self::DEFAULT_DEADZONE_SHAPE, ) } @@ -710,7 +707,7 @@ impl From for Vec2 { /// The shape of the deadzone for a [`DualAxis`] input. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] pub enum DeadZoneShape { - /// Deadzone with the shape of a cross. The cross is represented by two perpendicular rectangles. + /// Deadzone with the shape of a cross. The cross is represented by two rectangles. Cross { /// The width of the first rectangle. rect_1_width: f32, @@ -728,7 +725,7 @@ pub enum DeadZoneShape { /// The height of the rectangle. height: f32, }, - /// Deadzone with the shpae of an ellipse. + /// Deadzone with the shape of an ellipse. Ellipse { /// The horizontal radius of the ellipse. radius_x: f32, @@ -760,9 +757,7 @@ impl DeadZoneShape { *rect_2_height, ), DeadZoneShape::Rect { width, height } => self.outside_rectangle(x, y, *width, *height), - DeadZoneShape::Ellipse { radius_x, radius_y } => { - self.outside_ellipse(x, y, *radius_x, *radius_y) - } + DeadZoneShape::Ellipse { radius_x, radius_y } => self.outside_ellipse(x, y, *radius_x, *radius_y), } } @@ -771,13 +766,13 @@ impl DeadZoneShape { &self, x: f32, y: f32, - horizontal_width: f32, - horizontal_height: f32, - vertical_width: f32, - vertical_height: f32, + rect_1_width: f32, + rect_1_height: f32, + rect_2_width: f32, + rect_2_height: f32, ) -> bool { - self.outside_rectangle(x, y, horizontal_width, horizontal_height) - && self.outside_rectangle(x, y, vertical_width, vertical_height) + self.outside_rectangle(x, y, rect_1_width, rect_1_height) + && self.outside_rectangle(x, y, rect_2_width, rect_2_height) } /// Returns whether the (x, y) input is outside a rectangle. @@ -787,6 +782,6 @@ impl DeadZoneShape { /// Returns whether the (x, y) input is outside an ellipse. fn outside_ellipse(&self, x: f32, y: f32, radius_x: f32, radius_y: f32) -> bool { - (x.powi(2) / radius_x.powi(2) + y.powi(2) / radius_y.powi(2)) >= 1.0 + (x.powi(2) / radius_x.powi(2) + y.powi(2) / radius_y.powi(2)) > 1.0 } } diff --git a/src/input_streams.rs b/src/input_streams.rs index b32c93fc..da42d02e 100644 --- a/src/input_streams.rs +++ b/src/input_streams.rs @@ -129,8 +129,10 @@ impl<'a> InputStreams<'a> { pub fn button_pressed(&self, button: InputKind) -> bool { match button { InputKind::DualAxis(axis) => { - self.button_pressed(InputKind::SingleAxis(axis.x)) - || self.button_pressed(InputKind::SingleAxis(axis.y)) + let x_value = self.input_value(&UserInput::Single(InputKind::SingleAxis(axis.x)), false); + let y_value = self.input_value(&UserInput::Single(InputKind::SingleAxis(axis.y)), false); + + axis.deadzone.input_outside_deadzone(x_value, y_value) } InputKind::SingleAxis(axis) => { let value = self.input_value(&UserInput::Single(button), true); diff --git a/tests/gamepad_axis.rs b/tests/gamepad_axis.rs index 368bf5ca..0d08d26e 100644 --- a/tests/gamepad_axis.rs +++ b/tests/gamepad_axis.rs @@ -3,7 +3,7 @@ use bevy::input::gamepad::{ }; use bevy::input::InputPlugin; use bevy::prelude::*; -use leafwing_input_manager::axislike::{AxisType, DualAxisData}; +use leafwing_input_manager::axislike::{AxisType, DualAxisData, DeadZoneShape}; use leafwing_input_manager::prelude::*; #[derive(Actionlike, Clone, Copy, Debug, Reflect)] @@ -211,36 +211,64 @@ fn game_pad_single_axis() { } #[test] -fn game_pad_dual_axis() { +fn game_pad_dual_axis_cross() { let mut app = test_app(); app.insert_resource(InputMap::new([( - DualAxis::left_stick(), + DualAxis::left_stick().with_deadzone(DeadZoneShape::Cross { rect_1_width: 0.1, rect_1_height: 0.05, rect_2_width: 0.05, rect_2_height: 0.1 }), AxislikeTestAction::XY, )])); + // Test that an input inside the cross deadzone is filtered out app.send_input(DualAxis::from_value( GamepadAxisType::LeftStickX, GamepadAxisType::LeftStickY, - 0.8, - 0.0, + 0.1, + 0.05, + )); + + app.update(); + + let action_state = app.world.resource::>(); + assert!(action_state.released(AxislikeTestAction::XY)); + assert_eq!(action_state.value(AxislikeTestAction::XY), 0.0); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + DualAxisData::new(0.0, 0.0) + ); + + // Test that an input outside the cross deadzone is not filtered out + app.send_input(DualAxis::from_value( + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + 0.06, + 0.06, )); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(AxislikeTestAction::XY)); - assert_eq!(action_state.value(AxislikeTestAction::XY), 0.8); + assert_eq!(action_state.value(AxislikeTestAction::XY), 0.084852815); assert_eq!( action_state.axis_pair(AxislikeTestAction::XY).unwrap(), - DualAxisData::new(0.8, 0.0) + DualAxisData::new(0.06, 0.06) ); +} + +#[test] +fn game_pad_dual_axis_rect() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + DualAxis::left_stick().with_deadzone(DeadZoneShape::Rect { width: 0.1, height: 0.1 }), + AxislikeTestAction::XY, + )])); - // Test deadzones, assuming the default of 0.1. + // Test that an input inside the rect deadzone is filtered out, assuming values of 0.1 app.send_input(DualAxis::from_value( GamepadAxisType::LeftStickX, GamepadAxisType::LeftStickY, - 0.05, - 0.0, + 0.1, + 0.1, )); app.update(); @@ -253,23 +281,67 @@ fn game_pad_dual_axis() { DualAxisData::new(0.0, 0.0) ); - // Test that a single axis below the deadzone is filtered out, assuming the - // default deadzone of 0.1. + // Test that an input outside the rect deadzone is not filtered out, assuming values of 0.1 app.send_input(DualAxis::from_value( GamepadAxisType::LeftStickX, GamepadAxisType::LeftStickY, + 0.1, 0.2, - 0.05, )); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(AxislikeTestAction::XY)); - assert_eq!(action_state.value(AxislikeTestAction::XY), 0.2); + assert_eq!(action_state.value(AxislikeTestAction::XY), 0.22360681); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + DualAxisData::new(0.1, 0.2) + ); +} + +#[test] +fn game_pad_dual_axis_ellipse() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + DualAxis::left_stick().with_deadzone(DeadZoneShape::Ellipse { radius_x: 0.1, radius_y: 0.1 }), + AxislikeTestAction::XY, + )])); + + // Test that an input inside the ellipse deadzone is filtered out, assuming values of 0.1 + app.send_input(DualAxis::from_value( + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + 0.06, + 0.06, + )); + + app.update(); + + let action_state = app.world.resource::>(); + assert!(action_state.released(AxislikeTestAction::XY)); + assert_eq!(action_state.value(AxislikeTestAction::XY), 0.0); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + DualAxisData::new(0.0, 0.0) + ); + + // Test that an input outside the ellipse deadzone is not filtered out, assuming values of 0.1 + app.send_input(DualAxis::from_value( + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + 0.1, + 0.1, + )); + + app.update(); + + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::XY)); + assert_eq!(action_state.value(AxislikeTestAction::XY), 0.14142136); assert_eq!( action_state.axis_pair(AxislikeTestAction::XY).unwrap(), - DualAxisData::new(0.2, 0.0) + DualAxisData::new(0.1, 0.1) ); } From beed734d8e45deb9399553fe946c4867b7427309 Mon Sep 17 00:00:00 2001 From: 100-TomatoJuice Date: Tue, 1 Aug 2023 10:28:57 -0700 Subject: [PATCH 05/12] Improved docs + formatting --- src/axislike.rs | 11 +- src/input_streams.rs | 8 +- tests/gamepad_axis.rs | 749 +++++++++++++++++++++--------------------- 3 files changed, 394 insertions(+), 374 deletions(-) diff --git a/src/axislike.rs b/src/axislike.rs index 51fa233c..895c44da 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -705,9 +705,14 @@ impl From for Vec2 { } /// The shape of the deadzone for a [`DualAxis`] input. +/// +/// Input values that are on the line of the shape are counted as inside #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] pub enum DeadZoneShape { - /// Deadzone with the shape of a cross. The cross is represented by two rectangles. + /// Deadzone with the shape of a cross. + /// + /// The cross is represented by two rectangles. When using [`DeadZoneShape::Cross`], + /// make sure rect_1 and rect_2 do not have the same values, otherwise the shape will be a rectangle Cross { /// The width of the first rectangle. rect_1_width: f32, @@ -757,7 +762,9 @@ impl DeadZoneShape { *rect_2_height, ), DeadZoneShape::Rect { width, height } => self.outside_rectangle(x, y, *width, *height), - DeadZoneShape::Ellipse { radius_x, radius_y } => self.outside_ellipse(x, y, *radius_x, *radius_y), + DeadZoneShape::Ellipse { radius_x, radius_y } => { + self.outside_ellipse(x, y, *radius_x, *radius_y) + } } } diff --git a/src/input_streams.rs b/src/input_streams.rs index da42d02e..51e276a4 100644 --- a/src/input_streams.rs +++ b/src/input_streams.rs @@ -129,9 +129,11 @@ impl<'a> InputStreams<'a> { pub fn button_pressed(&self, button: InputKind) -> bool { match button { InputKind::DualAxis(axis) => { - let x_value = self.input_value(&UserInput::Single(InputKind::SingleAxis(axis.x)), false); - let y_value = self.input_value(&UserInput::Single(InputKind::SingleAxis(axis.y)), false); - + let x_value = + self.input_value(&UserInput::Single(InputKind::SingleAxis(axis.x)), false); + let y_value = + self.input_value(&UserInput::Single(InputKind::SingleAxis(axis.y)), false); + axis.deadzone.input_outside_deadzone(x_value, y_value) } InputKind::SingleAxis(axis) => { diff --git a/tests/gamepad_axis.rs b/tests/gamepad_axis.rs index 0d08d26e..c08f14de 100644 --- a/tests/gamepad_axis.rs +++ b/tests/gamepad_axis.rs @@ -1,369 +1,380 @@ -use bevy::input::gamepad::{ - GamepadAxisChangedEvent, GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadInfo, -}; -use bevy::input::InputPlugin; -use bevy::prelude::*; -use leafwing_input_manager::axislike::{AxisType, DualAxisData, DeadZoneShape}; -use leafwing_input_manager::prelude::*; - -#[derive(Actionlike, Clone, Copy, Debug, Reflect)] -enum ButtonlikeTestAction { - Up, - Down, - Left, - Right, -} - -#[derive(Actionlike, Clone, Copy, Debug, Reflect)] -enum AxislikeTestAction { - X, - Y, - XY, -} - -fn test_app() -> App { - let mut app = App::new(); - app.add_plugins(MinimalPlugins) - .add_plugins(InputPlugin) - .add_plugins(InputManagerPlugin::::default()) - .add_plugins(InputManagerPlugin::::default()) - .init_resource::>() - .init_resource::>(); - - // WARNING: you MUST register your gamepad during tests, or all gamepad input mocking will fail - let mut gamepad_events = app.world.resource_mut::>(); - gamepad_events.send(GamepadEvent::Connection(GamepadConnectionEvent { - // This MUST be consistent with any other mocked events - gamepad: Gamepad { id: 1 }, - connection: GamepadConnection::Connected(GamepadInfo { - name: "TestController".into(), - }), - })); - - // Ensure that the gamepad is picked up by the appropriate system - app.update(); - // Ensure that the connection event is flushed through - app.update(); - - app -} - -#[test] -fn raw_gamepad_axis_events() { - let mut app = test_app(); - app.insert_resource(InputMap::new([( - SingleAxis::symmetric(GamepadAxisType::RightStickX, 0.1), - ButtonlikeTestAction::Up, - )])); - - let mut events = app.world.resource_mut::>(); - events.send(GamepadEvent::Axis(GamepadAxisChangedEvent { - gamepad: Gamepad { id: 1 }, - axis_type: GamepadAxisType::RightStickX, - value: 1.0, - })); - - app.update(); - let action_state = app.world.resource::>(); - assert!(action_state.pressed(ButtonlikeTestAction::Up)); -} - -#[test] -fn game_pad_single_axis_mocking() { - let mut app = test_app(); - let mut events = app.world.resource_mut::>(); - assert_eq!(events.drain().count(), 0); - - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - }; - - app.send_input(input); - let mut events = app.world.resource_mut::>(); - assert_eq!(events.drain().count(), 1); -} - -#[test] -fn game_pad_dual_axis_mocking() { - let mut app = test_app(); - let mut events = app.world.resource_mut::>(); - assert_eq!(events.drain().count(), 0); - - let input = DualAxis { - x: SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - }, - y: SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - value: Some(0.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - }, - deadzone: DualAxis::DEFAULT_DEADZONE_SHAPE, - }; - app.send_input(input); - let mut events = app.world.resource_mut::>(); - // Dual axis events are split out - assert_eq!(events.drain().count(), 2); -} - -#[test] -fn game_pad_single_axis() { - let mut app = test_app(); - app.insert_resource(InputMap::new([ - ( - SingleAxis::symmetric(GamepadAxisType::LeftStickX, 0.1), - AxislikeTestAction::X, - ), - ( - SingleAxis::symmetric(GamepadAxisType::LeftStickY, 0.1), - AxislikeTestAction::Y, - ), - ])); - - // +X - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - }; - app.send_input(input); - app.update(); - let action_state = app.world.resource::>(); - assert!(action_state.pressed(AxislikeTestAction::X)); - - // -X - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - }; - app.send_input(input); - app.update(); - let action_state = app.world.resource::>(); - assert!(action_state.pressed(AxislikeTestAction::X)); - - // +Y - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - }; - app.send_input(input); - app.update(); - let action_state = app.world.resource::>(); - assert!(action_state.pressed(AxislikeTestAction::Y)); - - // -Y - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - }; - app.send_input(input); - app.update(); - let action_state = app.world.resource::>(); - assert!(action_state.pressed(AxislikeTestAction::Y)); - - // 0 - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - value: Some(0.0), - // Usually a small deadzone threshold will be set - positive_low: 0.1, - negative_low: 0.1, - inverted: false, - }; - app.send_input(input); - app.update(); - let action_state = app.world.resource::>(); - assert!(!action_state.pressed(AxislikeTestAction::Y)); - - // None - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - value: None, - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - }; - app.send_input(input); - app.update(); - let action_state = app.world.resource::>(); - assert!(!action_state.pressed(AxislikeTestAction::Y)); -} - -#[test] -fn game_pad_dual_axis_cross() { - let mut app = test_app(); - app.insert_resource(InputMap::new([( - DualAxis::left_stick().with_deadzone(DeadZoneShape::Cross { rect_1_width: 0.1, rect_1_height: 0.05, rect_2_width: 0.05, rect_2_height: 0.1 }), - AxislikeTestAction::XY, - )])); - - // Test that an input inside the cross deadzone is filtered out - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.1, - 0.05, - )); - - app.update(); - - let action_state = app.world.resource::>(); - assert!(action_state.released(AxislikeTestAction::XY)); - assert_eq!(action_state.value(AxislikeTestAction::XY), 0.0); - assert_eq!( - action_state.axis_pair(AxislikeTestAction::XY).unwrap(), - DualAxisData::new(0.0, 0.0) - ); - - // Test that an input outside the cross deadzone is not filtered out - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.06, - 0.06, - )); - - app.update(); - - let action_state = app.world.resource::>(); - assert!(action_state.pressed(AxislikeTestAction::XY)); - assert_eq!(action_state.value(AxislikeTestAction::XY), 0.084852815); - assert_eq!( - action_state.axis_pair(AxislikeTestAction::XY).unwrap(), - DualAxisData::new(0.06, 0.06) - ); -} - -#[test] -fn game_pad_dual_axis_rect() { - let mut app = test_app(); - app.insert_resource(InputMap::new([( - DualAxis::left_stick().with_deadzone(DeadZoneShape::Rect { width: 0.1, height: 0.1 }), - AxislikeTestAction::XY, - )])); - - // Test that an input inside the rect deadzone is filtered out, assuming values of 0.1 - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.1, - 0.1, - )); - - app.update(); - - let action_state = app.world.resource::>(); - assert!(action_state.released(AxislikeTestAction::XY)); - assert_eq!(action_state.value(AxislikeTestAction::XY), 0.0); - assert_eq!( - action_state.axis_pair(AxislikeTestAction::XY).unwrap(), - DualAxisData::new(0.0, 0.0) - ); - - // Test that an input outside the rect deadzone is not filtered out, assuming values of 0.1 - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.1, - 0.2, - )); - - app.update(); - - let action_state = app.world.resource::>(); - assert!(action_state.pressed(AxislikeTestAction::XY)); - assert_eq!(action_state.value(AxislikeTestAction::XY), 0.22360681); - assert_eq!( - action_state.axis_pair(AxislikeTestAction::XY).unwrap(), - DualAxisData::new(0.1, 0.2) - ); -} - -#[test] -fn game_pad_dual_axis_ellipse() { - let mut app = test_app(); - app.insert_resource(InputMap::new([( - DualAxis::left_stick().with_deadzone(DeadZoneShape::Ellipse { radius_x: 0.1, radius_y: 0.1 }), - AxislikeTestAction::XY, - )])); - - // Test that an input inside the ellipse deadzone is filtered out, assuming values of 0.1 - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.06, - 0.06, - )); - - app.update(); - - let action_state = app.world.resource::>(); - assert!(action_state.released(AxislikeTestAction::XY)); - assert_eq!(action_state.value(AxislikeTestAction::XY), 0.0); - assert_eq!( - action_state.axis_pair(AxislikeTestAction::XY).unwrap(), - DualAxisData::new(0.0, 0.0) - ); - - // Test that an input outside the ellipse deadzone is not filtered out, assuming values of 0.1 - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.1, - 0.1, - )); - - app.update(); - - let action_state = app.world.resource::>(); - assert!(action_state.pressed(AxislikeTestAction::XY)); - assert_eq!(action_state.value(AxislikeTestAction::XY), 0.14142136); - assert_eq!( - action_state.axis_pair(AxislikeTestAction::XY).unwrap(), - DualAxisData::new(0.1, 0.1) - ); -} - -#[test] -fn game_pad_virtualdpad() { - let mut app = test_app(); - app.insert_resource(InputMap::new([( - VirtualDPad::dpad(), - AxislikeTestAction::XY, - )])); - - app.send_input(GamepadButtonType::DPadLeft); - app.update(); - - let action_state = app.world.resource::>(); - - assert!(action_state.pressed(AxislikeTestAction::XY)); - // This should be unit length, because we're working with a VirtualDpad - assert_eq!(action_state.value(AxislikeTestAction::XY), 1.0); - assert_eq!( - action_state.axis_pair(AxislikeTestAction::XY).unwrap(), - // This should be unit length, because we're working with a VirtualDpad - DualAxisData::new(-1.0, 0.0) - ); -} +use bevy::input::gamepad::{ + GamepadAxisChangedEvent, GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadInfo, +}; +use bevy::input::InputPlugin; +use bevy::prelude::*; +use leafwing_input_manager::axislike::{AxisType, DeadZoneShape, DualAxisData}; +use leafwing_input_manager::prelude::*; + +#[derive(Actionlike, Clone, Copy, Debug, Reflect)] +enum ButtonlikeTestAction { + Up, + Down, + Left, + Right, +} + +#[derive(Actionlike, Clone, Copy, Debug, Reflect)] +enum AxislikeTestAction { + X, + Y, + XY, +} + +fn test_app() -> App { + let mut app = App::new(); + app.add_plugins(MinimalPlugins) + .add_plugins(InputPlugin) + .add_plugins(InputManagerPlugin::::default()) + .add_plugins(InputManagerPlugin::::default()) + .init_resource::>() + .init_resource::>(); + + // WARNING: you MUST register your gamepad during tests, or all gamepad input mocking will fail + let mut gamepad_events = app.world.resource_mut::>(); + gamepad_events.send(GamepadEvent::Connection(GamepadConnectionEvent { + // This MUST be consistent with any other mocked events + gamepad: Gamepad { id: 1 }, + connection: GamepadConnection::Connected(GamepadInfo { + name: "TestController".into(), + }), + })); + + // Ensure that the gamepad is picked up by the appropriate system + app.update(); + // Ensure that the connection event is flushed through + app.update(); + + app +} + +#[test] +fn raw_gamepad_axis_events() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + SingleAxis::symmetric(GamepadAxisType::RightStickX, 0.1), + ButtonlikeTestAction::Up, + )])); + + let mut events = app.world.resource_mut::>(); + events.send(GamepadEvent::Axis(GamepadAxisChangedEvent { + gamepad: Gamepad { id: 1 }, + axis_type: GamepadAxisType::RightStickX, + value: 1.0, + })); + + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(ButtonlikeTestAction::Up)); +} + +#[test] +fn game_pad_single_axis_mocking() { + let mut app = test_app(); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 0); + + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + inverted: false, + }; + + app.send_input(input); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 1); +} + +#[test] +fn game_pad_dual_axis_mocking() { + let mut app = test_app(); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 0); + + let input = DualAxis { + x: SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + inverted: false, + }, + y: SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + value: Some(0.), + positive_low: 0.0, + negative_low: 0.0, + inverted: false, + }, + deadzone: DualAxis::DEFAULT_DEADZONE_SHAPE, + }; + app.send_input(input); + let mut events = app.world.resource_mut::>(); + // Dual axis events are split out + assert_eq!(events.drain().count(), 2); +} + +#[test] +fn game_pad_single_axis() { + let mut app = test_app(); + app.insert_resource(InputMap::new([ + ( + SingleAxis::symmetric(GamepadAxisType::LeftStickX, 0.1), + AxislikeTestAction::X, + ), + ( + SingleAxis::symmetric(GamepadAxisType::LeftStickY, 0.1), + AxislikeTestAction::Y, + ), + ])); + + // +X + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + inverted: false, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::X)); + + // -X + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + inverted: false, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::X)); + + // +Y + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + inverted: false, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::Y)); + + // -Y + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + inverted: false, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::Y)); + + // 0 + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + value: Some(0.0), + // Usually a small deadzone threshold will be set + positive_low: 0.1, + negative_low: 0.1, + inverted: false, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(!action_state.pressed(AxislikeTestAction::Y)); + + // None + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + value: None, + positive_low: 0.0, + negative_low: 0.0, + inverted: false, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(!action_state.pressed(AxislikeTestAction::Y)); +} + +#[test] +fn game_pad_dual_axis_cross() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + DualAxis::left_stick().with_deadzone(DeadZoneShape::Cross { + rect_1_width: 0.1, + rect_1_height: 0.05, + rect_2_width: 0.05, + rect_2_height: 0.1, + }), + AxislikeTestAction::XY, + )])); + + // Test that an input inside the cross deadzone is filtered out + app.send_input(DualAxis::from_value( + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + 0.1, + 0.05, + )); + + app.update(); + + let action_state = app.world.resource::>(); + assert!(action_state.released(AxislikeTestAction::XY)); + assert_eq!(action_state.value(AxislikeTestAction::XY), 0.0); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + DualAxisData::new(0.0, 0.0) + ); + + // Test that an input outside the cross deadzone is not filtered out + app.send_input(DualAxis::from_value( + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + 0.06, + 0.06, + )); + + app.update(); + + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::XY)); + assert_eq!(action_state.value(AxislikeTestAction::XY), 0.084852815); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + DualAxisData::new(0.06, 0.06) + ); +} + +#[test] +fn game_pad_dual_axis_rect() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + DualAxis::left_stick().with_deadzone(DeadZoneShape::Rect { + width: 0.1, + height: 0.1, + }), + AxislikeTestAction::XY, + )])); + + // Test that an input inside the rect deadzone is filtered out, assuming values of 0.1 + app.send_input(DualAxis::from_value( + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + 0.1, + 0.1, + )); + + app.update(); + + let action_state = app.world.resource::>(); + assert!(action_state.released(AxislikeTestAction::XY)); + assert_eq!(action_state.value(AxislikeTestAction::XY), 0.0); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + DualAxisData::new(0.0, 0.0) + ); + + // Test that an input outside the rect deadzone is not filtered out, assuming values of 0.1 + app.send_input(DualAxis::from_value( + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + 0.1, + 0.2, + )); + + app.update(); + + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::XY)); + assert_eq!(action_state.value(AxislikeTestAction::XY), 0.22360681); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + DualAxisData::new(0.1, 0.2) + ); +} + +#[test] +fn game_pad_dual_axis_ellipse() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + DualAxis::left_stick().with_deadzone(DeadZoneShape::Ellipse { + radius_x: 0.1, + radius_y: 0.1, + }), + AxislikeTestAction::XY, + )])); + + // Test that an input inside the ellipse deadzone is filtered out, assuming values of 0.1 + app.send_input(DualAxis::from_value( + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + 0.06, + 0.06, + )); + + app.update(); + + let action_state = app.world.resource::>(); + assert!(action_state.released(AxislikeTestAction::XY)); + assert_eq!(action_state.value(AxislikeTestAction::XY), 0.0); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + DualAxisData::new(0.0, 0.0) + ); + + // Test that an input outside the ellipse deadzone is not filtered out, assuming values of 0.1 + app.send_input(DualAxis::from_value( + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + 0.1, + 0.1, + )); + + app.update(); + + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::XY)); + assert_eq!(action_state.value(AxislikeTestAction::XY), 0.14142136); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + DualAxisData::new(0.1, 0.1) + ); +} + +#[test] +fn game_pad_virtualdpad() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + VirtualDPad::dpad(), + AxislikeTestAction::XY, + )])); + + app.send_input(GamepadButtonType::DPadLeft); + app.update(); + + let action_state = app.world.resource::>(); + + assert!(action_state.pressed(AxislikeTestAction::XY)); + // This should be unit length, because we're working with a VirtualDpad + assert_eq!(action_state.value(AxislikeTestAction::XY), 1.0); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + // This should be unit length, because we're working with a VirtualDpad + DualAxisData::new(-1.0, 0.0) + ); +} From 92334f8cd2e923d154be998116458498370c1b93 Mon Sep 17 00:00:00 2001 From: 100-TomatoJuice Date: Tue, 1 Aug 2023 10:35:34 -0700 Subject: [PATCH 06/12] Impl `Hash` for `DeadZoneShape` --- src/axislike.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/axislike.rs b/src/axislike.rs index 895c44da..b3191c8a 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -741,7 +741,29 @@ pub enum DeadZoneShape { impl Eq for DeadZoneShape {} impl std::hash::Hash for DeadZoneShape { - fn hash(&self, state: &mut H) {} + fn hash(&self, state: &mut H) { + match self { + DeadZoneShape::Cross { + rect_1_width, + rect_1_height, + rect_2_width, + rect_2_height, + } => { + FloatOrd(*rect_1_width).hash(state); + FloatOrd(*rect_1_height).hash(state); + FloatOrd(*rect_2_width).hash(state); + FloatOrd(*rect_2_height).hash(state); + } + DeadZoneShape::Rect { width, height } => { + FloatOrd(*width).hash(state); + FloatOrd(*height).hash(state); + } + DeadZoneShape::Ellipse { radius_x, radius_y } => { + FloatOrd(*radius_x).hash(state); + FloatOrd(*radius_y).hash(state); + } + } + } } impl DeadZoneShape { From b8f76836b7cfea26d5b91b67f104e17768778add Mon Sep 17 00:00:00 2001 From: 100-TomatoJuice Date: Tue, 1 Aug 2023 10:49:53 -0700 Subject: [PATCH 07/12] Update RELEASES.md --- RELEASES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASES.md b/RELEASES.md index 3692bb05..44120cd2 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,6 +6,9 @@ - Fixed invalid example code in README +### Enhancements +- Added `DeadZoneShape` for `DualAxis` which allows for different deadzones shapes: cross, rectangle, and ellipse. + ## Version 0.10 ### Usability From 7c1dfbf753e8655ac0c74848b4b5e310378e4fa9 Mon Sep 17 00:00:00 2001 From: 100-TomatoJuice Date: Tue, 1 Aug 2023 18:52:05 -0700 Subject: [PATCH 08/12] Simplify ellipse formula --- src/axislike.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axislike.rs b/src/axislike.rs index b3191c8a..bd7b92ad 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -811,6 +811,6 @@ impl DeadZoneShape { /// Returns whether the (x, y) input is outside an ellipse. fn outside_ellipse(&self, x: f32, y: f32, radius_x: f32, radius_y: f32) -> bool { - (x.powi(2) / radius_x.powi(2) + y.powi(2) / radius_y.powi(2)) > 1.0 + ((x / radius_x).powi(2) + (y / radius_y).powi(2)) > 1.0 } } From 870dc5624bb95040192867b5a17a6bb753c5db6a Mon Sep 17 00:00:00 2001 From: 100-TomatoJuice Date: Wed, 2 Aug 2023 18:02:02 -0700 Subject: [PATCH 09/12] Change default deadzone to circle --- src/axislike.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/axislike.rs b/src/axislike.rs index bd7b92ad..88004553 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -199,11 +199,9 @@ impl DualAxis { /// The default shape of the deadzone used by constructor methods. /// /// This cannot be changed, but the struct can be easily manually constructed. - pub const DEFAULT_DEADZONE_SHAPE: DeadZoneShape = DeadZoneShape::Cross { - rect_1_width: Self::DEFAULT_DEADZONE / 2.0, - rect_1_height: Self::DEFAULT_DEADZONE, - rect_2_width: Self::DEFAULT_DEADZONE, - rect_2_height: Self::DEFAULT_DEADZONE / 2.0, + pub const DEFAULT_DEADZONE_SHAPE: DeadZoneShape = DeadZoneShape::Ellipse { + radius_x: Self::DEFAULT_DEADZONE, + radius_y: Self::DEFAULT_DEADZONE, }; /// Creates a [`DualAxis`] with both `positive_low` and `negative_low` in both axes set to `threshold` with a `deadzone_shape`. From 17ec32232f9d530fecee294a793e6c55d783e06f Mon Sep 17 00:00:00 2001 From: 100-TomatoJuice Date: Thu, 3 Aug 2023 11:25:25 -0700 Subject: [PATCH 10/12] Add DeadZoneShape to prelude --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 5178cc77..76fec483 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,7 @@ pub use leafwing_input_manager_macros::Actionlike; /// Everything you need to get started pub mod prelude { pub use crate::action_state::{ActionState, ActionStateDriver}; - pub use crate::axislike::{DualAxis, MouseWheelAxisType, SingleAxis, VirtualDPad}; + pub use crate::axislike::{DualAxis, MouseWheelAxisType, SingleAxis, VirtualDPad, DeadZoneShape}; pub use crate::buttonlike::MouseWheelDirection; pub use crate::clashing_inputs::ClashStrategy; pub use crate::input_map::InputMap; From ae11e3a06c44cd31f746b25c138ceff32de4e6b9 Mon Sep 17 00:00:00 2001 From: 100-TomatoJuice Date: Fri, 4 Aug 2023 09:35:06 -0700 Subject: [PATCH 11/12] Cargo fmt --- src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 76fec483..ed500e20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,9 @@ pub use leafwing_input_manager_macros::Actionlike; /// Everything you need to get started pub mod prelude { pub use crate::action_state::{ActionState, ActionStateDriver}; - pub use crate::axislike::{DualAxis, MouseWheelAxisType, SingleAxis, VirtualDPad, DeadZoneShape}; + pub use crate::axislike::{ + DeadZoneShape, DualAxis, MouseWheelAxisType, SingleAxis, VirtualDPad, + }; pub use crate::buttonlike::MouseWheelDirection; pub use crate::clashing_inputs::ClashStrategy; pub use crate::input_map::InputMap; From 37f64772ed7322de22ca12dda7a9a374f935cb9b Mon Sep 17 00:00:00 2001 From: 100-TomatoJuice Date: Fri, 4 Aug 2023 13:04:12 -0700 Subject: [PATCH 12/12] Box `Chord(PetitSet)` per clippy suggestion --- src/user_input.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/user_input.rs b/src/user_input.rs index a60b8cd4..27376542 100644 --- a/src/user_input.rs +++ b/src/user_input.rs @@ -26,7 +26,7 @@ pub enum UserInput { /// A combination of buttons, pressed simultaneously /// /// Up to 8 (!!) buttons can be chorded together at once. - Chord(PetitSet), + Chord(Box>), /// A virtual DPad that you can get an [`DualAxis`] from VirtualDPad(VirtualDPad), /// A virtual axis that you can get a [`SingleAxis`] from @@ -44,7 +44,7 @@ impl UserInput { set.insert(modifier); set.insert(input); - UserInput::Chord(set) + UserInput::Chord(Box::new(set)) } /// Creates a [`UserInput::Chord`] from an iterator of inputs of the same type that can be converted into an [`InputKind`]s @@ -62,7 +62,7 @@ impl UserInput { match length { 1 => UserInput::Single(set.into_iter().next().unwrap()), - _ => UserInput::Chord(set), + _ => UserInput::Chord(Box::new(set)), } }