Skip to content

Commit

Permalink
Rework modifiers logic
Browse files Browse the repository at this point in the history
- Values from `Input` are now converted to the action-level dimension only after applying all input-level modifiers and conditions. This allows things like mapping the Y-axis of `ActionValue::Axis2D` into an action with `ActionValueDim::Axis1D`.
  • Loading branch information
Shatur committed Oct 23, 2024
1 parent 842fe31 commit 32f7835
Show file tree
Hide file tree
Showing 12 changed files with 321 additions and 128 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Remove world access from conditions and modifiers. This means that you no longer can write game-specific conditions or modifiers. But it's much nicer (and faster) to just do it in observers instead.
- Values from `Input` are now converted to the action-level dimension only after applying all input-level modifiers and conditions. This allows things like mapping the Y-axis of `ActionValue::Axis2D` into an action with `ActionValueDim::Axis1D`.
- Modifiers are now allowed to change passed value dimensions.
- All built-in modifiers now handle values of any dimention.
- Replace `with_held_timer` with `relative_speed` that just accepts a boolean.
- Rename `HeldTimer` into `ConditionTimer`.
- Use Use `trace!` instead of `debug!` for triggered events.

### Removed

- `ignore_incompatible!` since no longer needed.

## [0.1.0] - 2024-10-20

