Skip to content

Commit

Permalink
Make the action_state module less huge (#453)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
alice-i-cecile and Alice Cecile authored Jan 24, 2024
1 parent 779ebc1 commit 7c0117c
Show file tree
Hide file tree
Showing 11 changed files with 495 additions and 442 deletions.
10 changes: 7 additions & 3 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -34,7 +34,11 @@
- `InputMap::which_pressed` now returns a `HashMap<A, ActionState>`
- `handle_clashes` now takes a `HashMap<A, ActionState>`
- `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
Expand All @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions benches/action_state.rs
Original file line number Diff line number Diff line change
@@ -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,
};

Expand Down
2 changes: 1 addition & 1 deletion examples/send_actions_over_network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
60 changes: 60 additions & 0 deletions src/action_diff.rs
Original file line number Diff line number Diff line change
@@ -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<ActionDiffEvent>` 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<A: Actionlike> {
/// 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<ActionDiffEvent>` resource.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Event)]
pub struct ActionDiffEvent<A: Actionlike> {
/// If some: the entity that has the `ActionState<A>` component
/// If none: `ActionState<A>` is a Resource, not a component
pub owner: Option<Entity>,
/// The `ActionDiff` that was generated
pub action_diffs: Vec<ActionDiff<A>>,
}
253 changes: 253 additions & 0 deletions src/action_driver.rs
Original file line number Diff line number Diff line change
@@ -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::<DanceDance>::default())
/// .id();
///
/// // Spawn a button, which is wired up to the dance tracker
/// // When used with InputManagerPlugin<DanceDance>, 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<A: Actionlike> {
/// 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<Entity>),
}

impl ActionStateDriverTarget {
/// Get an iterator for the entities targeted.
#[inline(always)]
pub fn iter(&self) -> impl Iterator<Item = &Entity> {
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<Item = Entity>) {
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<Entity> for ActionStateDriverTarget {
fn from(value: Entity) -> Self {
Self::Single(value)
}
}

impl From<()> for ActionStateDriverTarget {
fn from(_value: ()) -> Self {
Self::None
}
}

impl FromIterator<Entity> for ActionStateDriverTarget {
fn from_iter<T: IntoIterator<Item = Entity>>(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<T: IntoIterator<Item = &'a Entity>>(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<Self::Item> {
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);
}
}
Loading

0 comments on commit 7c0117c

Please sign in to comment.