diff --git a/RELEASES.md b/RELEASES.md index d844f9bc..1081aba2 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -12,6 +12,7 @@ ### Enhancements - Added `DeadZoneShape` for `DualAxis` which allows for different deadzones shapes: cross, rectangle, and ellipse. +- Added sensitivity for `SingleAxis` and `DualAxis`, allowing you to scale mouse, keypad and gamepad inputs differently for each action. ## Version 0.10 diff --git a/src/axislike.rs b/src/axislike.rs index 6784dc59..f9ee87e3 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -30,6 +30,12 @@ pub struct SingleAxis { pub negative_low: f32, /// Whether to invert output values from this axis. pub inverted: bool, + /// How sensitive the axis is to input values. + /// + /// Since sensitivity is a multiplier, any value `>1.0` will increase sensitivity while any value `<1.0` will decrease sensitivity. + /// This value should always be strictly positive: a value of 0 will cause the axis to stop functioning, + /// while negative values will invert the direction. + pub sensitivity: f32, /// The target value for this input, used for input mocking. /// /// WARNING: this field is ignored for the sake of [`Eq`] and [`Hash`](std::hash::Hash) @@ -45,6 +51,7 @@ impl SingleAxis { positive_low: threshold, negative_low: -threshold, inverted: false, + sensitivity: 1.0, value: None, } } @@ -60,6 +67,7 @@ impl SingleAxis { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, value: Some(value), } } @@ -72,6 +80,7 @@ impl SingleAxis { positive_low: 0., negative_low: 0., inverted: false, + sensitivity: 1.0, value: None, } } @@ -84,6 +93,7 @@ impl SingleAxis { positive_low: 0., negative_low: 0., inverted: false, + sensitivity: 1.0, value: None, } } @@ -96,6 +106,7 @@ impl SingleAxis { positive_low: 0., negative_low: 0., inverted: false, + sensitivity: 1.0, value: None, } } @@ -108,6 +119,7 @@ impl SingleAxis { positive_low: 0., negative_low: 0., inverted: false, + sensitivity: 1.0, value: None, } } @@ -121,6 +133,7 @@ impl SingleAxis { negative_low: threshold, positive_low: f32::MAX, inverted: false, + sensitivity: 1.0, value: None, } } @@ -134,6 +147,7 @@ impl SingleAxis { negative_low: f32::MIN, positive_low: threshold, inverted: false, + sensitivity: 1.0, value: None, } } @@ -146,6 +160,13 @@ impl SingleAxis { self } + /// Returns this [`SingleAxis`] with the sensitivity set to the specified value + #[must_use] + pub fn with_sensitivity(mut self, sensitivity: f32) -> SingleAxis { + self.sensitivity = sensitivity; + self + } + /// Returns this [`SingleAxis`] inverted. #[must_use] pub fn inverted(mut self) -> Self { @@ -159,6 +180,7 @@ impl PartialEq for SingleAxis { self.axis_type == other.axis_type && FloatOrd(self.positive_low) == FloatOrd(other.positive_low) && FloatOrd(self.negative_low) == FloatOrd(other.negative_low) + && FloatOrd(self.sensitivity) == FloatOrd(other.sensitivity) } } impl Eq for SingleAxis {} @@ -167,6 +189,7 @@ impl std::hash::Hash for SingleAxis { self.axis_type.hash(state); FloatOrd(self.positive_low).hash(state); FloatOrd(self.negative_low).hash(state); + FloatOrd(self.sensitivity).hash(state); } } @@ -281,6 +304,14 @@ impl DualAxis { self } + /// Returns this [`DualAxis`] with the sensitivity set to the specified values + #[must_use] + pub fn with_sensitivity(mut self, x_sensitivity: f32, y_sensitivity: f32) -> DualAxis { + self.x.sensitivity = x_sensitivity; + self.y.sensitivity = y_sensitivity; + self + } + /// Returns this [`DualAxis`] with an inverted X-axis. #[must_use] pub fn inverted_x(mut self) -> DualAxis { diff --git a/src/input_map.rs b/src/input_map.rs index 862235ff..9d59be33 100644 --- a/src/input_map.rs +++ b/src/input_map.rs @@ -19,60 +19,101 @@ use std::collections::HashMap; use std::hash::Hash; use std::marker::PhantomData; -/// Maps from raw inputs to an input-method agnostic representation -/// -/// Multiple inputs can be mapped to the same action, -/// and each input can be mapped to multiple actions. -/// -/// The provided input types must be able to be converted into a [`UserInput`]. -/// -/// The maximum number of bindings (total) that can be stored for each action is 16. -/// Insertions will silently fail if you have reached this cap. -/// -/// By default, if two actions would be triggered by a combination of buttons, -/// and one combination is a strict subset of the other, only the larger input is registered. -/// For example, pressing both `S` and `Ctrl + S` in your text editor app would save your file, -/// but not enter the letters `s`. -/// Set the [`ClashStrategy`](crate::clashing_inputs::ClashStrategy) resource -/// to configure this behavior. -/// -/// # Example -/// ```rust -/// use bevy::prelude::*; -/// use leafwing_input_manager::prelude::*; -/// use leafwing_input_manager::user_input::InputKind; -/// -/// // You can Run! -/// // But you can't Hide :( -/// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] -/// enum Action { -/// Run, -/// Hide, -/// } -/// -/// // Construction -/// let mut input_map = InputMap::new([ -/// // Note that the type of your iterators must be homogenous; -/// // you can use `InputKind` or `UserInput` if needed -/// // as unifying types -/// (GamepadButtonType::South, Action::Run), -/// (GamepadButtonType::LeftTrigger, Action::Hide), -/// (GamepadButtonType::RightTrigger, Action::Hide), -/// ]); -/// -/// // Insertion -/// input_map.insert(MouseButton::Left, Action::Run) -/// .insert(KeyCode::ShiftLeft, Action::Run) -/// // Chords -/// .insert_modified(Modifier::Control, KeyCode::R, Action::Run) -/// .insert_chord([InputKind::Keyboard(KeyCode::H), -/// InputKind::GamepadButton(GamepadButtonType::South), -/// InputKind::Mouse(MouseButton::Middle)], -/// Action::Run); -/// -/// // Removal -/// input_map.clear_action(Action::Hide); -///``` +/** +Maps from raw inputs to an input-method agnostic representation + +Multiple inputs can be mapped to the same action, +and each input can be mapped to multiple actions. + +The provided input types must be able to be converted into a [`UserInput`]. + +The maximum number of bindings (total) that can be stored for each action is 16. +Insertions will silently fail if you have reached this cap. + +By default, if two actions would be triggered by a combination of buttons, +and one combination is a strict subset of the other, only the larger input is registered. +For example, pressing both `S` and `Ctrl + S` in your text editor app would save your file, +but not enter the letters `s`. +Set the [`ClashStrategy`](crate::clashing_inputs::ClashStrategy) resource +to configure this behavior. + +# Example +```rust +use bevy::prelude::*; +use leafwing_input_manager::prelude::*; +use leafwing_input_manager::user_input::InputKind; + +// You can Run! +// But you can't Hide :( +#[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] +enum Action { + Run, + Hide, +} + +// Construction +let mut input_map = InputMap::new([ + // Note that the type of your iterators must be homogenous; + // you can use `InputKind` or `UserInput` if needed + // as unifying types + (GamepadButtonType::South, Action::Run), + (GamepadButtonType::LeftTrigger, Action::Hide), + (GamepadButtonType::RightTrigger, Action::Hide), +]); + +// Insertion +input_map.insert(MouseButton::Left, Action::Run) +.insert(KeyCode::ShiftLeft, Action::Run) +// Chords +.insert_modified(Modifier::Control, KeyCode::R, Action::Run) +.insert_chord([InputKind::Keyboard(KeyCode::H), + InputKind::GamepadButton(GamepadButtonType::South), + InputKind::Mouse(MouseButton::Middle)], + Action::Run); + +// Removal +input_map.clear_action(Action::Hide); +``` + +# Example +```rust +use bevy::prelude::*; +use leafwing_input_manager::prelude::*; +use leafwing_input_manager::user_input::InputKind; + +#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)] +enum Action { + Look, +} + +fn spawn_player(mut commands: Commands){ + commands.spawn(InputManagerBundle:: { + action_state: ActionState::default(), + input_map: InputMap::default() + .insert(DualAxis::left_stick().with_sensitivity(1.0, 1.0), Action::Look) + .insert(DualAxis::mouse_motion().with_sensitivity(1.0, 1.0), Action::Look) + .build(), + }); +} + +fn change_left_stick_values(mut query: Query<&mut InputMap>){ + let mut input_map = query.single_mut(); + + // Get the input at the 0 index since the left stick was added first in `Action::Look` + let input = input_map.get_mut(Action::Look).get_at_mut(0).unwrap(); + + // Some pattern matching is needed to get to the `DualAxis` + if let UserInput::Single(kind) = input{ + if let InputKind::DualAxis(dual_axis) = kind{ + // Here any value of the left stick `DualAxis` can be changed + dual_axis.x.sensitivity = 0.8; + dual_axis.y.sensitivity = 0.8; + dual_axis.deadzone = DeadZoneShape::Rect { width: 1.0, height: 1.0 } + } + } +} +``` +**/ #[derive(Resource, Component, Debug, Clone, PartialEq, Eq, TypeUuid)] #[uuid = "D7DECC78-8573-42FF-851A-F0344C7D05C9"] pub struct InputMap { @@ -425,6 +466,12 @@ impl InputMap { &self.map[action.index()] } + /// Returns the `action` mappings + #[must_use] + pub fn get_mut(&mut self, action: A) -> &mut PetitSet { + &mut self.map[action.index()] + } + /// How many input bindings are registered total? #[must_use] pub fn len(&self) -> usize { diff --git a/src/input_streams.rs b/src/input_streams.rs index 51e276a4..f557dd72 100644 --- a/src/input_streams.rs +++ b/src/input_streams.rs @@ -266,9 +266,9 @@ impl<'a> InputStreams<'a> { if value >= axis.negative_low && value <= axis.positive_low && include_deadzone { 0.0 } else if axis.inverted { - -value + -value * axis.sensitivity } else { - value + value * axis.sensitivity } }; diff --git a/tests/gamepad_axis.rs b/tests/gamepad_axis.rs index c08f14de..cbababf4 100644 --- a/tests/gamepad_axis.rs +++ b/tests/gamepad_axis.rs @@ -79,6 +79,7 @@ fn game_pad_single_axis_mocking() { value: Some(-1.), positive_low: 0.0, negative_low: 0.0, + sensitivity: 1.0, inverted: false, }; @@ -99,6 +100,7 @@ fn game_pad_dual_axis_mocking() { value: Some(1.), positive_low: 0.0, negative_low: 0.0, + sensitivity: 1.0, inverted: false, }, y: SingleAxis { @@ -106,6 +108,7 @@ fn game_pad_dual_axis_mocking() { value: Some(0.), positive_low: 0.0, negative_low: 0.0, + sensitivity: 1.0, inverted: false, }, deadzone: DualAxis::DEFAULT_DEADZONE_SHAPE, @@ -137,6 +140,7 @@ fn game_pad_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -150,6 +154,7 @@ fn game_pad_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -163,6 +168,7 @@ fn game_pad_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -176,6 +182,7 @@ fn game_pad_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -190,6 +197,7 @@ fn game_pad_single_axis() { positive_low: 0.1, negative_low: 0.1, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -203,6 +211,7 @@ fn game_pad_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -210,6 +219,82 @@ fn game_pad_single_axis() { assert!(!action_state.pressed(AxislikeTestAction::Y)); } +#[test] +fn game_pad_single_axis_inverted() { + let mut app = test_app(); + app.insert_resource(InputMap::new([ + ( + SingleAxis::symmetric(GamepadAxisType::LeftStickX, 0.1).inverted(), + AxislikeTestAction::X, + ), + ( + SingleAxis::symmetric(GamepadAxisType::LeftStickY, 0.1).inverted(), + AxislikeTestAction::Y, + ), + ])); + + // +X + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + inverted: true, + sensitivity: -1.0, + } + .inverted(); + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::X)); + assert!(action_state.value(AxislikeTestAction::X) == -1.0); + + // -X + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + inverted: true, + sensitivity: -1.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::X)); + assert!(action_state.value(AxislikeTestAction::X) == 1.0); + + // +Y + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + inverted: true, + sensitivity: -1.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::Y)); + assert!(action_state.value(AxislikeTestAction::Y) == -1.0); + + // -Y + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + inverted: true, + sensitivity: -1.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::Y)); + assert!(action_state.value(AxislikeTestAction::Y) == 1.0); +} + #[test] fn game_pad_dual_axis_cross() { let mut app = test_app(); diff --git a/tests/mouse_motion.rs b/tests/mouse_motion.rs index 9a5ccc51..fd8a575b 100644 --- a/tests/mouse_motion.rs +++ b/tests/mouse_motion.rs @@ -75,6 +75,7 @@ fn mouse_motion_single_axis_mocking() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); @@ -95,6 +96,7 @@ fn mouse_motion_dual_axis_mocking() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }, y: SingleAxis { axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), @@ -102,6 +104,7 @@ fn mouse_motion_dual_axis_mocking() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }, deadzone: DualAxis::DEFAULT_DEADZONE_SHAPE, }; @@ -171,6 +174,7 @@ fn mouse_motion_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -184,6 +188,7 @@ fn mouse_motion_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -197,6 +202,7 @@ fn mouse_motion_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -210,6 +216,7 @@ fn mouse_motion_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -224,6 +231,7 @@ fn mouse_motion_single_axis() { positive_low: 0.1, negative_low: 0.1, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -237,6 +245,7 @@ fn mouse_motion_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); diff --git a/tests/mouse_wheel.rs b/tests/mouse_wheel.rs index e43aa568..72f46fcf 100644 --- a/tests/mouse_wheel.rs +++ b/tests/mouse_wheel.rs @@ -76,6 +76,7 @@ fn mouse_wheel_single_axis_mocking() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); @@ -96,6 +97,7 @@ fn mouse_wheel_dual_axis_mocking() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }, y: SingleAxis { axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), @@ -103,6 +105,7 @@ fn mouse_wheel_dual_axis_mocking() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }, deadzone: DualAxis::DEFAULT_DEADZONE_SHAPE, }; @@ -172,6 +175,7 @@ fn mouse_wheel_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -185,6 +189,7 @@ fn mouse_wheel_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -198,6 +203,7 @@ fn mouse_wheel_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -211,6 +217,7 @@ fn mouse_wheel_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -225,6 +232,7 @@ fn mouse_wheel_single_axis() { positive_low: 0.1, negative_low: 0.1, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update(); @@ -238,6 +246,7 @@ fn mouse_wheel_single_axis() { positive_low: 0.0, negative_low: 0.0, inverted: false, + sensitivity: 1.0, }; app.send_input(input); app.update();