Initial release.
Expand Down
3 changes: 2 additions & 1 deletion src/input_context/context_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use crate::{
/// 1.1. Apply input-level [`InputModifier`]s.
/// 1.2. Evaluate input-level [`InputCondition`]s, combining their results based on their [`InputCondition::kind`].
/// 2. Select all [`ActionValue`]s with the most significant [`ActionState`] and combine based on [`InputAction::ACCUMULATION`].
/// The value will also be converted using [`ActionValue::convert`] into [`InputAction::DIM`].
/// 3. Apply action level [`InputModifier`]s.
/// 4. Evaluate action level [`InputCondition`]s, combining their results according to [`InputCondition::kind`].
/// 5. Set the final [`ActionState`] based on the results.
Expand Down Expand Up @@ -264,7 +265,7 @@ impl ActionBind {
reader.set_consume_input(self.consume_input);
let mut tracker = TriggerTracker::new(ActionValue::zero(self.dim));
for binding in &mut self.bindings {
let value = reader.value(binding.input).convert(self.dim);
let value = reader.value(binding.input);
if binding.ignored {
// Ignore until we read zero for this mapping.
if value.as_bool() {
Expand Down
19 changes: 0 additions & 19 deletions src/input_context/input_modifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ use crate::action_value::ActionValue;
/// Input modifiers are useful for applying sensitivity settings, smoothing input over multiple frames,
/// or changing how input maps to axes.
///
/// Modifiers should preserve the original value dimention.
///
/// Can be applied both to inputs and actions.
/// See [`ActionBind::with_modifier`](super::context_instance::ActionBind::with_modifier)
/// and [`InputBind::with_modifier`](super::context_instance::InputBind::with_modifier).
Expand All @@ -29,20 +27,3 @@ pub trait InputModifier: Sync + Send + Debug + 'static {
/// Called each frame.
fn apply(&mut self, time: &Time<Virtual>, value: ActionValue) -> ActionValue;
}

/// Simple helper to emit a warning if a dimension is not compatible with a modifier.
///
/// We use a macro to make [`warn_once`](bevy::log::warn_once) print independently.
#[macro_export]
macro_rules! ignore_incompatible {
($value:expr) => {
warn_once!(
"trying to apply `{}` to a `{:?}` value, which is not possible",
std::any::type_name::<Self>(),
$value.dim(),
);
return $value
};
}

pub use ignore_incompatible;
20 changes: 13 additions & 7 deletions src/input_context/input_modifier/dead_zone.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use bevy::prelude::*;

use super::{ignore_incompatible, InputModifier};
use super::InputModifier;
use crate::action_value::ActionValue;

/// Input values within the range [Self::lower_threshold] -> [Self::upper_threshold] will be remapped from 0 -> 1.
/// Values outside this range will be clamped.
///
/// Can't be applied to [`ActionValue::Bool`].
/// [`ActionValue::Bool`] will be transformed into [`ActionValue::Axis1D`].
#[derive(Clone, Copy, Debug)]
pub struct DeadZone {
pub kind: DeadZoneKind,
Expand Down Expand Up @@ -57,8 +57,9 @@ impl Default for DeadZone {
impl InputModifier for DeadZone {
fn apply(&mut self, _time: &Time<Virtual>, value: ActionValue) -> ActionValue {
match value {
ActionValue::Bool(_) => {
ignore_incompatible!(value);
ActionValue::Bool(value) => {
let value = if value { 1.0 } else { 0.0 };
self.dead_zone(value).into()
}
ActionValue::Axis1D(value) => self.dead_zone(value).into(),
ActionValue::Axis2D(mut value) => match self.kind {
Expand Down Expand Up @@ -92,7 +93,8 @@ pub enum DeadZoneKind {
/// Apply dead zone logic to all axes simultaneously.
///
/// This gives smooth input (circular/spherical coverage).
/// For [`ActionValue::Axis1D`] this works identically to [`Self::Axial`].
/// For [`ActionValue::Axis1D`] and [`ActionValue::Bool`]
/// this works identically to [`Self::Axial`].
#[default]
Radial,
/// Apply dead zone to axes individually.
Expand All @@ -111,7 +113,9 @@ mod tests {
let mut modifier = DeadZone::new(DeadZoneKind::Radial);
let time = Time::default();

assert_eq!(modifier.apply(&time, true.into()), true.into());
assert_eq!(modifier.apply(&time, true.into()), 1.0.into());
assert_eq!(modifier.apply(&time, false.into()), 0.0.into());

assert_eq!(modifier.apply(&time, 1.0.into()), 1.0.into());
assert_eq!(modifier.apply(&time, 0.5.into()), 0.375.into());
assert_eq!(modifier.apply(&time, 0.2.into()), 0.0.into());
Expand Down Expand Up @@ -149,7 +153,9 @@ mod tests {
let mut modifier = DeadZone::new(DeadZoneKind::Axial);
let time = Time::default();

assert_eq!(modifier.apply(&time, true.into()), true.into());
assert_eq!(modifier.apply(&time, true.into()), 1.0.into());
assert_eq!(modifier.apply(&time, false.into()), 0.0.into());

assert_eq!(modifier.apply(&time, 1.0.into()), 1.0.into());
assert_eq!(modifier.apply(&time, 0.5.into()), 0.375.into());
assert_eq!(modifier.apply(&time, 0.2.into()), 0.0.into());
Expand Down
45 changes: 29 additions & 16 deletions src/input_context/input_modifier/exponential_curve.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
use bevy::prelude::*;

use super::{ignore_incompatible, InputModifier};
use crate::action_value::{ActionValue, ActionValueDim};
use super::InputModifier;
use crate::action_value::ActionValue;

/// Response curve exponential.
///
/// Apply a simple exponential response curve to input values, per axis.
///
/// Can't be applied to [`ActionValue::Bool`].
/// [`ActionValue::Bool`] will be transformed into [`ActionValue::Axis1D`].
#[derive(Clone, Copy, Debug)]
pub struct ExponentialCurve {
/// Curve exponent.
pub exponent: Vec3,
pub exp: Vec3,
}

impl ExponentialCurve {
Expand All @@ -22,26 +22,38 @@ impl ExponentialCurve {
}

#[must_use]
pub fn new(exponent: Vec3) -> Self {
Self { exponent }
pub fn new(exp: Vec3) -> Self {
Self { exp }
}
}

impl InputModifier for ExponentialCurve {
fn apply(&mut self, _time: &Time<Virtual>, value: ActionValue) -> ActionValue {
let dim = value.dim();
if dim == ActionValueDim::Bool {
ignore_incompatible!(value);
match value {
ActionValue::Bool(value) => {
let value = if value { 1.0 } else { 0.0 };
apply_exp(value, self.exp.x).into()
}
ActionValue::Axis1D(value) => apply_exp(value, self.exp.x).into(),
ActionValue::Axis2D(mut value) => {
value.x = apply_exp(value.x, self.exp.x);
value.y = apply_exp(value.y, self.exp.y);
value.into()
}
ActionValue::Axis3D(mut value) => {
value.x = apply_exp(value.x, self.exp.x);
value.y = apply_exp(value.y, self.exp.y);
value.z = apply_exp(value.z, self.exp.z);
value.into()
}
}

let mut value = value.as_axis3d();
value.x = value.x.signum() * value.x.abs().powf(self.exponent.x);
value.y = value.y.signum() * value.y.abs().powf(self.exponent.y);
value.z = value.z.signum() * value.z.abs().powf(self.exponent.z);
ActionValue::Axis3D(value).convert(dim)
}
}

fn apply_exp(value: f32, exp: f32) -> f32 {
value.abs().powf(exp).copysign(value)
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -51,7 +63,8 @@ mod tests {
let time = Time::default();
let mut modifier = ExponentialCurve::splat(2.0);

assert_eq!(modifier.apply(&time, true.into()), true.into());
assert_eq!(modifier.apply(&time, true.into()), 1.0.into());
assert_eq!(modifier.apply(&time, false.into()), 0.0.into());
assert_eq!(modifier.apply(&time, (-0.5).into()), (-0.25).into());
assert_eq!(modifier.apply(&time, 0.5.into()), 0.25.into());
assert_eq!(
Expand Down
101 changes: 86 additions & 15 deletions src/input_context/input_modifier/negate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use crate::action_value::ActionValue;
/// Inverts value per axis.
///
/// By default, all axes are inverted.
///
/// [`ActionValue::Bool`] will be transformed into [`ActionValue::Axis1D`].
#[derive(Clone, Copy, Debug)]
pub struct Negate {
/// Wheter to inverse the X axis.
Expand Down Expand Up @@ -67,13 +69,41 @@ impl Default for Negate {
}

impl InputModifier for Negate {
fn apply(&mut self, _time: &Time<Virtual>, value: ActionValue) -> ActionValue {
let x = if self.x { -1.0 } else { 1.0 };
let y = if self.y { -1.0 } else { 1.0 };
let z = if self.z { -1.0 } else { 1.0 };
let negated = value.as_axis3d() * Vec3::new(x, y, z);

ActionValue::Axis3D(negated).convert(value.dim())
fn apply(&mut self, time: &Time<Virtual>, value: ActionValue) -> ActionValue {
match value {
ActionValue::Bool(value) => {
let value = if value { 1.0 } else { 0.0 };
self.apply(time, value.into())
}
ActionValue::Axis1D(value) => {
if self.x {
(-value).into()
} else {
value.into()
}
}
ActionValue::Axis2D(mut value) => {
if self.x {
value.x = -value.x;
}
if self.y {
value.y = -value.y;
}
value.into()
}
ActionValue::Axis3D(mut value) => {
if self.x {
value.x = -value.x;
}
if self.y {
value.y = -value.y;
}
if self.z {
value.z = -value.z;
}
value.into()
}
}
}
}

Expand All @@ -82,24 +112,65 @@ mod tests {
use super::*;

#[test]
fn negation() {
fn x() {
let mut modifier = Negate::x(true);
let time = Time::default();

assert_eq!(modifier.apply(&time, true.into()), (-1.0).into());
assert_eq!(modifier.apply(&time, false.into()), 0.0.into());
assert_eq!(modifier.apply(&time, 0.5.into()), (-0.5).into());
assert_eq!(modifier.apply(&time, Vec2::ONE.into()), (-1.0, 1.0).into(),);
assert_eq!(
Negate::default().apply(&time, Vec3::ONE.into()),
Vec3::NEG_ONE.into(),
);
assert_eq!(
Negate::x(true).apply(&time, Vec3::ONE.into()),
modifier.apply(&time, Vec3::ONE.into()),
(-1.0, 1.0, 1.0).into(),
);
}

#[test]
fn y() {
let mut modifier = Negate::y(true);
let time = Time::default();

assert_eq!(modifier.apply(&time, true.into()), 1.0.into());
assert_eq!(modifier.apply(&time, false.into()), 0.0.into());
assert_eq!(modifier.apply(&time, 0.5.into()), 0.5.into());
assert_eq!(modifier.apply(&time, Vec2::ONE.into()), (1.0, -1.0).into(),);
assert_eq!(
Negate::y(true).apply(&time, Vec3::ONE.into()),
modifier.apply(&time, Vec3::ONE.into()),
(1.0, -1.0, 1.0).into(),
);
}

#[test]
fn z() {
let mut modifier = Negate::z(true);
let time = Time::default();

assert_eq!(modifier.apply(&time, true.into()), 1.0.into());
assert_eq!(modifier.apply(&time, false.into()), 0.0.into());
assert_eq!(modifier.apply(&time, 0.5.into()), 0.5.into());
assert_eq!(modifier.apply(&time, Vec2::ONE.into()), Vec2::ONE.into(),);
assert_eq!(
Negate::z(true).apply(&time, Vec3::ONE.into()),
modifier.apply(&time, Vec3::ONE.into()),
(1.0, 1.0, -1.0).into(),
);
}

#[test]
fn all() {
let mut modifier = Negate::default();
let time = Time::default();

assert_eq!(modifier.apply(&time, true.into()), (-1.0).into());
assert_eq!(modifier.apply(&time, false.into()), 0.0.into());
assert_eq!(modifier.apply(&time, 0.5.into()), (-0.5).into());
assert_eq!(
modifier.apply(&time, Vec2::ONE.into()),
Vec2::NEG_ONE.into(),
);
assert_eq!(
modifier.apply(&time, Vec3::ONE.into()),
Vec3::NEG_ONE.into(),
);
}
}
28 changes: 18 additions & 10 deletions src/input_context/input_modifier/normalize.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
use bevy::prelude::*;

use super::{ignore_incompatible, InputModifier};
use crate::action_value::{ActionValue, ActionValueDim};
use super::InputModifier;
use crate::action_value::ActionValue;

/// Normalizes input if possible or returns zero.
///
/// Does nothing for [`ActionValue::Bool`].
#[derive(Clone, Copy, Debug)]
pub struct Normalize;

impl InputModifier for Normalize {
fn apply(&mut self, _time: &Time<Virtual>, value: ActionValue) -> ActionValue {
let dim = value.dim();
if dim == ActionValueDim::Bool || dim == ActionValueDim::Axis1D {
ignore_incompatible!(value);
match value {
ActionValue::Bool(_) => value,
ActionValue::Axis1D(value) => {
if value != 0.0 {
1.0.into()
} else {
value.into()
}
}
ActionValue::Axis2D(value) => value.normalize_or_zero().into(),
ActionValue::Axis3D(value) => value.normalize_or_zero().into(),
}

let normalized = value.as_axis3d().normalize_or_zero();
ActionValue::Axis3D(normalized).convert(dim)
}
}

Expand All @@ -28,8 +35,9 @@ mod tests {
let time = Time::default();

assert_eq!(Normalize.apply(&time, true.into()), true.into());
assert_eq!(Normalize.apply(&time, 0.5.into()), 0.5.into());
assert_eq!(Normalize.apply(&time, Vec2::ZERO.into()), Vec2::ZERO.into());
assert_eq!(Normalize.apply(&time, false.into()), false.into());
assert_eq!(Normalize.apply(&time, 0.5.into()), 1.0.into());
assert_eq!(Normalize.apply(&time, 0.0.into()), 0.0.into());
assert_eq!(
Normalize.apply(&time, Vec2::ONE.into()),
Vec2::ONE.normalize_or_zero().into(),
Expand Down
Loading

0 comments on commit 32f7835

Please sign in to comment.