Scale input to account for deadzones (#17015)

# Objective

Fixes #3450 

## Solution

Scale the input to account for the range

## Testing

Updated unit tests

## Migration Guide

`GamepadButtonChangedEvent.value` is now linearly rescaled to be from
`0.0..=1.0` (instead of `low..=high`) and
`GamepadAxisChangedEvent.value` is now linearly rescaled to be from
`-1.0..=0.0`/`0.0..=1.0` (accounting for the deadzone).
This commit is contained in:
Benjamin Brienen 2025-01-03 17:27:59 -05:00 committed by GitHub
parent 120b733ab5
commit 43db44ca3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,5 +1,7 @@
//! The gamepad input functionality. //! The gamepad input functionality.
use core::ops::RangeInclusive;
use crate::{Axis, ButtonInput, ButtonState}; use crate::{Axis, ButtonInput, ButtonState};
use alloc::string::String; use alloc::string::String;
#[cfg(feature = "bevy_reflect")] #[cfg(feature = "bevy_reflect")]
@ -30,7 +32,7 @@ use thiserror::Error;
/// [`GamepadButtonChangedEvent`] and [`GamepadAxisChangedEvent`] when /// [`GamepadButtonChangedEvent`] and [`GamepadAxisChangedEvent`] when
/// the in-frame relative ordering of events is important. /// the in-frame relative ordering of events is important.
/// ///
/// This event is produced by `bevy_input` /// This event is produced by `bevy_input`.
#[derive(Event, Debug, Clone, PartialEq, From)] #[derive(Event, Debug, Clone, PartialEq, From)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -70,7 +72,7 @@ pub enum RawGamepadEvent {
Axis(RawGamepadAxisChangedEvent), Axis(RawGamepadAxisChangedEvent),
} }
/// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`] /// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`].
#[derive(Event, Debug, Copy, Clone, PartialEq)] #[derive(Event, Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -98,7 +100,7 @@ impl RawGamepadButtonChangedEvent {
} }
} }
/// [`GamepadAxis`] changed event unfiltered by [`GamepadSettings`] /// [`GamepadAxis`] changed event unfiltered by [`GamepadSettings`].
#[derive(Event, Debug, Copy, Clone, PartialEq)] #[derive(Event, Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -151,18 +153,18 @@ impl GamepadConnectionEvent {
} }
} }
/// Is the gamepad connected? /// Whether the gamepad is connected.
pub fn connected(&self) -> bool { pub fn connected(&self) -> bool {
matches!(self.connection, GamepadConnection::Connected { .. }) matches!(self.connection, GamepadConnection::Connected { .. })
} }
/// Is the gamepad disconnected? /// Whether the gamepad is disconnected.
pub fn disconnected(&self) -> bool { pub fn disconnected(&self) -> bool {
!self.connected() !self.connected()
} }
} }
/// [`GamepadButton`] event triggered by a digital state change /// [`GamepadButton`] event triggered by a digital state change.
#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] #[derive(Event, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -180,7 +182,7 @@ pub struct GamepadButtonStateChangedEvent {
} }
impl GamepadButtonStateChangedEvent { impl GamepadButtonStateChangedEvent {
/// Creates a new [`GamepadButtonStateChangedEvent`] /// Creates a new [`GamepadButtonStateChangedEvent`].
pub fn new(entity: Entity, button: GamepadButton, state: ButtonState) -> Self { pub fn new(entity: Entity, button: GamepadButton, state: ButtonState) -> Self {
Self { Self {
entity, entity,
@ -190,7 +192,7 @@ impl GamepadButtonStateChangedEvent {
} }
} }
/// [`GamepadButton`] event triggered by an analog state change /// [`GamepadButton`] event triggered by an analog state change.
#[derive(Event, Debug, Clone, Copy, PartialEq)] #[derive(Event, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -205,12 +207,12 @@ pub struct GamepadButtonChangedEvent {
pub button: GamepadButton, pub button: GamepadButton,
/// The pressed state of the button. /// The pressed state of the button.
pub state: ButtonState, pub state: ButtonState,
/// The analog value of the button. /// The analog value of the button (rescaled to be in the 0.0..=1.0 range).
pub value: f32, pub value: f32,
} }
impl GamepadButtonChangedEvent { impl GamepadButtonChangedEvent {
/// Creates a new [`GamepadButtonChangedEvent`] /// Creates a new [`GamepadButtonChangedEvent`].
pub fn new(entity: Entity, button: GamepadButton, state: ButtonState, value: f32) -> Self { pub fn new(entity: Entity, button: GamepadButton, state: ButtonState, value: f32) -> Self {
Self { Self {
entity, entity,
@ -221,7 +223,7 @@ impl GamepadButtonChangedEvent {
} }
} }
/// [`GamepadAxis`] event triggered by an analog state change /// [`GamepadAxis`] event triggered by an analog state change.
#[derive(Event, Debug, Clone, Copy, PartialEq)] #[derive(Event, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
@ -234,12 +236,12 @@ pub struct GamepadAxisChangedEvent {
pub entity: Entity, pub entity: Entity,
/// The gamepad axis assigned to the event. /// The gamepad axis assigned to the event.
pub axis: GamepadAxis, pub axis: GamepadAxis,
/// The value of this axis. /// The value of this axis (rescaled to account for axis settings).
pub value: f32, pub value: f32,
} }
impl GamepadAxisChangedEvent { impl GamepadAxisChangedEvent {
/// Creates a new [`GamepadAxisChangedEvent`] /// Creates a new [`GamepadAxisChangedEvent`].
pub fn new(entity: Entity, axis: GamepadAxis, value: f32) -> Self { pub fn new(entity: Entity, axis: GamepadAxis, value: f32) -> Self {
Self { Self {
entity, entity,
@ -339,12 +341,10 @@ pub struct Gamepad {
/// The USB vendor ID as assigned by the USB-IF, if available. /// The USB vendor ID as assigned by the USB-IF, if available.
pub(crate) vendor_id: Option<u16>, pub(crate) vendor_id: Option<u16>,
/// The USB product ID as assigned by the [vendor], if available. /// The USB product ID as assigned by the [vendor][Self::vendor_id], if available.
///
/// [vendor]: Self::vendor_id
pub(crate) product_id: Option<u16>, pub(crate) product_id: Option<u16>,
/// [`ButtonInput`] of [`GamepadButton`] representing their digital state /// [`ButtonInput`] of [`GamepadButton`] representing their digital state.
pub(crate) digital: ButtonInput<GamepadButton>, pub(crate) digital: ButtonInput<GamepadButton>,
/// [`Axis`] of [`GamepadButton`] representing their analog state. /// [`Axis`] of [`GamepadButton`] representing their analog state.
@ -378,7 +378,7 @@ impl Gamepad {
self.analog.get_unclamped(input.into()) self.analog.get_unclamped(input.into())
} }
/// Returns the left stick as a [`Vec2`] /// Returns the left stick as a [`Vec2`].
pub fn left_stick(&self) -> Vec2 { pub fn left_stick(&self) -> Vec2 {
Vec2 { Vec2 {
x: self.get(GamepadAxis::LeftStickX).unwrap_or(0.0), x: self.get(GamepadAxis::LeftStickX).unwrap_or(0.0),
@ -386,7 +386,7 @@ impl Gamepad {
} }
} }
/// Returns the right stick as a [`Vec2`] /// Returns the right stick as a [`Vec2`].
pub fn right_stick(&self) -> Vec2 { pub fn right_stick(&self) -> Vec2 {
Vec2 { Vec2 {
x: self.get(GamepadAxis::RightStickX).unwrap_or(0.0), x: self.get(GamepadAxis::RightStickX).unwrap_or(0.0),
@ -394,7 +394,7 @@ impl Gamepad {
} }
} }
/// Returns the directional pad as a [`Vec2`] /// Returns the directional pad as a [`Vec2`].
pub fn dpad(&self) -> Vec2 { pub fn dpad(&self) -> Vec2 {
Vec2 { Vec2 {
x: self.get(GamepadButton::DPadRight).unwrap_or(0.0) x: self.get(GamepadButton::DPadRight).unwrap_or(0.0)
@ -480,14 +480,12 @@ impl Gamepad {
self.digital.get_just_released() self.digital.get_just_released()
} }
/// Returns an iterator over all analog [axes]. /// Returns an iterator over all analog [axes][GamepadInput].
///
/// [axes]: GamepadInput
pub fn get_analog_axes(&self) -> impl Iterator<Item = &GamepadInput> { pub fn get_analog_axes(&self) -> impl Iterator<Item = &GamepadInput> {
self.analog.all_axes() self.analog.all_axes()
} }
/// [`ButtonInput`] of [`GamepadButton`] representing their digital state /// [`ButtonInput`] of [`GamepadButton`] representing their digital state.
pub fn digital(&self) -> &ButtonInput<GamepadButton> { pub fn digital(&self) -> &ButtonInput<GamepadButton> {
&self.digital &self.digital
} }
@ -531,7 +529,7 @@ impl Default for Gamepad {
/// ///
/// ## Usage /// ## Usage
/// ///
/// This is used to determine which button has changed its value when receiving gamepad button events /// This is used to determine which button has changed its value when receiving gamepad button events.
/// It is also used in the [`Gamepad`] component. /// It is also used in the [`Gamepad`] component.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr( #[cfg_attr(
@ -593,7 +591,7 @@ pub enum GamepadButton {
} }
impl GamepadButton { impl GamepadButton {
/// Returns an array of all the standard [`GamepadButton`] /// Returns an array of all the standard [`GamepadButton`].
pub const fn all() -> [GamepadButton; 19] { pub const fn all() -> [GamepadButton; 19] {
[ [
GamepadButton::South, GamepadButton::South,
@ -619,7 +617,7 @@ impl GamepadButton {
} }
} }
/// Represents gamepad input types that are mapped in the range [-1.0, 1.0] /// Represents gamepad input types that are mapped in the range [-1.0, 1.0].
/// ///
/// ## Usage /// ## Usage
/// ///
@ -665,14 +663,14 @@ impl GamepadAxis {
} }
} }
/// Encapsulation over [`GamepadAxis`] and [`GamepadButton`] /// Encapsulation over [`GamepadAxis`] and [`GamepadButton`].
// This is done so Gamepad can share a single Axis<T> and simplifies the API by having only one get/get_unclamped method // This is done so Gamepad can share a single Axis<T> and simplifies the API by having only one get/get_unclamped method
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, From)] #[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, From)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
pub enum GamepadInput { pub enum GamepadInput {
/// A [`GamepadAxis`] /// A [`GamepadAxis`].
Axis(GamepadAxis), Axis(GamepadAxis),
/// A [`GamepadButton`] /// A [`GamepadButton`].
Button(GamepadButton), Button(GamepadButton),
} }
@ -928,9 +926,9 @@ impl ButtonSettings {
/// threshold for an axis. /// threshold for an axis.
/// Values that are higher than `livezone_upperbound` will be rounded up to 1.0. /// Values that are higher than `livezone_upperbound` will be rounded up to 1.0.
/// Values that are lower than `livezone_lowerbound` will be rounded down to -1.0. /// Values that are lower than `livezone_lowerbound` will be rounded down to -1.0.
/// Values that are in-between `deadzone_lowerbound` and `deadzone_upperbound` will be rounded /// Values that are in-between `deadzone_lowerbound` and `deadzone_upperbound` will be rounded to 0.0.
/// to 0.0. /// Otherwise, values will be linearly rescaled to fit into the sensitivity range.
/// Otherwise, values will not be rounded. /// For example, a value that is one fourth of the way from `deadzone_upperbound` to `livezone_upperbound` will be scaled to 0.25.
/// ///
/// The valid range is `[-1.0, 1.0]`. /// The valid range is `[-1.0, 1.0]`.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -1041,7 +1039,7 @@ impl AxisSettings {
/// ///
/// # Errors /// # Errors
/// ///
/// If the value passed is less than the dead zone upper bound, /// If the value passed is less than the deadzone upper bound,
/// returns `AxisSettingsError::DeadZoneUpperBoundGreaterThanLiveZoneUpperBound`. /// returns `AxisSettingsError::DeadZoneUpperBoundGreaterThanLiveZoneUpperBound`.
/// If the value passed is not in range [0.0..=1.0], returns `AxisSettingsError::LiveZoneUpperBoundOutOfRange`. /// If the value passed is not in range [0.0..=1.0], returns `AxisSettingsError::LiveZoneUpperBoundOutOfRange`.
pub fn try_set_livezone_upperbound(&mut self, value: f32) -> Result<(), AxisSettingsError> { pub fn try_set_livezone_upperbound(&mut self, value: f32) -> Result<(), AxisSettingsError> {
@ -1117,7 +1115,7 @@ impl AxisSettings {
/// ///
/// # Errors /// # Errors
/// ///
/// If the value passed is less than the dead zone lower bound, /// If the value passed is less than the deadzone lower bound,
/// returns `AxisSettingsError::LiveZoneLowerBoundGreaterThanDeadZoneLowerBound`. /// returns `AxisSettingsError::LiveZoneLowerBoundGreaterThanDeadZoneLowerBound`.
/// If the value passed is not in range [-1.0..=0.0], returns `AxisSettingsError::LiveZoneLowerBoundOutOfRange`. /// If the value passed is not in range [-1.0..=0.0], returns `AxisSettingsError::LiveZoneLowerBoundOutOfRange`.
pub fn try_set_livezone_lowerbound(&mut self, value: f32) -> Result<(), AxisSettingsError> { pub fn try_set_livezone_lowerbound(&mut self, value: f32) -> Result<(), AxisSettingsError> {
@ -1213,39 +1211,135 @@ impl AxisSettings {
} }
/// Clamps the `raw_value` according to the `AxisSettings`. /// Clamps the `raw_value` according to the `AxisSettings`.
pub fn clamp(&self, new_value: f32) -> f32 { pub fn clamp(&self, raw_value: f32) -> f32 {
if self.deadzone_lowerbound <= new_value && new_value <= self.deadzone_upperbound { if self.deadzone_lowerbound <= raw_value && raw_value <= self.deadzone_upperbound {
0.0 0.0
} else if new_value >= self.livezone_upperbound { } else if raw_value >= self.livezone_upperbound {
1.0 1.0
} else if new_value <= self.livezone_lowerbound { } else if raw_value <= self.livezone_lowerbound {
-1.0 -1.0
} else { } else {
new_value raw_value
} }
} }
/// Determines whether the change from `old_value` to `new_value` should /// Determines whether the change from `old_raw_value` to `new_raw_value` should
/// be registered as a change, according to the [`AxisSettings`]. /// be registered as a change, according to the [`AxisSettings`].
fn should_register_change(&self, new_value: f32, old_value: Option<f32>) -> bool { fn should_register_change(&self, new_raw_value: f32, old_raw_value: Option<f32>) -> bool {
if old_value.is_none() { match old_raw_value {
return true; None => true,
Some(old_raw_value) => ops::abs(new_raw_value - old_raw_value) >= self.threshold,
} }
ops::abs(new_value - old_value.unwrap()) > self.threshold
} }
/// Filters the `new_value` based on the `old_value`, according to the [`AxisSettings`]. /// Filters the `new_raw_value` based on the `old_raw_value`, according to the [`AxisSettings`].
/// ///
/// Returns the clamped `new_value` if the change exceeds the settings threshold, /// Returns the clamped and scaled `new_raw_value` if the change exceeds the settings threshold,
/// and `None` otherwise. /// and `None` otherwise.
pub fn filter(&self, new_value: f32, old_value: Option<f32>) -> Option<f32> { fn filter(
let new_value = self.clamp(new_value); &self,
new_raw_value: f32,
if self.should_register_change(new_value, old_value) { old_raw_value: Option<f32>,
return Some(new_value); ) -> Option<FilteredAxisPosition> {
let clamped_unscaled = self.clamp(new_raw_value);
match self.should_register_change(clamped_unscaled, old_raw_value) {
true => Some(FilteredAxisPosition {
scaled: self.get_axis_position_from_value(clamped_unscaled),
raw: new_raw_value,
}),
false => None,
}
}
#[inline(always)]
fn get_axis_position_from_value(&self, value: f32) -> ScaledAxisWithDeadZonePosition {
if value < self.deadzone_upperbound && value > self.deadzone_lowerbound {
ScaledAxisWithDeadZonePosition::Dead
} else if value > self.livezone_upperbound {
ScaledAxisWithDeadZonePosition::AboveHigh
} else if value < self.livezone_lowerbound {
ScaledAxisWithDeadZonePosition::BelowLow
} else if value >= self.deadzone_upperbound {
ScaledAxisWithDeadZonePosition::High(linear_remapping(
value,
self.deadzone_upperbound..=self.livezone_upperbound,
0.0..=1.0,
))
} else if value <= self.deadzone_lowerbound {
ScaledAxisWithDeadZonePosition::Low(linear_remapping(
value,
self.livezone_lowerbound..=self.deadzone_lowerbound,
-1.0..=0.0,
))
} else {
unreachable!();
}
}
}
/// A linear remapping of `value` from `old` to `new`.
fn linear_remapping(value: f32, old: RangeInclusive<f32>, new: RangeInclusive<f32>) -> f32 {
// https://stackoverflow.com/a/929104
((value - old.start()) / (old.end() - old.start())) * (new.end() - new.start()) + new.start()
}
#[derive(Debug, Clone, Copy)]
/// Deadzone-aware axis position.
enum ScaledAxisWithDeadZonePosition {
/// The input clipped below the valid range of the axis.
BelowLow,
/// The input is lower than the deadzone.
Low(f32),
/// The input falls within the deadzone, meaning it is counted as 0.
Dead,
/// The input is higher than the deadzone.
High(f32),
/// The input clipped above the valid range of the axis.
AboveHigh,
}
struct FilteredAxisPosition {
scaled: ScaledAxisWithDeadZonePosition,
raw: f32,
}
impl ScaledAxisWithDeadZonePosition {
/// Converts the value into a float in the range [-1, 1].
fn to_f32(self) -> f32 {
match self {
ScaledAxisWithDeadZonePosition::BelowLow => -1.,
ScaledAxisWithDeadZonePosition::Low(scaled)
| ScaledAxisWithDeadZonePosition::High(scaled) => scaled,
ScaledAxisWithDeadZonePosition::Dead => 0.,
ScaledAxisWithDeadZonePosition::AboveHigh => 1.,
}
}
}
#[derive(Debug, Clone, Copy)]
/// Low/High-aware axis position.
enum ScaledAxisPosition {
/// The input fell short of the "low" value.
ClampedLow,
/// The input was in the normal range.
Scaled(f32),
/// The input surpassed the "high" value.
ClampedHigh,
}
struct FilteredButtonAxisPosition {
scaled: ScaledAxisPosition,
raw: f32,
}
impl ScaledAxisPosition {
/// Converts the value into a float in the range [0, 1].
fn to_f32(self) -> f32 {
match self {
ScaledAxisPosition::ClampedLow => 0.,
ScaledAxisPosition::Scaled(scaled) => scaled,
ScaledAxisPosition::ClampedHigh => 1.,
} }
None
} }
} }
@ -1300,27 +1394,48 @@ impl ButtonAxisSettings {
raw_value raw_value
} }
/// Determines whether the change from an `old_value` to a `new_value` should /// Determines whether the change from an `old_raw_value` to a `new_raw_value` should
/// be registered as a change event, according to the specified settings. /// be registered as a change event, according to the specified settings.
fn should_register_change(&self, new_value: f32, old_value: Option<f32>) -> bool { fn should_register_change(&self, new_raw_value: f32, old_raw_value: Option<f32>) -> bool {
if old_value.is_none() { match old_raw_value {
return true; None => true,
Some(old_raw_value) => ops::abs(new_raw_value - old_raw_value) >= self.threshold,
} }
ops::abs(new_value - old_value.unwrap()) > self.threshold
} }
/// Filters the `new_value` based on the `old_value`, according to the [`ButtonAxisSettings`]. /// Filters the `new_raw_value` based on the `old_raw_value`, according to the [`ButtonAxisSettings`].
/// ///
/// Returns the clamped `new_value`, according to the [`ButtonAxisSettings`], if the change /// Returns the clamped and scaled `new_raw_value`, according to the [`ButtonAxisSettings`], if the change
/// exceeds the settings threshold, and `None` otherwise. /// exceeds the settings threshold, and `None` otherwise.
pub fn filter(&self, new_value: f32, old_value: Option<f32>) -> Option<f32> { fn filter(
let new_value = self.clamp(new_value); &self,
new_raw_value: f32,
if self.should_register_change(new_value, old_value) { old_raw_value: Option<f32>,
return Some(new_value); ) -> Option<FilteredButtonAxisPosition> {
let clamped_unscaled = self.clamp(new_raw_value);
match self.should_register_change(clamped_unscaled, old_raw_value) {
true => Some(FilteredButtonAxisPosition {
scaled: self.get_axis_position_from_value(clamped_unscaled),
raw: new_raw_value,
}),
false => None,
}
}
/// Clamps and scales the `value` according to the specified settings.
///
/// If the `value` is:
/// - lower than or equal to `low` it will be rounded to 0.0.
/// - higher than or equal to `high` it will be rounded to 1.0.
/// - Otherwise, it will be scaled from (low, high) to (0, 1).
fn get_axis_position_from_value(&self, value: f32) -> ScaledAxisPosition {
if value <= self.low {
ScaledAxisPosition::ClampedLow
} else if value >= self.high {
ScaledAxisPosition::ClampedHigh
} else {
ScaledAxisPosition::Scaled(linear_remapping(value, self.low..=self.high, 0.0..=1.0))
} }
None
} }
} }
@ -1328,7 +1443,7 @@ impl ButtonAxisSettings {
/// ///
/// On connection, adds the components representing a [`Gamepad`] to the entity. /// On connection, adds the components representing a [`Gamepad`] to the entity.
/// On disconnection, removes the [`Gamepad`] and other related components. /// On disconnection, removes the [`Gamepad`] and other related components.
/// Entities are left alive and might leave components like [`GamepadSettings`] to preserve state in the case of a reconnection /// Entities are left alive and might leave components like [`GamepadSettings`] to preserve state in the case of a reconnection.
/// ///
/// ## Note /// ## Note
/// ///
@ -1441,9 +1556,9 @@ pub fn gamepad_event_processing_system(
else { else {
continue; continue;
}; };
gamepad_axis.analog.set(axis, filtered_value.raw);
gamepad_axis.analog.set(axis, filtered_value); let send_event =
let send_event = GamepadAxisChangedEvent::new(gamepad, axis, filtered_value); GamepadAxisChangedEvent::new(gamepad, axis, filtered_value.scaled.to_f32());
processed_axis_events.send(send_event); processed_axis_events.send(send_event);
processed_events.send(GamepadEvent::from(send_event)); processed_events.send(GamepadEvent::from(send_event));
} }
@ -1463,9 +1578,9 @@ pub fn gamepad_event_processing_system(
continue; continue;
}; };
let button_settings = settings.get_button_settings(button); let button_settings = settings.get_button_settings(button);
gamepad_buttons.analog.set(button, filtered_value); gamepad_buttons.analog.set(button, filtered_value.raw);
if button_settings.is_released(filtered_value) { if button_settings.is_released(filtered_value.raw) {
// Check if button was previously pressed // Check if button was previously pressed
if gamepad_buttons.pressed(button) { if gamepad_buttons.pressed(button) {
processed_digital_events.send(GamepadButtonStateChangedEvent::new( processed_digital_events.send(GamepadButtonStateChangedEvent::new(
@ -1477,7 +1592,7 @@ pub fn gamepad_event_processing_system(
// We don't have to check if the button was previously pressed here // We don't have to check if the button was previously pressed here
// because that check is performed within Input<T>::release() // because that check is performed within Input<T>::release()
gamepad_buttons.digital.release(button); gamepad_buttons.digital.release(button);
} else if button_settings.is_pressed(filtered_value) { } else if button_settings.is_pressed(filtered_value.raw) {
// Check if button was previously not pressed // Check if button was previously not pressed
if !gamepad_buttons.pressed(button) { if !gamepad_buttons.pressed(button) {
processed_digital_events.send(GamepadButtonStateChangedEvent::new( processed_digital_events.send(GamepadButtonStateChangedEvent::new(
@ -1494,8 +1609,12 @@ pub fn gamepad_event_processing_system(
} else { } else {
ButtonState::Released ButtonState::Released
}; };
let send_event = let send_event = GamepadButtonChangedEvent::new(
GamepadButtonChangedEvent::new(gamepad, button, button_state, filtered_value); gamepad,
button,
button_state,
filtered_value.scaled.to_f32(),
);
processed_analog_events.send(send_event); processed_analog_events.send(send_event);
processed_events.send(GamepadEvent::from(send_event)); processed_events.send(GamepadEvent::from(send_event));
} }
@ -1649,130 +1768,161 @@ mod tests {
fn test_button_axis_settings_filter( fn test_button_axis_settings_filter(
settings: ButtonAxisSettings, settings: ButtonAxisSettings,
new_value: f32, new_raw_value: f32,
old_value: Option<f32>, old_raw_value: Option<f32>,
expected: Option<f32>, expected: Option<f32>,
) { ) {
let actual = settings.filter(new_value, old_value); let actual = settings
.filter(new_raw_value, old_raw_value)
.map(|f| f.scaled.to_f32());
assert_eq!( assert_eq!(
expected, actual, expected, actual,
"Testing filtering for {settings:?} with new_value = {new_value:?}, old_value = {old_value:?}", "Testing filtering for {settings:?} with new_raw_value = {new_raw_value:?}, old_raw_value = {old_raw_value:?}",
); );
} }
#[test] #[test]
fn test_button_axis_settings_default_filter() { fn test_button_axis_settings_default_filter() {
let cases = [ let cases = [
// clamped
(1.0, None, Some(1.0)), (1.0, None, Some(1.0)),
(0.99, None, Some(1.0)), (0.99, None, Some(1.0)),
(0.96, None, Some(1.0)), (0.96, None, Some(1.0)),
(0.95, None, Some(1.0)), (0.95, None, Some(1.0)),
(0.9499, None, Some(0.9499)), // linearly rescaled from 0.05..=0.95 to 0.0..=1.0
(0.84, None, Some(0.84)), (0.9499, None, Some(0.9998889)),
(0.43, None, Some(0.43)), (0.84, None, Some(0.87777776)),
(0.05001, None, Some(0.05001)), (0.43, None, Some(0.42222223)),
(0.05001, None, Some(0.000011109644)),
// clamped
(0.05, None, Some(0.0)), (0.05, None, Some(0.0)),
(0.04, None, Some(0.0)), (0.04, None, Some(0.0)),
(0.01, None, Some(0.0)), (0.01, None, Some(0.0)),
(0.0, None, Some(0.0)), (0.0, None, Some(0.0)),
]; ];
for (new_value, old_value, expected) in cases { for (new_raw_value, old_raw_value, expected) in cases {
let settings = ButtonAxisSettings::default(); let settings = ButtonAxisSettings::default();
test_button_axis_settings_filter(settings, new_value, old_value, expected); test_button_axis_settings_filter(settings, new_raw_value, old_raw_value, expected);
} }
} }
#[test] #[test]
fn test_button_axis_settings_default_filter_with_old_value() { fn test_button_axis_settings_default_filter_with_old_raw_value() {
let cases = [ let cases = [
(0.43, Some(0.44001), Some(0.43)), // 0.43 gets rescaled to 0.42222223 (0.05..=0.95 -> 0.0..=1.0)
(0.43, Some(0.44001), Some(0.42222223)),
(0.43, Some(0.44), None), (0.43, Some(0.44), None),
(0.43, Some(0.43), None), (0.43, Some(0.43), None),
(0.43, Some(0.41999), Some(0.43)), (0.43, Some(0.41999), Some(0.42222223)),
(0.43, Some(0.17), Some(0.43)), (0.43, Some(0.17), Some(0.42222223)),
(0.43, Some(0.84), Some(0.43)), (0.43, Some(0.84), Some(0.42222223)),
(0.05, Some(0.055), Some(0.0)), (0.05, Some(0.055), Some(0.0)),
(0.95, Some(0.945), Some(1.0)), (0.95, Some(0.945), Some(1.0)),
]; ];
for (new_value, old_value, expected) in cases { for (new_raw_value, old_raw_value, expected) in cases {
let settings = ButtonAxisSettings::default(); let settings = ButtonAxisSettings::default();
test_button_axis_settings_filter(settings, new_value, old_value, expected); test_button_axis_settings_filter(settings, new_raw_value, old_raw_value, expected);
} }
} }
fn test_axis_settings_filter( fn test_axis_settings_filter(
settings: AxisSettings, settings: AxisSettings,
new_value: f32, new_raw_value: f32,
old_value: Option<f32>, old_raw_value: Option<f32>,
expected: Option<f32>, expected: Option<f32>,
) { ) {
let actual = settings.filter(new_value, old_value); let actual = settings.filter(new_raw_value, old_raw_value);
assert_eq!( assert_eq!(
expected, actual, expected, actual.map(|f| f.scaled.to_f32()),
"Testing filtering for {settings:?} with new_value = {new_value:?}, old_value = {old_value:?}", "Testing filtering for {settings:?} with new_raw_value = {new_raw_value:?}, old_raw_value = {old_raw_value:?}",
); );
} }
#[test] #[test]
fn test_axis_settings_default_filter() { fn test_axis_settings_default_filter() {
// new (raw), expected (rescaled linearly)
let cases = [ let cases = [
// high enough to round to 1.0
(1.0, Some(1.0)), (1.0, Some(1.0)),
(0.99, Some(1.0)), (0.99, Some(1.0)),
(0.96, Some(1.0)), (0.96, Some(1.0)),
(0.95, Some(1.0)), (0.95, Some(1.0)),
(0.9499, Some(0.9499)), // for the following, remember that 0.05 is the "low" value and 0.95 is the "high" value
(0.84, Some(0.84)), // barely below the high value means barely below 1 after scaling
(0.43, Some(0.43)), (0.9499, Some(0.9998889)), // scaled as: (0.9499 - 0.05) / (0.95 - 0.05)
(0.05001, Some(0.05001)), (0.84, Some(0.87777776)), // scaled as: (0.84 - 0.05) / (0.95 - 0.05)
(0.43, Some(0.42222223)), // scaled as: (0.43 - 0.05) / (0.95 - 0.05)
// barely above the low value means barely above 0 after scaling
(0.05001, Some(0.000011109644)), // scaled as: (0.05001 - 0.05) / (0.95 - 0.05)
// low enough to be rounded to 0 (dead zone)
(0.05, Some(0.0)), (0.05, Some(0.0)),
(0.04, Some(0.0)), (0.04, Some(0.0)),
(0.01, Some(0.0)), (0.01, Some(0.0)),
(0.0, Some(0.0)), (0.0, Some(0.0)),
// same exact tests as above, but below 0 (bottom half of the dead zone and live zone)
// low enough to be rounded to -1
(-1.0, Some(-1.0)), (-1.0, Some(-1.0)),
(-0.99, Some(-1.0)), (-0.99, Some(-1.0)),
(-0.96, Some(-1.0)), (-0.96, Some(-1.0)),
(-0.95, Some(-1.0)), (-0.95, Some(-1.0)),
(-0.9499, Some(-0.9499)), // scaled inputs
(-0.84, Some(-0.84)), (-0.9499, Some(-0.9998889)), // scaled as: (-0.9499 - -0.05) / (-0.95 - -0.05)
(-0.43, Some(-0.43)), (-0.84, Some(-0.87777776)), // scaled as: (-0.84 - -0.05) / (-0.95 - -0.05)
(-0.05001, Some(-0.05001)), (-0.43, Some(-0.42222226)), // scaled as: (-0.43 - -0.05) / (-0.95 - -0.05)
(-0.05001, Some(-0.000011146069)), // scaled as: (-0.05001 - -0.05) / (-0.95 - -0.05)
// high enough to be rounded to 0 (dead zone)
(-0.05, Some(0.0)), (-0.05, Some(0.0)),
(-0.04, Some(0.0)), (-0.04, Some(0.0)),
(-0.01, Some(0.0)), (-0.01, Some(0.0)),
]; ];
for (new_value, expected) in cases { for (new_raw_value, expected) in cases {
let settings = AxisSettings::new(-0.95, -0.05, 0.05, 0.95, 0.01).unwrap(); let settings = AxisSettings::new(-0.95, -0.05, 0.05, 0.95, 0.01).unwrap();
test_axis_settings_filter(settings, new_value, None, expected); test_axis_settings_filter(settings, new_raw_value, None, expected);
} }
} }
#[test] #[test]
fn test_axis_settings_default_filter_with_old_values() { fn test_axis_settings_default_filter_with_old_raw_values() {
let threshold = 0.01;
// expected values are hardcoded to be rescaled to from 0.05..=0.95 to 0.0..=1.0
// new (raw), old (raw), expected
let cases = [ let cases = [
(0.43, Some(0.44001), Some(0.43)), // enough increase to change
(0.43, Some(0.44), None), (0.43, Some(0.43 + threshold * 1.1), Some(0.42222223)),
(0.43, Some(0.43), None), // enough decrease to change
(0.43, Some(0.41999), Some(0.43)), (0.43, Some(0.43 - threshold * 1.1), Some(0.42222223)),
(0.43, Some(0.17), Some(0.43)), // not enough increase to change
(0.43, Some(0.84), Some(0.43)), (0.43, Some(0.43 + threshold * 0.9), None),
(0.05, Some(0.055), Some(0.0)), // not enough decrease to change
(0.95, Some(0.945), Some(1.0)), (0.43, Some(0.43 - threshold * 0.9), None),
(-0.43, Some(-0.44001), Some(-0.43)), // enough increase to change
(-0.43, Some(-0.44), None), (-0.43, Some(-0.43 + threshold * 1.1), Some(-0.42222226)),
(-0.43, Some(-0.43), None), // enough decrease to change
(-0.43, Some(-0.41999), Some(-0.43)), (-0.43, Some(-0.43 - threshold * 1.1), Some(-0.42222226)),
(-0.43, Some(-0.17), Some(-0.43)), // not enough increase to change
(-0.43, Some(-0.84), Some(-0.43)), (-0.43, Some(-0.43 + threshold * 0.9), None),
(-0.05, Some(-0.055), Some(0.0)), // not enough decrease to change
(-0.95, Some(-0.945), Some(-1.0)), (-0.43, Some(-0.43 - threshold * 0.9), None),
// test upper deadzone logic
(0.05, Some(0.0), None),
(0.06, Some(0.0), Some(0.0111111095)),
// test lower deadzone logic
(-0.05, Some(0.0), None),
(-0.06, Some(0.0), Some(-0.011111081)),
// test upper livezone logic
(0.95, Some(1.0), None),
(0.94, Some(1.0), Some(0.9888889)),
// test lower livezone logic
(-0.95, Some(-1.0), None),
(-0.94, Some(-1.0), Some(-0.9888889)),
]; ];
for (new_value, old_value, expected) in cases { for (new_raw_value, old_raw_value, expected) in cases {
let settings = AxisSettings::new(-0.95, -0.05, 0.05, 0.95, 0.01).unwrap(); let settings = AxisSettings::new(-0.95, -0.05, 0.05, 0.95, threshold).unwrap();
test_axis_settings_filter(settings, new_value, old_value, expected); test_axis_settings_filter(settings, new_raw_value, old_raw_value, expected);
} }
} }