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:
parent
120b733ab5
commit
43db44ca3a
@ -1,5 +1,7 @@
|
||||
//! The gamepad input functionality.
|
||||
|
||||
use core::ops::RangeInclusive;
|
||||
|
||||
use crate::{Axis, ButtonInput, ButtonState};
|
||||
use alloc::string::String;
|
||||
#[cfg(feature = "bevy_reflect")]
|
||||
@ -30,7 +32,7 @@ use thiserror::Error;
|
||||
/// [`GamepadButtonChangedEvent`] and [`GamepadAxisChangedEvent`] when
|
||||
/// 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)]
|
||||
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
@ -70,7 +72,7 @@ pub enum RawGamepadEvent {
|
||||
Axis(RawGamepadAxisChangedEvent),
|
||||
}
|
||||
|
||||
/// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`]
|
||||
/// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`].
|
||||
#[derive(Event, Debug, Copy, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
|
||||
#[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)]
|
||||
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
|
||||
#[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 {
|
||||
matches!(self.connection, GamepadConnection::Connected { .. })
|
||||
}
|
||||
|
||||
/// Is the gamepad disconnected?
|
||||
/// Whether the gamepad is disconnected.
|
||||
pub fn disconnected(&self) -> bool {
|
||||
!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)]
|
||||
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
@ -180,7 +182,7 @@ pub struct GamepadButtonStateChangedEvent {
|
||||
}
|
||||
|
||||
impl GamepadButtonStateChangedEvent {
|
||||
/// Creates a new [`GamepadButtonStateChangedEvent`]
|
||||
/// Creates a new [`GamepadButtonStateChangedEvent`].
|
||||
pub fn new(entity: Entity, button: GamepadButton, state: ButtonState) -> Self {
|
||||
Self {
|
||||
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)]
|
||||
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
@ -205,12 +207,12 @@ pub struct GamepadButtonChangedEvent {
|
||||
pub button: GamepadButton,
|
||||
/// The pressed state of the button.
|
||||
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,
|
||||
}
|
||||
|
||||
impl GamepadButtonChangedEvent {
|
||||
/// Creates a new [`GamepadButtonChangedEvent`]
|
||||
/// Creates a new [`GamepadButtonChangedEvent`].
|
||||
pub fn new(entity: Entity, button: GamepadButton, state: ButtonState, value: f32) -> Self {
|
||||
Self {
|
||||
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)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
|
||||
@ -234,12 +236,12 @@ pub struct GamepadAxisChangedEvent {
|
||||
pub entity: Entity,
|
||||
/// The gamepad axis assigned to the event.
|
||||
pub axis: GamepadAxis,
|
||||
/// The value of this axis.
|
||||
/// The value of this axis (rescaled to account for axis settings).
|
||||
pub value: f32,
|
||||
}
|
||||
|
||||
impl GamepadAxisChangedEvent {
|
||||
/// Creates a new [`GamepadAxisChangedEvent`]
|
||||
/// Creates a new [`GamepadAxisChangedEvent`].
|
||||
pub fn new(entity: Entity, axis: GamepadAxis, value: f32) -> Self {
|
||||
Self {
|
||||
entity,
|
||||
@ -339,12 +341,10 @@ pub struct Gamepad {
|
||||
/// The USB vendor ID as assigned by the USB-IF, if available.
|
||||
pub(crate) vendor_id: Option<u16>,
|
||||
|
||||
/// The USB product ID as assigned by the [vendor], if available.
|
||||
///
|
||||
/// [vendor]: Self::vendor_id
|
||||
/// The USB product ID as assigned by the [vendor][Self::vendor_id], if available.
|
||||
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>,
|
||||
|
||||
/// [`Axis`] of [`GamepadButton`] representing their analog state.
|
||||
@ -378,7 +378,7 @@ impl Gamepad {
|
||||
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 {
|
||||
Vec2 {
|
||||
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 {
|
||||
Vec2 {
|
||||
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 {
|
||||
Vec2 {
|
||||
x: self.get(GamepadButton::DPadRight).unwrap_or(0.0)
|
||||
@ -480,14 +480,12 @@ impl Gamepad {
|
||||
self.digital.get_just_released()
|
||||
}
|
||||
|
||||
/// Returns an iterator over all analog [axes].
|
||||
///
|
||||
/// [axes]: GamepadInput
|
||||
/// Returns an iterator over all analog [axes][GamepadInput].
|
||||
pub fn get_analog_axes(&self) -> impl Iterator<Item = &GamepadInput> {
|
||||
self.analog.all_axes()
|
||||
}
|
||||
|
||||
/// [`ButtonInput`] of [`GamepadButton`] representing their digital state
|
||||
/// [`ButtonInput`] of [`GamepadButton`] representing their digital state.
|
||||
pub fn digital(&self) -> &ButtonInput<GamepadButton> {
|
||||
&self.digital
|
||||
}
|
||||
@ -531,7 +529,7 @@ impl Default for Gamepad {
|
||||
///
|
||||
/// ## 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.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[cfg_attr(
|
||||
@ -593,7 +591,7 @@ pub enum 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] {
|
||||
[
|
||||
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
|
||||
///
|
||||
@ -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
|
||||
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, From)]
|
||||
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
|
||||
pub enum GamepadInput {
|
||||
/// A [`GamepadAxis`]
|
||||
/// A [`GamepadAxis`].
|
||||
Axis(GamepadAxis),
|
||||
/// A [`GamepadButton`]
|
||||
/// A [`GamepadButton`].
|
||||
Button(GamepadButton),
|
||||
}
|
||||
|
||||
@ -928,9 +926,9 @@ impl ButtonSettings {
|
||||
/// threshold for an axis.
|
||||
/// 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 in-between `deadzone_lowerbound` and `deadzone_upperbound` will be rounded
|
||||
/// to 0.0.
|
||||
/// Otherwise, values will not be rounded.
|
||||
/// Values that are in-between `deadzone_lowerbound` and `deadzone_upperbound` will be rounded to 0.0.
|
||||
/// Otherwise, values will be linearly rescaled to fit into the sensitivity range.
|
||||
/// 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]`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@ -1041,7 +1039,7 @@ impl AxisSettings {
|
||||
///
|
||||
/// # 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`.
|
||||
/// 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> {
|
||||
@ -1117,7 +1115,7 @@ impl AxisSettings {
|
||||
///
|
||||
/// # 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`.
|
||||
/// 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> {
|
||||
@ -1213,39 +1211,135 @@ impl AxisSettings {
|
||||
}
|
||||
|
||||
/// Clamps the `raw_value` according to the `AxisSettings`.
|
||||
pub fn clamp(&self, new_value: f32) -> f32 {
|
||||
if self.deadzone_lowerbound <= new_value && new_value <= self.deadzone_upperbound {
|
||||
pub fn clamp(&self, raw_value: f32) -> f32 {
|
||||
if self.deadzone_lowerbound <= raw_value && raw_value <= self.deadzone_upperbound {
|
||||
0.0
|
||||
} else if new_value >= self.livezone_upperbound {
|
||||
} else if raw_value >= self.livezone_upperbound {
|
||||
1.0
|
||||
} else if new_value <= self.livezone_lowerbound {
|
||||
} else if raw_value <= self.livezone_lowerbound {
|
||||
-1.0
|
||||
} 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`].
|
||||
fn should_register_change(&self, new_value: f32, old_value: Option<f32>) -> bool {
|
||||
if old_value.is_none() {
|
||||
return true;
|
||||
fn should_register_change(&self, new_raw_value: f32, old_raw_value: Option<f32>) -> bool {
|
||||
match old_raw_value {
|
||||
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.
|
||||
pub fn filter(&self, new_value: f32, old_value: Option<f32>) -> Option<f32> {
|
||||
let new_value = self.clamp(new_value);
|
||||
|
||||
if self.should_register_change(new_value, old_value) {
|
||||
return Some(new_value);
|
||||
fn filter(
|
||||
&self,
|
||||
new_raw_value: f32,
|
||||
old_raw_value: Option<f32>,
|
||||
) -> 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
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn should_register_change(&self, new_value: f32, old_value: Option<f32>) -> bool {
|
||||
if old_value.is_none() {
|
||||
return true;
|
||||
fn should_register_change(&self, new_raw_value: f32, old_raw_value: Option<f32>) -> bool {
|
||||
match old_raw_value {
|
||||
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.
|
||||
pub fn filter(&self, new_value: f32, old_value: Option<f32>) -> Option<f32> {
|
||||
let new_value = self.clamp(new_value);
|
||||
|
||||
if self.should_register_change(new_value, old_value) {
|
||||
return Some(new_value);
|
||||
fn filter(
|
||||
&self,
|
||||
new_raw_value: f32,
|
||||
old_raw_value: Option<f32>,
|
||||
) -> 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 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
|
||||
///
|
||||
@ -1441,9 +1556,9 @@ pub fn gamepad_event_processing_system(
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
gamepad_axis.analog.set(axis, filtered_value);
|
||||
let send_event = GamepadAxisChangedEvent::new(gamepad, axis, filtered_value);
|
||||
gamepad_axis.analog.set(axis, filtered_value.raw);
|
||||
let send_event =
|
||||
GamepadAxisChangedEvent::new(gamepad, axis, filtered_value.scaled.to_f32());
|
||||
processed_axis_events.send(send_event);
|
||||
processed_events.send(GamepadEvent::from(send_event));
|
||||
}
|
||||
@ -1463,9 +1578,9 @@ pub fn gamepad_event_processing_system(
|
||||
continue;
|
||||
};
|
||||
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
|
||||
if gamepad_buttons.pressed(button) {
|
||||
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
|
||||
// because that check is performed within Input<T>::release()
|
||||
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
|
||||
if !gamepad_buttons.pressed(button) {
|
||||
processed_digital_events.send(GamepadButtonStateChangedEvent::new(
|
||||
@ -1494,8 +1609,12 @@ pub fn gamepad_event_processing_system(
|
||||
} else {
|
||||
ButtonState::Released
|
||||
};
|
||||
let send_event =
|
||||
GamepadButtonChangedEvent::new(gamepad, button, button_state, filtered_value);
|
||||
let send_event = GamepadButtonChangedEvent::new(
|
||||
gamepad,
|
||||
button,
|
||||
button_state,
|
||||
filtered_value.scaled.to_f32(),
|
||||
);
|
||||
processed_analog_events.send(send_event);
|
||||
processed_events.send(GamepadEvent::from(send_event));
|
||||
}
|
||||
@ -1649,130 +1768,161 @@ mod tests {
|
||||
|
||||
fn test_button_axis_settings_filter(
|
||||
settings: ButtonAxisSettings,
|
||||
new_value: f32,
|
||||
old_value: Option<f32>,
|
||||
new_raw_value: f32,
|
||||
old_raw_value: 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!(
|
||||
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]
|
||||
fn test_button_axis_settings_default_filter() {
|
||||
let cases = [
|
||||
// clamped
|
||||
(1.0, None, Some(1.0)),
|
||||
(0.99, None, Some(1.0)),
|
||||
(0.96, None, Some(1.0)),
|
||||
(0.95, None, Some(1.0)),
|
||||
(0.9499, None, Some(0.9499)),
|
||||
(0.84, None, Some(0.84)),
|
||||
(0.43, None, Some(0.43)),
|
||||
(0.05001, None, Some(0.05001)),
|
||||
// linearly rescaled from 0.05..=0.95 to 0.0..=1.0
|
||||
(0.9499, None, Some(0.9998889)),
|
||||
(0.84, None, Some(0.87777776)),
|
||||
(0.43, None, Some(0.42222223)),
|
||||
(0.05001, None, Some(0.000011109644)),
|
||||
// clamped
|
||||
(0.05, None, Some(0.0)),
|
||||
(0.04, None, Some(0.0)),
|
||||
(0.01, 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();
|
||||
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]
|
||||
fn test_button_axis_settings_default_filter_with_old_value() {
|
||||
fn test_button_axis_settings_default_filter_with_old_raw_value() {
|
||||
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.43), None),
|
||||
(0.43, Some(0.41999), Some(0.43)),
|
||||
(0.43, Some(0.17), Some(0.43)),
|
||||
(0.43, Some(0.84), Some(0.43)),
|
||||
(0.43, Some(0.41999), Some(0.42222223)),
|
||||
(0.43, Some(0.17), Some(0.42222223)),
|
||||
(0.43, Some(0.84), Some(0.42222223)),
|
||||
(0.05, Some(0.055), Some(0.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();
|
||||
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(
|
||||
settings: AxisSettings,
|
||||
new_value: f32,
|
||||
old_value: Option<f32>,
|
||||
new_raw_value: f32,
|
||||
old_raw_value: 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!(
|
||||
expected, actual,
|
||||
"Testing filtering for {settings:?} with new_value = {new_value:?}, old_value = {old_value:?}",
|
||||
expected, actual.map(|f| f.scaled.to_f32()),
|
||||
"Testing filtering for {settings:?} with new_raw_value = {new_raw_value:?}, old_raw_value = {old_raw_value:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_axis_settings_default_filter() {
|
||||
// new (raw), expected (rescaled linearly)
|
||||
let cases = [
|
||||
// high enough to round to 1.0
|
||||
(1.0, Some(1.0)),
|
||||
(0.99, Some(1.0)),
|
||||
(0.96, Some(1.0)),
|
||||
(0.95, Some(1.0)),
|
||||
(0.9499, Some(0.9499)),
|
||||
(0.84, Some(0.84)),
|
||||
(0.43, Some(0.43)),
|
||||
(0.05001, Some(0.05001)),
|
||||
// for the following, remember that 0.05 is the "low" value and 0.95 is the "high" value
|
||||
// barely below the high value means barely below 1 after scaling
|
||||
(0.9499, Some(0.9998889)), // scaled as: (0.9499 - 0.05) / (0.95 - 0.05)
|
||||
(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.04, Some(0.0)),
|
||||
(0.01, 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)),
|
||||
(-0.99, Some(-1.0)),
|
||||
(-0.96, Some(-1.0)),
|
||||
(-0.95, Some(-1.0)),
|
||||
(-0.9499, Some(-0.9499)),
|
||||
(-0.84, Some(-0.84)),
|
||||
(-0.43, Some(-0.43)),
|
||||
(-0.05001, Some(-0.05001)),
|
||||
// scaled inputs
|
||||
(-0.9499, Some(-0.9998889)), // scaled as: (-0.9499 - -0.05) / (-0.95 - -0.05)
|
||||
(-0.84, Some(-0.87777776)), // scaled as: (-0.84 - -0.05) / (-0.95 - -0.05)
|
||||
(-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.04, 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();
|
||||
test_axis_settings_filter(settings, new_value, None, expected);
|
||||
test_axis_settings_filter(settings, new_raw_value, None, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = [
|
||||
(0.43, Some(0.44001), Some(0.43)),
|
||||
(0.43, Some(0.44), None),
|
||||
(0.43, Some(0.43), None),
|
||||
(0.43, Some(0.41999), Some(0.43)),
|
||||
(0.43, Some(0.17), Some(0.43)),
|
||||
(0.43, Some(0.84), Some(0.43)),
|
||||
(0.05, Some(0.055), Some(0.0)),
|
||||
(0.95, Some(0.945), Some(1.0)),
|
||||
(-0.43, Some(-0.44001), Some(-0.43)),
|
||||
(-0.43, Some(-0.44), None),
|
||||
(-0.43, Some(-0.43), None),
|
||||
(-0.43, Some(-0.41999), Some(-0.43)),
|
||||
(-0.43, Some(-0.17), Some(-0.43)),
|
||||
(-0.43, Some(-0.84), Some(-0.43)),
|
||||
(-0.05, Some(-0.055), Some(0.0)),
|
||||
(-0.95, Some(-0.945), Some(-1.0)),
|
||||
// enough increase to change
|
||||
(0.43, Some(0.43 + threshold * 1.1), Some(0.42222223)),
|
||||
// enough decrease to change
|
||||
(0.43, Some(0.43 - threshold * 1.1), Some(0.42222223)),
|
||||
// not enough increase to change
|
||||
(0.43, Some(0.43 + threshold * 0.9), None),
|
||||
// not enough decrease to change
|
||||
(0.43, Some(0.43 - threshold * 0.9), None),
|
||||
// enough increase to change
|
||||
(-0.43, Some(-0.43 + threshold * 1.1), Some(-0.42222226)),
|
||||
// enough decrease to change
|
||||
(-0.43, Some(-0.43 - threshold * 1.1), Some(-0.42222226)),
|
||||
// not enough increase to change
|
||||
(-0.43, Some(-0.43 + threshold * 0.9), None),
|
||||
// not enough decrease to change
|
||||
(-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 {
|
||||
let settings = AxisSettings::new(-0.95, -0.05, 0.05, 0.95, 0.01).unwrap();
|
||||
test_axis_settings_filter(settings, new_value, old_value, expected);
|
||||
for (new_raw_value, old_raw_value, expected) in cases {
|
||||
let settings = AxisSettings::new(-0.95, -0.05, 0.05, 0.95, threshold).unwrap();
|
||||
test_axis_settings_filter(settings, new_raw_value, old_raw_value, expected);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user