From 7c0117ce93e698bdfeb4c5f1969bd5630714b55a Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 24 Jan 2024 15:37:10 -0500 Subject: [PATCH] Make the `action_state` module less huge (#453) * Split out timing code * Split out action driver code * Split out action_diff code * Tweak release notes * Doc links * More doc linkage * One last doc link plz --------- Co-authored-by: Alice Cecile --- RELEASES.md | 10 +- benches/action_state.rs | 4 +- examples/send_actions_over_network.rs | 2 +- src/action_diff.rs | 60 ++++ src/action_driver.rs | 253 +++++++++++++++ src/action_state.rs | 425 +------------------------- src/lib.rs | 6 +- src/plugin.rs | 7 +- src/systems.rs | 13 +- src/timing.rs | 151 +++++++++ tests/action_diffs.rs | 6 +- 11 files changed, 495 insertions(+), 442 deletions(-) create mode 100644 src/action_diff.rs create mode 100644 src/action_driver.rs create mode 100644 src/timing.rs diff --git a/RELEASES.md b/RELEASES.md index b781b916..9ea020b9 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -23,7 +23,7 @@ - added `InputMap::clear` - fixed [a bug](https://github.com/Leafwing-Studios/leafwing-input-manager/issues/430) related to incorrect axis data in `Chord` when not all buttons are pressed. -### Code quality +### Code Quality - all non-insertion methods now take `&A: Actionlike` rather than `A: Actionlike` to avoid pointless cloning - removed `multimap` dependency in favor of regular `HashMap` which allowed to derive `Reflect` for `InputMap` @@ -34,7 +34,11 @@ - `InputMap::which_pressed` now returns a `HashMap` - `handle_clashes` now takes a `HashMap` - `ClashStrategy::UseActionOrder` has been removed - +- the `action_state` module has been pared down to something more reasonable in scope: + - timing-related code now lives in its own `timing` module + - `ActionStateDriver` code now lives in its own `action_driver` module + - `ActionDiff`-related code now lives in its own `action_diff` module + ## Version 0.11.2 - fixed [a bug](https://github.com/Leafwing-Studios/leafwing-input-manager/issues/285) with mouse motion and mouse wheel events being improperly counted @@ -51,7 +55,7 @@ ## Version 0.11 -### Known issues +### Known Issues - `bevy_egui` integration and the `egui` feature flag have been temporarily removed to ensure a timely release - gamepad input mocking is not completely functional due to upstream changes: see [#407](https://github.com/Leafwing-Studios/leafwing-input-manager/issues/407) diff --git a/benches/action_state.rs b/benches/action_state.rs index 7bbed104..7c194175 100644 --- a/benches/action_state.rs +++ b/benches/action_state.rs @@ -1,9 +1,7 @@ use bevy::{prelude::Reflect, utils::HashMap}; use criterion::{criterion_group, criterion_main, Criterion}; use leafwing_input_manager::{ - action_state::{ActionData, Timing}, - buttonlike::ButtonState, - prelude::ActionState, + action_state::ActionData, buttonlike::ButtonState, prelude::ActionState, timing::Timing, Actionlike, }; diff --git a/examples/send_actions_over_network.rs b/examples/send_actions_over_network.rs index 9bf6f3a5..525ea12d 100644 --- a/examples/send_actions_over_network.rs +++ b/examples/send_actions_over_network.rs @@ -9,7 +9,7 @@ use bevy::ecs::event::{Events, ManualEventReader}; use bevy::input::InputPlugin; use bevy::prelude::*; -use leafwing_input_manager::action_state::ActionDiffEvent; +use leafwing_input_manager::action_diff::ActionDiffEvent; use leafwing_input_manager::prelude::*; use leafwing_input_manager::systems::generate_action_diffs; diff --git a/src/action_diff.rs b/src/action_diff.rs new file mode 100644 index 00000000..31ffb902 --- /dev/null +++ b/src/action_diff.rs @@ -0,0 +1,60 @@ +//! Serialization-friendly representation of changes to [`ActionState`](crate::action_state::ActionState). +//! +//! These are predominantly intended for use in networked games, where the server needs to know what the players are doing, +//! and would like a compact, semantically-meaningful representation of the changes to the game state without needing to know +//! about things like keybindings or input devices. + +use bevy::{ + ecs::{entity::Entity, event::Event}, + math::Vec2, +}; +use serde::{Deserialize, Serialize}; + +use crate::Actionlike; + +/// Stores presses and releases of buttons without timing information +/// +/// These are typically accessed using the `Events` resource. +/// Uses a minimal storage format, in order to facilitate transport over the network. +/// +/// An `ActionState` can be fully reconstructed from a stream of `ActionDiff`. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum ActionDiff { + /// The action was pressed + Pressed { + /// The value of the action + action: A, + }, + /// The action was released + Released { + /// The value of the action + action: A, + }, + /// The value of the action changed + ValueChanged { + /// The value of the action + action: A, + /// The new value of the action + value: f32, + }, + /// The axis pair of the action changed + AxisPairChanged { + /// The value of the action + action: A, + /// The new value of the axis + axis_pair: Vec2, + }, +} + +/// Will store an `ActionDiff` as well as what generated it (either an Entity, or nothing if the +/// input actions are represented by a `Resource`) +/// +/// These are typically accessed using the `Events` resource. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Event)] +pub struct ActionDiffEvent { + /// If some: the entity that has the `ActionState` component + /// If none: `ActionState` is a Resource, not a component + pub owner: Option, + /// The `ActionDiff` that was generated + pub action_diffs: Vec>, +} diff --git a/src/action_driver.rs b/src/action_driver.rs new file mode 100644 index 00000000..7a8c5cc1 --- /dev/null +++ b/src/action_driver.rs @@ -0,0 +1,253 @@ +//! Tools to control the [`ActionState`] of other entities automatically from other entities. + +#[allow(unused_imports)] // For the documentation +use crate::action_state::ActionState; + +use bevy::utils::hashbrown::hash_set::Iter; +use std::iter::Once; + +use bevy::{ + ecs::{component::Component, entity::Entity}, + utils::HashSet, +}; + +use crate::Actionlike; + +/// A component that allows the attached entity to drive the [`ActionState`] of the associated entity +/// +/// # Examples +/// +/// By default, [`update_action_state_from_interaction`](crate::systems::update_action_state_from_interaction) uses this component +/// in order to connect `bevy::ui` buttons to the corresponding `ActionState`. +/// +/// ```rust +/// use bevy::prelude::*; +/// use leafwing_input_manager::prelude::*; +/// +/// #[derive(Actionlike, PartialEq, Eq, Hash, Clone, Copy, Reflect)] +/// enum DanceDance { +/// Left, +/// Right, +/// Up, +/// Down, +/// } +/// +/// // Spawn entity to track dance inputs +/// let mut world = World::new(); +/// let dance_tracker = world +/// .spawn(ActionState::::default()) +/// .id(); +/// +/// // Spawn a button, which is wired up to the dance tracker +/// // When used with InputManagerPlugin, this button will press the DanceDance::Left action when it is pressed. +/// world +/// .spawn(ButtonBundle::default()) +/// // This component links the button to the entity with the `ActionState` component +/// .insert(ActionStateDriver { +/// action: DanceDance::Left, +/// targets: dance_tracker.into(), +/// }); +///``` +/// +/// Writing your own systems that use the [`ActionStateDriver`] component is easy, +/// although this should be reserved for cases where the entity whose value you want to check +/// is distinct from the entity whose [`ActionState`] you want to set. +/// Check the source code of [`update_action_state_from_interaction`](crate::systems::update_action_state_from_interaction) for an example of how this is done. +#[derive(Debug, Component, Clone, PartialEq, Eq)] +pub struct ActionStateDriver { + /// The action triggered by this entity + pub action: A, + /// The entity whose action state should be updated + pub targets: ActionStateDriverTarget, +} + +/// Represents the entities that an ``ActionStateDriver`` targets. +#[derive(Debug, Component, Clone, PartialEq, Eq)] +pub enum ActionStateDriverTarget { + /// No targets + None, + /// Single target + Single(Entity), + /// Multiple targets + Multi(HashSet), +} + +impl ActionStateDriverTarget { + /// Get an iterator for the entities targeted. + #[inline(always)] + pub fn iter(&self) -> impl Iterator { + match self { + Self::None => ActionStateDriverTargetIterator::None, + Self::Single(entity) => { + ActionStateDriverTargetIterator::Single(std::iter::once(entity)) + } + Self::Multi(entities) => ActionStateDriverTargetIterator::Multi(entities.iter()), + } + } + + /// Insert an entity as a target. + #[inline(always)] + pub fn insert(&mut self, entity: Entity) { + // Don't want to copy a bunch of logic, switch out the ref, then replace it + // rust doesn't like in place replacement + *self = std::mem::replace(self, Self::None).with(entity); + } + + /// Remove an entity as a target if it's in the target set. + #[inline(always)] + pub fn remove(&mut self, entity: Entity) { + // see insert + *self = std::mem::replace(self, Self::None).without(entity); + } + + /// Add an entity as a target. + #[inline(always)] + pub fn add(&mut self, entities: impl Iterator) { + for entity in entities { + self.insert(entity) + } + } + + /// Get the number of targets. + #[inline(always)] + pub fn len(&self) -> usize { + match self { + Self::None => 0, + Self::Single(_) => 1, + Self::Multi(targets) => targets.len(), + } + } + + /// Returns true if there are no targets. + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Add an entity as a target using a builder style pattern. + #[inline(always)] + pub fn with(mut self, entity: Entity) -> Self { + match self { + Self::None => Self::Single(entity), + Self::Single(og) => Self::Multi(HashSet::from([og, entity])), + Self::Multi(ref mut targets) => { + targets.insert(entity); + self + } + } + } + + /// Remove an entity as a target if it's in the set using a builder style pattern. + pub fn without(self, entity: Entity) -> Self { + match self { + Self::None => Self::None, + Self::Single(_) => Self::None, + Self::Multi(mut targets) => { + targets.remove(&entity); + Self::from_iter(targets) + } + } + } +} + +impl From for ActionStateDriverTarget { + fn from(value: Entity) -> Self { + Self::Single(value) + } +} + +impl From<()> for ActionStateDriverTarget { + fn from(_value: ()) -> Self { + Self::None + } +} + +impl FromIterator for ActionStateDriverTarget { + fn from_iter>(iter: T) -> Self { + let entities = HashSet::from_iter(iter); + + match entities.len() { + 0 => Self::None, + 1 => Self::Single(entities.into_iter().next().unwrap()), + _ => Self::Multi(entities), + } + } +} + +impl<'a> FromIterator<&'a Entity> for ActionStateDriverTarget { + fn from_iter>(iter: T) -> Self { + let entities = HashSet::from_iter(iter.into_iter().cloned()); + + match entities.len() { + 0 => Self::None, + 1 => Self::Single(entities.into_iter().next().unwrap()), + _ => Self::Multi(entities), + } + } +} + +enum ActionStateDriverTargetIterator<'a> { + None, + Single(Once<&'a Entity>), + Multi(Iter<'a, Entity>), +} + +impl<'a> Iterator for ActionStateDriverTargetIterator<'a> { + type Item = &'a Entity; + + fn next(&mut self) -> Option { + match self { + Self::None => None, + Self::Single(iter) => iter.next(), + Self::Multi(iter) => iter.next(), + } + } +} + +#[cfg(test)] +mod tests { + use super::ActionStateDriverTarget; + use bevy::prelude::Entity; + + #[test] + fn action_state_driver_targets() { + let mut target = ActionStateDriverTarget::from(()); + + assert_eq!(0, target.len()); + + target.insert(Entity::from_raw(0)); + assert_eq!(1, target.len()); + + target.insert(Entity::from_raw(1)); + assert_eq!(2, target.len()); + + target.remove(Entity::from_raw(0)); + assert_eq!(1, target.len()); + + target.remove(Entity::from_raw(1)); + assert_eq!(0, target.len()); + + target = target.with(Entity::from_raw(0)); + assert_eq!(1, target.len()); + + target = target.without(Entity::from_raw(0)); + assert_eq!(0, target.len()); + + target.add( + [ + Entity::from_raw(0), + Entity::from_raw(1), + Entity::from_raw(2), + ] + .iter() + .cloned(), + ); + assert_eq!(3, target.len()); + + let mut sum = 0; + for entity in target.iter() { + sum += entity.index(); + } + assert_eq!(3, sum); + } +} diff --git a/src/action_state.rs b/src/action_state.rs index 1c4a59a7..2003abb4 100644 --- a/src/action_state.rs +++ b/src/action_state.rs @@ -1,16 +1,15 @@ //! This module contains [`ActionState`] and its supporting methods and impls. +use crate::action_diff::ActionDiff; +use crate::timing::Timing; use crate::Actionlike; use crate::{axislike::DualAxisData, buttonlike::ButtonState}; -use bevy::ecs::{component::Component, entity::Entity}; -use bevy::math::Vec2; -use bevy::prelude::{Event, Resource}; +use bevy::ecs::component::Component; +use bevy::prelude::Resource; use bevy::reflect::Reflect; -use bevy::utils::hashbrown::hash_set::Iter; -use bevy::utils::{Duration, Entry, HashMap, HashSet, Instant}; +use bevy::utils::{Duration, Entry, HashMap, Instant}; use serde::{Deserialize, Serialize}; -use std::iter::Once; /// Metadata about an [`Actionlike`] action /// @@ -605,302 +604,13 @@ impl ActionState { } } -/// A component that allows the attached entity to drive the [`ActionState`] of the associated entity -/// -/// # Examples -/// -/// By default, [`update_action_state_from_interaction`](crate::systems::update_action_state_from_interaction) uses this component -/// in order to connect `bevy::ui` buttons to the corresponding `ActionState`. -/// -/// ```rust -/// use bevy::prelude::*; -/// use leafwing_input_manager::prelude::*; -/// -/// #[derive(Actionlike, PartialEq, Eq, Hash, Clone, Copy, Reflect)] -/// enum DanceDance { -/// Left, -/// Right, -/// Up, -/// Down, -/// } -/// -/// // Spawn entity to track dance inputs -/// let mut world = World::new(); -/// let dance_tracker = world -/// .spawn(ActionState::::default()) -/// .id(); -/// -/// // Spawn a button, which is wired up to the dance tracker -/// // When used with InputManagerPlugin, this button will press the DanceDance::Left action when it is pressed. -/// world -/// .spawn(ButtonBundle::default()) -/// // This component links the button to the entity with the `ActionState` component -/// .insert(ActionStateDriver { -/// action: DanceDance::Left, -/// targets: dance_tracker.into(), -/// }); -///``` -/// -/// Writing your own systems that use the [`ActionStateDriver`] component is easy, -/// although this should be reserved for cases where the entity whose value you want to check -/// is distinct from the entity whose [`ActionState`] you want to set. -/// Check the source code of [`update_action_state_from_interaction`](crate::systems::update_action_state_from_interaction) for an example of how this is done. -#[derive(Debug, Component, Clone, PartialEq, Eq)] -pub struct ActionStateDriver { - /// The action triggered by this entity - pub action: A, - /// The entity whose action state should be updated - pub targets: ActionStateDriverTarget, -} - -/// Represents the entities that an ``ActionStateDriver`` targets. -#[derive(Debug, Component, Clone, PartialEq, Eq)] -pub enum ActionStateDriverTarget { - /// No targets - None, - /// Single target - Single(Entity), - /// Multiple targets - Multi(HashSet), -} - -impl ActionStateDriverTarget { - /// Get an iterator for the entities targeted. - #[inline(always)] - pub fn iter(&self) -> impl Iterator { - match self { - Self::None => ActionStateDriverTargetIterator::None, - Self::Single(entity) => { - ActionStateDriverTargetIterator::Single(std::iter::once(entity)) - } - Self::Multi(entities) => ActionStateDriverTargetIterator::Multi(entities.iter()), - } - } - - /// Insert an entity as a target. - #[inline(always)] - pub fn insert(&mut self, entity: Entity) { - // Don't want to copy a bunch of logic, switch out the ref, then replace it - // rust doesn't like in place replacement - *self = std::mem::replace(self, Self::None).with(entity); - } - - /// Remove an entity as a target if it's in the target set. - #[inline(always)] - pub fn remove(&mut self, entity: Entity) { - // see insert - *self = std::mem::replace(self, Self::None).without(entity); - } - - /// Add an entity as a target. - #[inline(always)] - pub fn add(&mut self, entities: impl Iterator) { - for entity in entities { - self.insert(entity) - } - } - - /// Get the number of targets. - #[inline(always)] - pub fn len(&self) -> usize { - match self { - Self::None => 0, - Self::Single(_) => 1, - Self::Multi(targets) => targets.len(), - } - } - - /// Returns true if there are no targets. - #[inline(always)] - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Add an entity as a target using a builder style pattern. - #[inline(always)] - pub fn with(mut self, entity: Entity) -> Self { - match self { - Self::None => Self::Single(entity), - Self::Single(og) => Self::Multi(HashSet::from([og, entity])), - Self::Multi(ref mut targets) => { - targets.insert(entity); - self - } - } - } - - /// Remove an entity as a target if it's in the set using a builder style pattern. - pub fn without(self, entity: Entity) -> Self { - match self { - Self::None => Self::None, - Self::Single(_) => Self::None, - Self::Multi(mut targets) => { - targets.remove(&entity); - Self::from_iter(targets) - } - } - } -} - -impl From for ActionStateDriverTarget { - fn from(value: Entity) -> Self { - Self::Single(value) - } -} - -impl From<()> for ActionStateDriverTarget { - fn from(_value: ()) -> Self { - Self::None - } -} - -impl FromIterator for ActionStateDriverTarget { - fn from_iter>(iter: T) -> Self { - let entities = HashSet::from_iter(iter); - - match entities.len() { - 0 => Self::None, - 1 => Self::Single(entities.into_iter().next().unwrap()), - _ => Self::Multi(entities), - } - } -} - -impl<'a> FromIterator<&'a Entity> for ActionStateDriverTarget { - fn from_iter>(iter: T) -> Self { - let entities = HashSet::from_iter(iter.into_iter().cloned()); - - match entities.len() { - 0 => Self::None, - 1 => Self::Single(entities.into_iter().next().unwrap()), - _ => Self::Multi(entities), - } - } -} - -enum ActionStateDriverTargetIterator<'a> { - None, - Single(Once<&'a Entity>), - Multi(Iter<'a, Entity>), -} - -impl<'a> Iterator for ActionStateDriverTargetIterator<'a> { - type Item = &'a Entity; - - fn next(&mut self) -> Option { - match self { - Self::None => None, - Self::Single(iter) => iter.next(), - Self::Multi(iter) => iter.next(), - } - } -} - -/// Stores information about when an action was pressed or released -/// -/// This struct is principally used as a field on [`ActionData`], -/// which itself lives inside an [`ActionState`]. -#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, Reflect)] -pub struct Timing { - /// The [`Instant`] at which the button was pressed or released - /// Recorded as the [`Time`](bevy::time::Time) at the start of the tick after the state last changed. - /// If this is none, [`Timing::tick`] has not been called yet. - #[serde(skip)] - pub instant_started: Option, - /// The [`Duration`] for which the button has been pressed or released. - /// - /// This begins at [`Duration::ZERO`] when [`ActionState::update`] is called. - pub current_duration: Duration, - /// The [`Duration`] for which the button was pressed or released before the state last changed. - pub previous_duration: Duration, -} - -impl PartialOrd for Timing { - fn partial_cmp(&self, other: &Self) -> Option { - self.current_duration.partial_cmp(&other.current_duration) - } -} - -impl Timing { - /// Advances the `current_duration` of this timer - /// - /// If the `instant_started` is None, it will be set to the current time. - /// This design allows us to ensure that the timing is always synchronized with the start of each frame. - pub fn tick(&mut self, current_instant: Instant, previous_instant: Instant) { - if let Some(instant_started) = self.instant_started { - self.current_duration = current_instant - instant_started; - } else { - self.current_duration = current_instant - previous_instant; - self.instant_started = Some(previous_instant); - } - } - - /// Flips the metaphorical hourglass, storing `current_duration` in `previous_duration` and resetting `instant_started` - /// - /// This method is called whenever actions are pressed or released - pub fn flip(&mut self) { - self.previous_duration = self.current_duration; - self.current_duration = Duration::ZERO; - self.instant_started = None; - } -} - -/// Will store an `ActionDiff` as well as what generated it (either an Entity, or nothing if the -/// input actions are represented by a `Resource`) -/// -/// These are typically accessed using the `Events` resource. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Event)] -pub struct ActionDiffEvent { - /// If some: the entity that has the `ActionState` component - /// If none: `ActionState` is a Resource, not a component - pub owner: Option, - /// The `ActionDiff` that was generated - pub action_diffs: Vec>, -} - -/// Stores presses and releases of buttons without timing information -/// -/// These are typically accessed using the `Events` resource. -/// Uses a minimal storage format, in order to facilitate transport over the network. -/// -/// An `ActionState` can be fully reconstructed from a stream of `ActionDiff`. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum ActionDiff { - /// The action was pressed - Pressed { - /// The value of the action - action: A, - }, - /// The action was released - Released { - /// The value of the action - action: A, - }, - /// The value of the action changed - ValueChanged { - /// The value of the action - action: A, - /// The new value of the action - value: f32, - }, - /// The axis pair of the action changed - AxisPairChanged { - /// The value of the action - action: A, - /// The new value of the axis - axis_pair: Vec2, - }, -} - #[cfg(test)] mod tests { use crate as leafwing_input_manager; use crate::input_mocking::MockInput; - use bevy::prelude::{Entity, Reflect}; + use bevy::prelude::Reflect; use leafwing_input_manager_macros::Actionlike; - use super::ActionStateDriverTarget; - #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug, Reflect)] enum Action { Run, @@ -980,127 +690,4 @@ mod tests { assert!(action_state.released(&Action::Run)); assert!(!action_state.just_released(&Action::Run)); } - - #[test] - fn time_tick_ticks_away() { - use crate::action_state::ActionState; - use bevy::utils::{Duration, Instant}; - - let mut action_state = ActionState::::default(); - - // Actions start released (but not just released) - assert!(action_state.released(&Action::Run)); - assert!(!action_state.just_released(&Action::Jump)); - - // Ticking causes buttons that were just released to no longer be just released - action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); - assert!(action_state.released(&Action::Jump)); - assert!(!action_state.just_released(&Action::Jump)); - action_state.press(&Action::Jump); - assert!(action_state.just_pressed(&Action::Jump)); - - // Ticking causes buttons that were just pressed to no longer be just pressed - action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); - assert!(action_state.pressed(&Action::Jump)); - assert!(!action_state.just_pressed(&Action::Jump)); - } - - #[test] - fn durations() { - use crate::action_state::ActionState; - use bevy::utils::{Duration, Instant}; - - let mut action_state = ActionState::::default(); - - // Actions start released - assert!(action_state.released(&Action::Jump)); - assert_eq!(action_state.instant_started(&Action::Jump), None,); - assert_eq!(action_state.current_duration(&Action::Jump), Duration::ZERO); - assert_eq!( - action_state.previous_duration(&Action::Jump), - Duration::ZERO - ); - - // Pressing a button swaps the state - action_state.press(&Action::Jump); - assert!(action_state.pressed(&Action::Jump)); - assert_eq!(action_state.instant_started(&Action::Jump), None); - assert_eq!(action_state.current_duration(&Action::Jump), Duration::ZERO); - assert_eq!( - action_state.previous_duration(&Action::Jump), - Duration::ZERO - ); - - // Ticking time sets the instant for the new state - let t0 = Instant::now(); - let t1 = t0 + Duration::new(1, 0); - - action_state.tick(t1, t0); - assert_eq!(action_state.instant_started(&Action::Jump), Some(t0)); - assert_eq!(action_state.current_duration(&Action::Jump), t1 - t0); - assert_eq!( - action_state.previous_duration(&Action::Jump), - Duration::ZERO - ); - - // Time passes - let t2 = t1 + Duration::new(5, 0); - - // The duration is updated - action_state.tick(t2, t1); - assert_eq!(action_state.instant_started(&Action::Jump), Some(t0)); - assert_eq!(action_state.current_duration(&Action::Jump), t2 - t0); - assert_eq!( - action_state.previous_duration(&Action::Jump), - Duration::ZERO - ); - - // Releasing again, swapping the current duration to the previous one - action_state.release(&Action::Jump); - assert_eq!(action_state.instant_started(&Action::Jump), None); - assert_eq!(action_state.current_duration(&Action::Jump), Duration::ZERO); - assert_eq!(action_state.previous_duration(&Action::Jump), t2 - t0); - } - - #[test] - fn action_state_driver_targets() { - let mut target = ActionStateDriverTarget::from(()); - - assert_eq!(0, target.len()); - - target.insert(Entity::from_raw(0)); - assert_eq!(1, target.len()); - - target.insert(Entity::from_raw(1)); - assert_eq!(2, target.len()); - - target.remove(Entity::from_raw(0)); - assert_eq!(1, target.len()); - - target.remove(Entity::from_raw(1)); - assert_eq!(0, target.len()); - - target = target.with(Entity::from_raw(0)); - assert_eq!(1, target.len()); - - target = target.without(Entity::from_raw(0)); - assert_eq!(0, target.len()); - - target.add( - [ - Entity::from_raw(0), - Entity::from_raw(1), - Entity::from_raw(2), - ] - .iter() - .cloned(), - ); - assert_eq!(3, target.len()); - - let mut sum = 0; - for entity in target.iter() { - sum += entity.index(); - } - assert_eq!(3, sum); - } } diff --git a/src/lib.rs b/src/lib.rs index de7d5a9d..e20316f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ use bevy::ecs::prelude::*; use bevy::reflect::{FromReflect, Reflect, TypePath}; use std::hash::Hash; +pub mod action_diff; +pub mod action_driver; pub mod action_state; pub mod axislike; pub mod buttonlike; @@ -23,6 +25,7 @@ pub mod orientation; pub mod plugin; pub mod scan_codes; pub mod systems; +pub mod timing; pub mod user_input; // Importing the derive macro @@ -30,7 +33,8 @@ pub use leafwing_input_manager_macros::Actionlike; /// Everything you need to get started pub mod prelude { - pub use crate::action_state::{ActionState, ActionStateDriver}; + pub use crate::action_driver::ActionStateDriver; + pub use crate::action_state::ActionState; pub use crate::axislike::{ DeadZoneShape, DualAxis, MouseWheelAxisType, SingleAxis, VirtualDPad, }; diff --git a/src/plugin.rs b/src/plugin.rs index f4073f74..858ee47e 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,6 +1,6 @@ //! Contains main plugin exported by this crate. -use crate::action_state::{ActionData, ActionState, Timing}; +use crate::action_state::{ActionData, ActionState}; use crate::axislike::{ AxisType, DeadZoneShape, DualAxis, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, SingleAxis, VirtualAxis, VirtualDPad, @@ -8,6 +8,7 @@ use crate::axislike::{ use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; use crate::clashing_inputs::ClashStrategy; use crate::input_map::InputMap; +use crate::timing::Timing; use crate::user_input::{InputKind, Modifier, UserInput}; use crate::Actionlike; use core::hash::Hash; @@ -49,7 +50,7 @@ use bevy::ui::UiSystem; /// - [`tick_action_state`](crate::systems::tick_action_state), which resets the `pressed` and `just_pressed` fields of the [`ActionState`] each frame /// - [`update_action_state`](crate::systems::update_action_state), which collects [`Input`](bevy::input::Input) resources to update the [`ActionState`] /// - [`update_action_state_from_interaction`](crate::systems::update_action_state_from_interaction), for triggering actions from buttons -/// - powers the [`ActionStateDriver`](crate::action_state::ActionStateDriver) component based on an [`Interaction`](bevy::ui::Interaction) component +/// - powers the [`ActionStateDriver`](crate::action_driver::ActionStateDriver) component based on an [`Interaction`](bevy::ui::Interaction) component /// - [`release_on_disable`](crate::systems::release_on_disable), which resets action states when [`ToggleActions`] is flipped, to avoid persistent presses. pub struct InputManagerPlugin { _phantom: PhantomData, @@ -71,7 +72,7 @@ impl InputManagerPlugin { /// /// Inputs will not be processed; instead, [`ActionState`] /// should be copied directly from the state provided by the client, - /// or constructed from [`ActionDiff`](crate::action_state::ActionDiff) event streams. + /// or constructed from [`ActionDiff`](crate::action_diff::ActionDiff) event streams. #[must_use] pub fn server() -> Self { Self { diff --git a/src/systems.rs b/src/systems.rs index b9916ccb..bad94b99 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,14 +1,10 @@ //! The systems that power each [`InputManagerPlugin`](crate::plugin::InputManagerPlugin). #[cfg(feature = "ui")] -use crate::action_state::ActionStateDriver; +use crate::action_driver::ActionStateDriver; use crate::{ - action_state::{ActionDiff, ActionState}, - clashing_inputs::ClashStrategy, - input_map::InputMap, - input_streams::InputStreams, - plugin::ToggleActions, - Actionlike, + action_state::ActionState, clashing_inputs::ClashStrategy, input_map::InputMap, + input_streams::InputStreams, plugin::ToggleActions, Actionlike, }; use bevy::{ecs::prelude::*, prelude::ScanCode}; @@ -25,7 +21,8 @@ use bevy::{ utils::{HashMap, Instant}, }; -use crate::action_state::ActionDiffEvent; +use crate::action_diff::{ActionDiff, ActionDiffEvent}; + #[cfg(feature = "ui")] use bevy::ui::Interaction; #[cfg(feature = "egui")] diff --git a/src/timing.rs b/src/timing.rs new file mode 100644 index 00000000..b9200f67 --- /dev/null +++ b/src/timing.rs @@ -0,0 +1,151 @@ +//! Information about when an action was pressed or released. + +use bevy::{ + reflect::Reflect, + utils::{Duration, Instant}, +}; +use serde::{Deserialize, Serialize}; + +/// Stores information about when an action was pressed or released +/// +/// This struct is principally used as a field on [`ActionData`](crate::action_state::ActionData), +/// which itself lives inside an [`ActionState`](crate::action_state::ActionState). +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, Reflect)] +pub struct Timing { + /// The [`Instant`] at which the button was pressed or released + /// Recorded as the [`Time`](bevy::time::Time) at the start of the tick after the state last changed. + /// If this is none, [`Timing::tick`] has not been called yet. + #[serde(skip)] + pub instant_started: Option, + /// The [`Duration`] for which the button has been pressed or released. + /// + /// This begins at [`Duration::ZERO`] when [`ActionState::update`](crate::action_state::ActionState::update) is called. + pub current_duration: Duration, + /// The [`Duration`] for which the button was pressed or released before the state last changed. + pub previous_duration: Duration, +} + +impl PartialOrd for Timing { + fn partial_cmp(&self, other: &Self) -> Option { + self.current_duration.partial_cmp(&other.current_duration) + } +} + +impl Timing { + /// Advances the `current_duration` of this timer + /// + /// If the `instant_started` is None, it will be set to the current time. + /// This design allows us to ensure that the timing is always synchronized with the start of each frame. + pub fn tick(&mut self, current_instant: Instant, previous_instant: Instant) { + if let Some(instant_started) = self.instant_started { + self.current_duration = current_instant - instant_started; + } else { + self.current_duration = current_instant - previous_instant; + self.instant_started = Some(previous_instant); + } + } + + /// Flips the metaphorical hourglass, storing `current_duration` in `previous_duration` and resetting `instant_started` + /// + /// This method is called whenever actions are pressed or released + pub fn flip(&mut self) { + self.previous_duration = self.current_duration; + self.current_duration = Duration::ZERO; + self.instant_started = None; + } +} + +#[cfg(test)] +mod tests { + use crate as leafwing_input_manager; + use bevy::prelude::Reflect; + use leafwing_input_manager_macros::Actionlike; + + #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug, Reflect)] + enum Action { + Run, + Jump, + Hide, + } + + #[test] + fn time_tick_ticks_away() { + use crate::action_state::ActionState; + use bevy::utils::{Duration, Instant}; + + let mut action_state = ActionState::::default(); + + // Actions start released (but not just released) + assert!(action_state.released(&Action::Run)); + assert!(!action_state.just_released(&Action::Jump)); + + // Ticking causes buttons that were just released to no longer be just released + action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); + assert!(action_state.released(&Action::Jump)); + assert!(!action_state.just_released(&Action::Jump)); + action_state.press(&Action::Jump); + assert!(action_state.just_pressed(&Action::Jump)); + + // Ticking causes buttons that were just pressed to no longer be just pressed + action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); + assert!(action_state.pressed(&Action::Jump)); + assert!(!action_state.just_pressed(&Action::Jump)); + } + + #[test] + fn durations() { + use crate::action_state::ActionState; + use bevy::utils::{Duration, Instant}; + + let mut action_state = ActionState::::default(); + + // Actions start released + assert!(action_state.released(&Action::Jump)); + assert_eq!(action_state.instant_started(&Action::Jump), None,); + assert_eq!(action_state.current_duration(&Action::Jump), Duration::ZERO); + assert_eq!( + action_state.previous_duration(&Action::Jump), + Duration::ZERO + ); + + // Pressing a button swaps the state + action_state.press(&Action::Jump); + assert!(action_state.pressed(&Action::Jump)); + assert_eq!(action_state.instant_started(&Action::Jump), None); + assert_eq!(action_state.current_duration(&Action::Jump), Duration::ZERO); + assert_eq!( + action_state.previous_duration(&Action::Jump), + Duration::ZERO + ); + + // Ticking time sets the instant for the new state + let t0 = Instant::now(); + let t1 = t0 + Duration::new(1, 0); + + action_state.tick(t1, t0); + assert_eq!(action_state.instant_started(&Action::Jump), Some(t0)); + assert_eq!(action_state.current_duration(&Action::Jump), t1 - t0); + assert_eq!( + action_state.previous_duration(&Action::Jump), + Duration::ZERO + ); + + // Time passes + let t2 = t1 + Duration::new(5, 0); + + // The duration is updated + action_state.tick(t2, t1); + assert_eq!(action_state.instant_started(&Action::Jump), Some(t0)); + assert_eq!(action_state.current_duration(&Action::Jump), t2 - t0); + assert_eq!( + action_state.previous_duration(&Action::Jump), + Duration::ZERO + ); + + // Releasing again, swapping the current duration to the previous one + action_state.release(&Action::Jump); + assert_eq!(action_state.instant_started(&Action::Jump), None); + assert_eq!(action_state.current_duration(&Action::Jump), Duration::ZERO); + assert_eq!(action_state.previous_duration(&Action::Jump), t2 - t0); + } +} diff --git a/tests/action_diffs.rs b/tests/action_diffs.rs index 3c323747..7e448b55 100644 --- a/tests/action_diffs.rs +++ b/tests/action_diffs.rs @@ -1,8 +1,6 @@ use bevy::{input::InputPlugin, prelude::*}; -use leafwing_input_manager::action_state::ActionDiffEvent; -use leafwing_input_manager::{ - action_state::ActionDiff, axislike::DualAxisData, prelude::*, systems::generate_action_diffs, -}; +use leafwing_input_manager::action_diff::{ActionDiff, ActionDiffEvent}; +use leafwing_input_manager::{axislike::DualAxisData, prelude::*, systems::generate_action_diffs}; #[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)] enum Action {