From dec8a587903ab5db82e8846c1bad3441742f01d6 Mon Sep 17 00:00:00 2001 From: Seldom <38388947+Seldom-SE@users.noreply.github.com> Date: Fri, 12 Jan 2024 21:41:06 -0700 Subject: [PATCH] Accept systems as triggers --- README.md | 38 ++-- examples/chase.rs | 104 ++++----- examples/done.rs | 28 +-- examples/input.rs | 38 ++-- src/lib.rs | 16 +- src/machine.rs | 123 +++++----- src/state.rs | 16 +- src/trigger.rs | 395 ++++++++++++++++---------------- src/trigger/input.rs | 522 ++++++++++++++++--------------------------- 9 files changed, 556 insertions(+), 724 deletions(-) diff --git a/README.md b/README.md index 17f5d2f..a9f8d3e 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ and other entities that occupy various states. It allows for greater reusability between entities, compared to managing mutually-exclusive components directly in your systems. A *state* is a component attached to an entity that defines its current behavior, such as `Jumping` -or `Stunned`. A *trigger* is a type that checks information about entities in the world, such as -`NearPosition` or `HealthBelowThreshold`. A *transition* links two states: one to transition from, -and one to transition to; once a given trigger has occurred. A *state machine* is a component +or `Stunned`. A *trigger* is a system that checks information about entities in the world, such as +`near_position` or `health_below_threshold`. A *transition* links two states: one to transition +from, and one to transition to; once a given trigger has occurred. A *state machine* is a component attached to an entity that keeps track of that entity's transitions, and automatically changes the entity's state according to those transitions. @@ -41,18 +41,23 @@ improvement, feel free to submit an issue or pr! ## Features - State machine component with user-defined states and triggers -- 15 built-in triggers - - `AlwaysTrigger`: always triggers +- 30 built-in triggers + - `always`: always triggers - `NotTrigger`, `AndTrigger`, and `OrTrigger`: combines triggers with boolean logic - - `DoneTrigger`: triggers when the user adds the `Done` component to the entity - - 9 more triggers enabled by the `leafwing_input` feature: `ValueTrigger`, - `ClampedValueTrigger`, `AxisPairTrigger`, `ClampedAxisPairTrigger`, `JustPressedTrigger`, - `PressedTrigger`, `JustReleasedTrigger`, `ReleasedTrigger`, and `ActionDataTrigger` - - `EventTrigger`: triggers when it reads an event of the given type + - `done`: triggers when the `Done` component is added to the entity + - 24 more triggers enabled by the `leafwing_input` feature: `action_data`, `axis_pair`, + `axis_pair_length_bounds`, `axis_pair_max_length`, `axis_pair_min_length`, + `axis_pair_rotation_bounds`, `axis_pair_unbounded`, `clamped_axis_pair`, + `clamped_axis_pair_length_bounds`, `clamped_axis_pair_max_length`, + `clamped_axis_pair_min_length`, `clamped_axis_pair_rotation_bounds`, + `clamped_axis_pair_unbounded`, `clamped_value`, `clamped_value_max`, `clamped_value_min`, + `clamped_value_unbounded`, `just_pressed`, `just_released`, `pressed`, `value`, `value_max`, + `value_min`, and `value_unbounded` + - `on_event`: triggers when it reads an event of the given type - `AnyState` state, that can be used in type parameters to represent any state - Transition builders that allow dataflow from outgoing states and triggers to incoming states (`StateMachine::trans_builder`) -- Automatically run events upon entering or exiting states (`StateMachine::on_enter`, +- Automatically perform behavior upon entering or exiting states (`StateMachine::on_enter`, `StateMachine::on_exit`, `StateMachine::command_on_enter` and `StateMachine::command_on_exit`) ## Comparison with [`big-brain`](https://github.com/zkat/big-brain) @@ -60,11 +65,12 @@ improvement, feel free to submit an issue or pr! Finite state machine is an old and well-worn pattern in game AI, so its strengths and limitations are known. It is good for entities that: -1. Do not have a large number of interconnected states, since the number of transitions can grow +1. Do not have a huge number of interconnected states, since the number of transitions can grow quadratically. Then it becomes easy to forget to add a transition, causing difficult bugs. 2. Act rigidly, like the enemies in Spelunky, who act according to clear triggers such as -got-jumped-on-by-player and waited-for-5-seconds, and unlike the dwarves in Dwarf Fortress, who -weigh their options of what to do before taking an action. +got-jumped-on-by-player and waited-for-5-seconds and are predictable to the player, and unlike the +dwarves in Dwarf Fortress, who weigh their options of what to do before taking an action and feel +lively. `seldom_state` is a finite state machine implementation, so it may not be suitable for all types of game AI. If you need a solution that works with more complex states and transitions, then you may @@ -105,8 +111,8 @@ Consider a 2D platformer, where the player has a sword. The player can run and j can swing the sword. So whether you're running, jumping, or dashing, you always swing the sword the same way, independently of movement state. In this case, you might want to have a movement state machine and an attack state machine. Since entities can only have one state machine, spawn another -entity with its own state machine, and capture the original `Entity` in closures in -`command_on_enter` and `command_on_exit`. +entity (as a child, I would suggest) with its own state machine, and capture the original `Entity` +in closures in `command_on_enter` and `command_on_exit`. However, perhaps your states are not so independent. Maybe attacking while dashing puts the player in a `PowerAttack` state, or the attack cooldown doesn't count down while moving. Depending on the diff --git a/examples/chase.rs b/examples/chase.rs index 845d0a2..730ab4b 100644 --- a/examples/chase.rs +++ b/examples/chase.rs @@ -28,10 +28,23 @@ fn init(mut commands: Commands, asset_server: Res) { )) .id(); - // Since we use this trigger twice, let's declare it out here so we can reuse it - let near_player = Near { - target: player, - range: 300., + // This is our trigger, which is a Bevy system that returns a `bool`, `Option`, or `Result`. We + // define the trigger as a closure within this function so it can use variables in the scope + // (namely, `player`). For the sake of example, we also define this trigger as an external + // function later. + let near_player = move |In(entity): In, transforms: Query<&Transform>| { + let distance = transforms + .get(player) + .unwrap() + .translation + .truncate() + .distance(transforms.get(entity).unwrap().translation.truncate()); + + // Check whether the target is within range. If it is, return `Ok` to trigger! + match distance <= 300. { + true => Ok(distance), + false => Err(distance), + } }; // The enemy @@ -45,8 +58,8 @@ fn init(mut commands: Commands, asset_server: Res) { // priority, but triggers after the first accepted one may still be checked. StateMachine::default() // Add a transition. When they're in `Idle` state, and the `near_player` trigger occurs, - // switch to that instance of the `Follow` state - .trans::( + // switch to this instance of the `Follow` state + .trans::( near_player, // Transitions accept specific instances of states Follow { @@ -57,7 +70,7 @@ fn init(mut commands: Commands, asset_server: Res) { // Add a second transition. When they're in the `Follow` state, and the `near_player` // trigger does not occur, switch to the `Idle` state. `.not()` is a combinator that // negates the trigger. `.and(other)` and `.or(other)` also exist. - .trans::(near_player.not(), Idle) + .trans::(near_player.not(), Idle) // Enable transition logging .set_trans_logging(true), // The initial state is `Idle` @@ -65,62 +78,18 @@ fn init(mut commands: Commands, asset_server: Res) { )); } -// Let's define our trigger!. `Clone` and `Copy` are not necessary, but it's nicer to do so here. - -// This trigger checks if the entity is within the the given range of the target -#[derive(Clone, Copy)] -struct Near { - target: Entity, - range: f32, -} - -// Also see `OptionTrigger` and `BoolTrigger` -impl Trigger for Near { - // Put the parameters that your trigger needs here. `Param` is read-only; you may not access - // system params that write to the `World`. `Time` is included here to demonstrate how to get - // multiple system params. - type Param<'w, 's> = (Query<'w, 's, &'static Transform>, Res<'w, Time>); - // These types are used by transition builders, for dataflow from triggers to transitions. See - // `StateMachine::trans_builder` - type Ok = f32; - type Err = f32; - - // This function checks if the given entity should trigger. It runs once per potential - // transition for each entity that is in a state that can transition on this trigger. return - // `Ok` to trigger or `Err` to not trigger. - fn trigger( - &self, - entity: Entity, - (transforms, _time): Self::Param<'_, '_>, - ) -> Result { - // Find the distance between the target and this entity - let distance = transforms - .get(self.target) - .unwrap() - .translation - .truncate() - .distance(transforms.get(entity).unwrap().translation.truncate()); - - // Check whether the target is within range. If it is, return `Ok` to trigger! - match distance <= self.range { - true => Ok(distance), - false => Err(distance), - } - } -} - -// Now let's define our states! States must implement `Component` and `Clone`. `MachineState` is +// Now let's define our states! States must implement `Component` and `Clone`. `EntityState` is // implemented automatically for valid states. Feel free to mutate/insert/remove states manually, -// but don't put it in multiple or zero states, else it will panic. Manually inserted/removed -// states will not trigger `on_enter`/`on_exit` events registered to the `StateMachine`. +// but don't put your entity in multiple or zero states, else it will panic. Manually inserted/ +// removed states will not trigger `on_enter`/`on_exit` events registered to the `StateMachine`. // Entities in the `Idle` state do nothing -#[derive(Clone, Component, Reflect)] +#[derive(Clone, Component)] #[component(storage = "SparseSet")] struct Idle; // Entities in the `Follow` state move toward the given entity at the given speed -#[derive(Clone, Component, Reflect)] +#[derive(Clone, Component)] #[component(storage = "SparseSet")] struct Follow { target: Entity, @@ -147,6 +116,29 @@ fn follow( } } +// For the sake of example, this is a function that returns the `near_player` trigger from before. +// This may be useful so that triggers that accept case-by-case values may be used across the +// codebase. Triggers that don't need to accept any values from local code may be defined as normal +// Bevy systems (see the `done` example). Also consider implementing the `Trigger` trait directly. +#[allow(dead_code)] +fn near(target: Entity) -> impl Trigger> { + (move |In(entity): In, transforms: Query<&Transform>| { + let distance = transforms + .get(target) + .unwrap() + .translation + .truncate() + .distance(transforms.get(entity).unwrap().translation.truncate()); + + // Check whether the target is within range. If it is, return `Ok` to trigger! + match distance <= 300. { + true => Ok(distance), + false => Err(distance), + } + }) + .into_trigger() +} + // The code after this comment is not related to `seldom_state`. It's just player movement. #[derive(Component)] diff --git a/examples/done.rs b/examples/done.rs index 7345789..b7638aa 100644 --- a/examples/done.rs +++ b/examples/done.rs @@ -24,33 +24,25 @@ fn init(mut commands: Commands, asset_server: Res) { Player, StateMachine::default() // When the player clicks, go there - .trans_builder(Click, |_: &AnyState, pos| { + .trans_builder(click, |_: &AnyState, pos| { Some(GoToSelection { speed: 200., target: pos, }) }) - // `DoneTrigger` triggers when the `Done` component is added to the entity. When they're - // done going to the selection, idle. - .trans::(DoneTrigger::Success, Idle) + // `done` triggers when the `Done` component is added to the entity. When they're done + // going to the selection, idle. + .trans::(done(Some(Done::Success)), Idle) .set_trans_logging(true), Idle, )); } -#[derive(Clone, Reflect)] -struct Click; - -impl OptionTrigger for Click { - type Param<'w, 's> = (Res<'w, Input>, Res<'w, CursorPosition>); - type Some = Vec2; - - fn trigger(&self, _: Entity, (mouse, cursor_position): Self::Param<'_, '_>) -> Option { - mouse - .just_pressed(MouseButton::Left) - .then_some(()) - .and(**cursor_position) - } +fn click(mouse: Res>, cursor_position: Res) -> Option { + mouse + .just_pressed(MouseButton::Left) + .then_some(()) + .and(**cursor_position) } #[derive(Clone, Component, Reflect)] @@ -77,7 +69,7 @@ fn go_to_target( if movement.length() > delta.length() { transform.translation = target.extend(transform.translation.z); // The player has reached the target! Add the `Done` component to the player, causing - // `DoneTrigger` to trigger. It will be automatically removed later this frame. + // `done` to trigger. It will be automatically removed later this frame. commands.entity(entity).insert(Done::Success); info!("Done!") } else { diff --git a/examples/input.rs b/examples/input.rs index 116aff5..11572e0 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -46,45 +46,35 @@ fn init(mut commands: Commands, asset_server: Res) { // in Castlevania and Celeste, and the attacks in a fighting game. StateMachine::default() // Whenever the player presses jump, jump - .trans::( - JustPressedTrigger(Action::Jump), + .trans::( + just_pressed(Action::Jump), Falling { velocity: JUMP_VELOCITY, }, ) // When the player hits the ground, idle - .trans::(GroundedTrigger, Grounded::Idle) + .trans::(grounded, Grounded::Idle) // When the player is grounded, set their movement direction - .trans_builder( - ValueTrigger::unbounded(Action::Move), - |_: &Grounded, value| { - Some(match value { - value if value > 0.5 => Grounded::Right, - value if value < -0.5 => Grounded::Left, - _ => Grounded::Idle, - }) - }, - ), + .trans_builder(value_unbounded(Action::Move), |_: &Grounded, value| { + Some(match value { + value if value > 0.5 => Grounded::Right, + value if value < -0.5 => Grounded::Left, + _ => Grounded::Idle, + }) + }), Grounded::Idle, )); } -#[derive(Actionlike, Clone, Reflect)] +#[derive(Actionlike, Clone, Eq, Hash, PartialEq, Reflect)] enum Action { Move, Jump, } -#[derive(Reflect)] -struct GroundedTrigger; - -impl BoolTrigger for GroundedTrigger { - type Param<'w, 's> = Query<'w, 's, (&'static Transform, &'static Falling)>; - - fn trigger(&self, entity: Entity, fallings: Self::Param<'_, '_>) -> bool { - let (transform, falling) = fallings.get(entity).unwrap(); - transform.translation.y <= 0. && falling.velocity <= 0. - } +fn grounded(In(entity): In, fallings: Query<(&Transform, &Falling)>) -> bool { + let (transform, falling) = fallings.get(entity).unwrap(); + transform.translation.y <= 0. && falling.velocity <= 0. } #[derive(Clone, Copy, Component, Reflect)] diff --git a/src/lib.rs b/src/lib.rs index 577eb64..943ca6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,17 +37,19 @@ pub mod prelude { #[cfg(feature = "leafwing_input")] pub use crate::trigger::{ - ActionDataTrigger, AxisPairTrigger, ClampedAxisPairTrigger, ClampedValueTrigger, - JustPressedTrigger, JustReleasedTrigger, PressedTrigger, ReleasedTrigger, ValueTrigger, + action_data, axis_pair, axis_pair_length_bounds, axis_pair_max_length, + axis_pair_min_length, axis_pair_rotation_bounds, axis_pair_unbounded, clamped_axis_pair, + clamped_axis_pair_length_bounds, clamped_axis_pair_max_length, + clamped_axis_pair_min_length, clamped_axis_pair_rotation_bounds, + clamped_axis_pair_unbounded, clamped_value, clamped_value_max, clamped_value_min, + clamped_value_unbounded, just_pressed, just_released, pressed, value, value_max, value_min, + value_unbounded, }; pub use crate::{ machine::StateMachine, - state::{AnyState, MachineState}, + state::{AnyState, EntityState}, state_machine_plugin, - trigger::{ - AlwaysTrigger, BoolTrigger, Done, DoneTrigger, EventTrigger, Never, OptionTrigger, - Trigger, - }, + trigger::{always, done, on_event, Done, IntoTrigger, Never, Trigger}, StateMachinePlugin, }; } diff --git a/src/machine.rs b/src/machine.rs index cf0de9d..169620f 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -14,6 +14,7 @@ use crate::{ prelude::*, set::StateSet, state::{Insert, OnEvent}, + trigger::{IntoTrigger, TriggerOut}, }; pub(crate) fn machine_plugin(app: &mut App) { @@ -22,43 +23,42 @@ pub(crate) fn machine_plugin(app: &mut App) { /// Performs a transition. We have a trait for this so we can erase [`TransitionImpl`]'s generics. trait Transition: Debug + Send + Sync + 'static { - /// Called before any call to `run` + /// Called before any call to `check` fn init(&mut self, world: &mut World); /// Checks whether the transition should be taken. `entity` is the entity that contains the /// state machine. - fn run(&mut self, world: &World, entity: Entity) -> Option<(Box, TypeId)>; + fn check(&mut self, world: &World, entity: Entity) -> Option<(Box, TypeId)>; } /// An edge in the state machine. The type parameters are the [`Trigger`] that causes this -/// transition, the previous state the function that takes the trigger's output and builds the next +/// transition, the previous state, the function that takes the trigger's output and builds the next /// state, and the next state itself. struct TransitionImpl where Trig: Trigger, - Prev: MachineState, - Build: 'static + Fn(&Prev, Trig::Ok) -> Option + Send + Sync, - Next: Component + MachineState, + Prev: EntityState, + Build: 'static + + Fn(&Prev, <::Out as TriggerOut>::Ok) -> Option + + Send + + Sync, + Next: Component + EntityState, { pub trigger: Trig, pub builder: Build, - // To run this, we need a [`SystemState`]. We can't initialize that until we have a [`World`], - // so it starts out empty - system_state: Option>>, phantom: PhantomData, } impl Debug for TransitionImpl where Trig: Trigger, - Prev: MachineState, - Build: Fn(&Prev, Trig::Ok) -> Option + Send + Sync, - Next: Component + MachineState, + Prev: EntityState, + Build: Fn(&Prev, <::Out as TriggerOut>::Ok) -> Option + Send + Sync, + Next: Component + EntityState, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TransitionImpl") .field("trigger", &self.trigger.type_id()) .field("builder", &self.builder.type_id()) - .field("system_state", &self.system_state.type_id()) .field("phantom", &self.phantom) .finish() } @@ -67,21 +67,19 @@ where impl Transition for TransitionImpl where Trig: Trigger, - Prev: MachineState, - Build: Fn(&Prev, Trig::Ok) -> Option + Send + Sync, - Next: Component + MachineState, + Prev: EntityState, + Build: Fn(&Prev, <::Out as TriggerOut>::Ok) -> Option + Send + Sync, + Next: Component + EntityState, { fn init(&mut self, world: &mut World) { - if self.system_state.is_none() { - self.system_state = Some(SystemState::new(world)); - } + self.trigger.init(world); } - fn run(&mut self, world: &World, entity: Entity) -> Option<(Box, TypeId)> { - let state = self.system_state.as_mut().unwrap(); - let Ok(res) = self.trigger.trigger(entity, state.get(world)) else { + fn check(&mut self, world: &World, entity: Entity) -> Option<(Box, TypeId)> { + let Ok(res) = self.trigger.check(entity, world).into_result() else { return None; }; + (self.builder)(Prev::from_entity(entity, world), res) .map(|state| (Box::new(state) as Box, TypeId::of::())) } @@ -90,15 +88,14 @@ where impl TransitionImpl where Trig: Trigger, - Prev: MachineState, - Build: Fn(&Prev, Trig::Ok) -> Option + Send + Sync, - Next: Component + MachineState, + Prev: EntityState, + Build: Fn(&Prev, <::Out as TriggerOut>::Ok) -> Option + Send + Sync, + Next: Component + EntityState, { pub fn new(trigger: Trig, builder: Build) -> Self { Self { trigger, builder, - system_state: None, phantom: PhantomData, } } @@ -114,7 +111,7 @@ struct StateMetadata { } impl StateMetadata { - fn new() -> Self { + fn new() -> Self { Self { name: type_name::().to_owned(), on_enter: default(), @@ -126,7 +123,7 @@ impl StateMetadata { } /// State machine component. Entities with this component will have components (the states) added -/// and removed based on the transitions that you add. Build one with `StateMachine::new`, +/// and removed based on the transitions that you add. Build one with `StateMachine::default`, /// `StateMachine::trans`, and other methods. #[derive(Component)] pub struct StateMachine { @@ -136,7 +133,9 @@ pub struct StateMachine { /// each StateMetadata would mean that e.g. we'd have to check every AnyState trigger before any /// state-specific trigger or vice versa. transitions: Vec<(TypeId, Box)>, - /// If true, all transitions are logged at info level. + /// Transitions must be initialized whenever a transition is added or a transition occurs + init_transitions: bool, + /// If true, all transitions are logged at info level log_transitions: bool, } @@ -152,6 +151,7 @@ impl Default for StateMachine { }, )]), transitions: vec![], + init_transitions: true, log_transitions: false, } } @@ -166,17 +166,18 @@ impl StateMachine { /// Adds a transition to the state machine. When the entity is in the state given as a /// type parameter, and the given trigger occurs, it will transition to the state given as a - /// function parameter. Transitions have priority in the order they are added. - pub fn trans( + /// function parameter. Elide the `Marker` type parameter with `_`. Transitions have priority + /// in the order they are added. + pub fn trans( self, - trigger: impl Trigger, + trigger: impl IntoTrigger, state: impl Clone + Component, ) -> Self { self.trans_builder(trigger, move |_: &S, _| Some(state.clone())) } - /// Get the medatada for the given state, creating it if necessary. - fn metadata_mut(&mut self) -> &mut StateMetadata { + /// Get the metadata for the given state, creating it if necessary. + fn metadata_mut(&mut self) -> &mut StateMetadata { self.states .entry(TypeId::of::()) .or_insert(StateMetadata::new::()) @@ -185,24 +186,34 @@ impl StateMachine { /// Adds a transition builder to the state machine. When the entity is in `Prev` state, and /// `Trig` occurs, the given builder will be run on `Trig::Ok`. If the builder returns /// `Some(Next)`, the machine will transition to that `Next` state. - pub fn trans_builder( + pub fn trans_builder< + Prev: EntityState, + Trig: IntoTrigger, + Next: Clone + Component, + Marker, + >( mut self, trigger: Trig, - builder: impl 'static + Clone + Fn(&Prev, Trig::Ok) -> Option + Send + Sync, + builder: impl 'static + + Clone + + Fn(&Prev, <::Out as TriggerOut>::Ok) -> Option + + Send + + Sync, ) -> Self { self.metadata_mut::(); self.metadata_mut::(); - let transition = TransitionImpl::<_, Prev, _, _>::new(trigger, builder); + let transition = TransitionImpl::<_, Prev, _, _>::new(trigger.into_trigger(), builder); self.transitions.push(( TypeId::of::(), Box::new(transition) as Box, )); + self.init_transitions = true; self } /// Adds an on-enter event to the state machine. Whenever the state machine transitions into the /// given state, it will run the event. - pub fn on_enter( + pub fn on_enter( mut self, on_enter: impl 'static + Fn(&mut EntityCommands) + Send + Sync, ) -> Self { @@ -215,7 +226,7 @@ impl StateMachine { /// Adds an on-exit event to the state machine. Whenever the state machine transitions from the /// given state, it will run the event. - pub fn on_exit( + pub fn on_exit( mut self, on_exit: impl 'static + Fn(&mut EntityCommands) + Send + Sync, ) -> Self { @@ -228,7 +239,7 @@ impl StateMachine { /// Adds an on-enter command to the state machine. Whenever the state machine transitions into /// the given state, it will run the command. - pub fn command_on_enter( + pub fn command_on_enter( mut self, command: impl Clone + Command + Sync, ) -> Self { @@ -241,10 +252,7 @@ impl StateMachine { /// Adds an on-exit command to the state machine. Whenever the state machine transitions from /// the given state, it will run the command. - pub fn command_on_exit( - mut self, - command: impl Clone + Command + Sync, - ) -> Self { + pub fn command_on_exit(mut self, command: impl Clone + Command + Sync) -> Self { self.metadata_mut::() .on_exit .push(OnEvent::Command(Box::new(command))); @@ -261,6 +269,10 @@ impl StateMachine { /// Initialize all transitions. Must be executed before `run`. This is separate because `run` is /// parallelizable (takes a `&World`) but this isn't (takes a `&mut World`). fn init_transitions(&mut self, world: &mut World) { + if !self.init_transitions { + return; + } + for (_, transition) in &mut self.transitions { transition.init(world); } @@ -287,7 +299,7 @@ impl StateMachine { .transitions .iter_mut() .filter(|(type_id, _)| *type_id == current || *type_id == TypeId::of::()) - .find_map(|(_, transition)| transition.run(world, entity)) + .find_map(|(_, transition)| transition.check(world, entity)) else { return; }; @@ -305,6 +317,8 @@ impl StateMachine { if self.log_transitions { info!("{entity:?} transitioned from {} to {}", from.name, to.name); } + + self.init_transitions = true; } /// When running the transition system, we replace all StateMachines in the world with their @@ -312,8 +326,9 @@ impl StateMachine { fn stub(&self) -> Self { Self { states: default(), - log_transitions: false, transitions: default(), + init_transitions: false, + log_transitions: false, } } } @@ -377,14 +392,8 @@ mod tests { struct SomeResource; /// Triggers when `SomeResource` is present - struct ResourcePresent; - - impl BoolTrigger for ResourcePresent { - type Param<'w, 's> = Option>; - - fn trigger(&self, _entity: Entity, param: Self::Param<'_, '_>) -> bool { - param.is_some() - } + fn resource_present(res: Option>) -> bool { + res.is_some() } #[test] @@ -407,8 +416,8 @@ mod tests { app.add_systems(Update, transition); let machine = StateMachine::default() - .trans::(AlwaysTrigger, StateTwo) - .trans::(ResourcePresent, StateThree); + .trans::(always, StateTwo) + .trans::(resource_present, StateThree); let entity = app.world.spawn((machine, StateOne)).id(); assert!(app.world.get::(entity).is_some()); @@ -438,7 +447,7 @@ mod tests { let entity = app .world .spawn(( - StateMachine::default().trans::(AlwaysTrigger, StateOne), + StateMachine::default().trans::(always, StateOne), StateOne, )) .id(); diff --git a/src/state.rs b/src/state.rs index 1397395..321011c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -7,19 +7,19 @@ use bevy::ecs::system::{Command, EntityCommands}; use crate::prelude::*; -use self::sealed::MachineStateSealed; +use self::sealed::EntityStateSealed; mod sealed { use bevy::ecs::system::EntityCommands; use crate::prelude::*; - pub trait MachineStateSealed { + pub trait EntityStateSealed { fn from_entity(entity: Entity, world: &World) -> &Self; fn remove(entity: &mut EntityCommands); } - impl MachineStateSealed for T { + impl EntityStateSealed for T { fn from_entity(entity: Entity, world: &World) -> &Self { world.entity(entity).get().unwrap() } @@ -29,7 +29,7 @@ mod sealed { } } - impl MachineStateSealed for AnyState { + impl EntityStateSealed for AnyState { fn from_entity(_: Entity, _: &World) -> &Self { &AnyState(()) } @@ -42,16 +42,16 @@ mod sealed { /// /// If you are concerned with performance, consider having your states use sparse set storage if /// transitions are very frequent. -pub trait MachineState: 'static + Clone + Send + Sync + MachineStateSealed {} +pub trait EntityState: 'static + Clone + Send + Sync + EntityStateSealed {} -impl MachineState for T {} +impl EntityState for T {} /// State that represents any state. Transitions from [`AnyState`] may transition from any other /// state. #[derive(Clone, Debug)] pub struct AnyState(pub(crate) ()); -impl MachineState for AnyState {} +impl EntityState for AnyState {} pub(crate) trait Insert: Send { fn insert(self: Box, entity: &mut EntityCommands) -> TypeId; @@ -133,7 +133,7 @@ mod tests { app.add_systems(Update, transition); let machine = StateMachine::default() - .trans::(AlwaysTrigger, StateTwo) + .trans::(always, StateTwo) .on_exit::(|commands| commands.commands().insert_resource(SomeResource)) .on_enter::(|commands| commands.commands().insert_resource(AnotherResource)); diff --git a/src/trigger.rs b/src/trigger.rs index abd3268..4a726a4 100644 --- a/src/trigger.rs +++ b/src/trigger.rs @@ -7,13 +7,15 @@ mod input; use either::Either; #[cfg(feature = "leafwing_input")] pub use input::{ - ActionDataTrigger, AxisPairTrigger, ClampedAxisPairTrigger, ClampedValueTrigger, - JustPressedTrigger, JustReleasedTrigger, PressedTrigger, ReleasedTrigger, ValueTrigger, + action_data, axis_pair, axis_pair_length_bounds, axis_pair_max_length, axis_pair_min_length, + axis_pair_rotation_bounds, axis_pair_unbounded, clamped_axis_pair, + clamped_axis_pair_length_bounds, clamped_axis_pair_max_length, clamped_axis_pair_min_length, + clamped_axis_pair_rotation_bounds, clamped_axis_pair_unbounded, clamped_value, + clamped_value_max, clamped_value_min, clamped_value_unbounded, just_pressed, just_released, + pressed, value, value_max, value_min, value_unbounded, }; -use std::{any::type_name, convert::Infallible, fmt::Debug, marker::PhantomData}; - -use bevy::ecs::system::{ReadOnlySystemParam, SystemParam}; +use std::{convert::Infallible, fmt::Debug}; use crate::{prelude::*, set::StateSet}; @@ -35,224 +37,238 @@ pub struct Never { never: Infallible, } -/// Types that implement this may be used in [`StateMachine`]s to transition from one state to -/// another. Look at an example for implementing this trait, since it can be tricky. -pub trait Trigger: 'static + Send + Sized + Sync { - /// System parameter provided to [`Trigger::trigger`] - type Param<'w, 's>: ReadOnlySystemParam; - /// When the trigger occurs, this data is returned from `trigger`, and passed to every - /// transition builder on this trigger. If there's no relevant information to pass, just use - /// `()`. If there's also no relevant information to pass to [`Trigger::Err`], implement - /// [`BoolTrigger`] instead. +/// Input requested by a trigger +pub trait TriggerIn { + /// Convert an `Entity` to `Self` + fn from_entity(entity: Entity) -> Self; +} + +impl TriggerIn for () { + fn from_entity(_: Entity) -> Self {} +} + +impl TriggerIn for Entity { + fn from_entity(entity: Entity) -> Self { + entity + } +} + +/// Output returned from a trigger. Indicates whether the transition will occur, and may include +/// data given to `StateMachine::trans_builder`. +pub trait TriggerOut { + /// Data given to `StateMachine::trans_builder` on a success type Ok; - /// When the trigger does not occur, this data is returned from `trigger`. In this case, - /// [`NotTrigger`] passes it to every transition builder on this trigger. If there's no - /// relevant information to pass, implement [`OptionTrigger`] instead. If this trigger is - /// infallible, use [`Never`]. + /// Data given to `StataMachine::trans_builder` if this trigger fails and is negated type Err; - /// Called for every entity that may transition to a state on this trigger. Return `Ok` if it - /// should transition, and `Err` if it should not. In most cases, you may use - /// `&Self::Param<'_, '_>` as `param`'s type. - fn trigger( - &self, - entity: Entity, - param: <::Param<'_, '_> as SystemParam>::Item<'_, '_>, - ) -> Result; - - /// Gets the name of the type, for use in logging - fn base_type_name(&self) -> &str { - type_name::() - } + /// Convert `Self` to a `Result` + fn into_result(self) -> Result; +} + +impl TriggerOut for bool { + type Ok = (); + type Err = (); - /// Negates the trigger - fn not(self) -> NotTrigger { - NotTrigger(self) + fn into_result(self) -> Result<(), ()> { + if self { + Ok(()) + } else { + Err(()) + } } +} + +impl TriggerOut for Option { + type Ok = T; + type Err = (); - /// Combines these triggers by logical AND - fn and(self, other: T) -> AndTrigger { - AndTrigger(self, other) + fn into_result(self) -> Result { + self.ok_or(()) } +} + +impl TriggerOut for Result { + type Ok = Ok; + type Err = Err; - /// Combines these triggers by logical OR - fn or(self, other: T) -> OrTrigger { - OrTrigger(self, other) + fn into_result(self) -> Self { + self } } -/// Automatically implements [`Trigger`]. Implement this instead if there is no relevant information -/// to pass for [`Trigger::Err`]. -pub trait OptionTrigger: 'static + Send + Sync { - /// System parameter provided to [`OptionTrigger::trigger`] - type Param<'w, 's>: ReadOnlySystemParam; - /// When the trigger occurs, this data is returned from `trigger`, and passed to every - /// transition builder on this trigger. If there's no relevant information to pass, implement - /// [`BoolTrigger`] instead. - type Some; - - /// Called for every entity that may transition to a state on this trigger. Return `Some` if it - /// should transition, and `None` if it should not. In most cases, you may use - /// `&Self::Param<'_, '_>` as `param`'s type. - fn trigger( - &self, - entity: Entity, - param: <::Param<'_, '_> as SystemParam>::Item<'_, '_>, - ) -> Option; +/// Automatically implemented for types that implement [`Trigger`] and certain types that implement +/// [`IntoSystem`]. Types that implement [`IntoSystem`] don't automatically implement [`Trigger`], +/// so if you want to accept a trigger somewhere, you can accept a generic that implements this +/// trait instead. Otherwise, the caller will usually have to call `.into_trigger()` when providing +/// a type that implements [`IntoSystem`]. +/// +/// The `Marker` type param is necessary to implement this trait for systems, to prevent a system +/// from implementing the same instance of this trait multiple times, since a type may implement +/// multiple instances of [`IntoSystem`]. It doesn't matter what type `Marker` is set to. +pub trait IntoTrigger: Sized { + /// The [`Trigger`] type that this is converted into + type Trigger: Trigger; + + /// Convert into a [`Trigger`] + fn into_trigger(self) -> Self::Trigger; + + /// Negates the trigger. Do not override. + fn not(self) -> impl Trigger { + NotTrigger(self.into_trigger()) + } + + /// Combines these triggers by logical AND. Do not override. + fn and(self, other: impl IntoTrigger) -> impl Trigger { + AndTrigger(self.into_trigger(), other.into_trigger()) + } + + /// Combines these triggers by logical OR. Do not override. + fn or(self, other: impl IntoTrigger) -> impl Trigger { + OrTrigger(self.into_trigger(), other.into_trigger()) + } } -impl Trigger for T { - type Param<'w, 's> = ::Param<'w, 's>; - type Ok = ::Some; - type Err = (); +impl> IntoTrigger<(In, Out, Marker)> for T +where + In: TriggerIn, + Out: TriggerOut, + T::System: ReadOnlySystem, +{ + type Trigger = SystemTrigger; - fn trigger( - &self, - entity: Entity, - param: <::Param<'_, '_> as SystemParam>::Item<'_, '_>, - ) -> Result { - OptionTrigger::trigger(self, entity, param).ok_or(()) + fn into_trigger(self) -> Self::Trigger { + SystemTrigger(IntoSystem::into_system(self)) } } -/// Automatically implements [`Trigger`]. Implement this instead if there is no relevant information -/// to pass for [`Trigger::Ok`] and [`Trigger::Err`]. -pub trait BoolTrigger: 'static + Send + Sync { - /// System parameter provided to [`BoolTrigger::trigger`] - type Param<'w, 's>: ReadOnlySystemParam; - - /// Called for every entity that may transition to a state on this trigger. Return `true` if it - /// should transition, and `false` if it should not. In most cases, you may use - /// `&Self::Param<'_, '_>` as `param`'s type. - fn trigger( - &self, - entity: Entity, - param: <::Param<'_, '_> as SystemParam>::Item<'_, '_>, - ) -> bool; +/// Types that implement this may be used in [`StateMachine`]s to transition from one state to +/// another. Look at an example for implementing this trait, since it can be tricky. +pub trait Trigger: 'static + Send + Sized + Sync { + /// The trigger's output. See [`TriggerOut`]. + type Out: TriggerOut; + + /// Initializes/resets this trigger. Runs every time the state machine transitions. + fn init(&mut self, world: &mut World); + /// Checks whether the state machine should transition + fn check(&mut self, entity: Entity, world: &World) -> Self::Out; } -impl OptionTrigger for T { - type Param<'w, 's> = ::Param<'w, 's>; - type Some = (); +impl IntoTrigger<()> for T { + type Trigger = T; - fn trigger( - &self, - entity: Entity, - param: <::Param<'_, '_> as SystemParam>::Item<'_, '_>, - ) -> Option<()> { - BoolTrigger::trigger(self, entity, param).then_some(()) + fn into_trigger(self) -> T { + self } } -/// Trigger that always transitions -#[derive(Debug, Clone, Copy)] -pub struct AlwaysTrigger; +/// The trigger form of a system. See [`IntoSystem`]. +pub struct SystemTrigger(T); -impl Trigger for AlwaysTrigger { - type Param<'w, 's> = (); - type Ok = (); - type Err = Never; +impl Trigger for SystemTrigger +where + T::In: TriggerIn, + T::Out: TriggerOut, +{ + type Out = T::Out; - fn trigger(&self, _: Entity, _: ()) -> Result<(), Never> { - Ok(()) + fn init(&mut self, world: &mut World) { + let Self(t) = self; + t.initialize(world); } + + fn check(&mut self, entity: Entity, world: &World) -> Self::Out { + let Self(t) = self; + t.run_readonly(T::In::from_entity(entity), world) + } +} + +/// Trigger that always transitions +pub fn always() -> bool { + true } -/// Trigger that negates the contained trigger -#[derive(Debug, Deref, DerefMut)] +/// Negates the given trigger +#[derive(Debug)] pub struct NotTrigger(pub T); impl Trigger for NotTrigger { - type Param<'w, 's> = T::Param<'w, 's>; - type Ok = T::Err; - type Err = T::Ok; - - fn trigger( - &self, - entity: Entity, - param: <::Param<'_, '_> as SystemParam>::Item<'_, '_>, - ) -> Result { - let Self(trigger) = self; - match trigger.trigger(entity, param) { + type Out = Result<::Err, ::Ok>; + + fn init(&mut self, world: &mut World) { + let Self(t) = self; + t.init(world); + } + + fn check(&mut self, entity: Entity, world: &World) -> Self::Out { + let Self(t) = self; + match t.check(entity, world).into_result() { Ok(ok) => Err(ok), Err(err) => Ok(err), } } } -impl Clone for NotTrigger { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -impl Copy for NotTrigger {} - -/// Trigger that combines two triggers by logical AND +/// Combines two triggers by logical AND #[derive(Debug)] pub struct AndTrigger(pub T, pub U); impl Trigger for AndTrigger { - type Param<'w, 's> = (T::Param<'w, 's>, U::Param<'w, 's>); - type Ok = (T::Ok, U::Ok); - type Err = Either; - - fn trigger( - &self, - entity: Entity, - (param1, param2): <::Param<'_, '_> as SystemParam>::Item<'_, '_>, - ) -> Result<(T::Ok, U::Ok), Either> { - let Self(trigger1, trigger2) = self; - Ok(( - trigger1.trigger(entity, param1).map_err(Either::Left)?, - trigger2.trigger(entity, param2).map_err(Either::Right)?, - )) + type Out = Result< + (::Ok, ::Ok), + Either<::Err, ::Err>, + >; + + fn init(&mut self, world: &mut World) { + let Self(t, u) = self; + + t.init(world); + u.init(world); } -} -impl Clone for AndTrigger { - fn clone(&self) -> Self { - Self(self.0.clone(), self.1.clone()) + fn check(&mut self, entity: Entity, world: &World) -> Self::Out { + let Self(t, u) = self; + + Ok(( + t.check(entity, world).into_result().map_err(Either::Left)?, + u.check(entity, world) + .into_result() + .map_err(Either::Right)?, + )) } } -impl Copy for AndTrigger {} - -/// Trigger that combines two triggers by logical OR +/// Combines two triggers by logical OR #[derive(Debug)] pub struct OrTrigger(pub T, pub U); impl Trigger for OrTrigger { - type Param<'w, 's> = (T::Param<'w, 's>, U::Param<'w, 's>); - type Ok = Either; - type Err = (T::Err, U::Err); - - fn trigger( - &self, - entity: Entity, - (param1, param2): <::Param<'_, '_> as SystemParam>::Item<'_, '_>, - ) -> Result, (T::Err, U::Err)> { - let Self(trigger1, trigger2) = self; - match trigger1.trigger(entity, param1) { + type Out = Result< + Either<::Ok, ::Ok>, + (::Err, ::Err), + >; + + fn init(&mut self, world: &mut World) { + let Self(t, u) = self; + + t.init(world); + u.init(world); + } + + fn check(&mut self, entity: Entity, world: &World) -> Self::Out { + let Self(t, u) = self; + + match t.check(entity, world).into_result() { Ok(ok) => Ok(Either::Left(ok)), - Err(err1) => match trigger2.trigger(entity, param2) { + Err(err_1) => match u.check(entity, world).into_result() { Ok(ok) => Ok(Either::Right(ok)), - Err(err2) => Err((err1, err2)), + Err(err_2) => Err((err_1, err_2)), }, } } } -impl Clone for OrTrigger { - fn clone(&self) -> Self { - Self(self.0.clone(), self.1.clone()) - } -} - -impl Copy for OrTrigger {} - /// Marker component that represents that the current state has completed. Removed from every entity -/// each frame after checking triggers. To be used with [`DoneTrigger`]. +/// each frame after checking triggers. To be used with [`done`]. #[derive(Component, Debug, Eq, PartialEq, Clone, Copy)] #[component(storage = "SparseSet")] pub enum Done { @@ -262,50 +278,21 @@ pub enum Done { Failure, } -/// Trigger that transitions if the entity has the [`Done`] component with the associated variant -#[derive(Debug, Clone, Copy)] -pub enum DoneTrigger { - /// Success variant - Success, - /// Failure variant - Failure, -} - -impl BoolTrigger for DoneTrigger { - type Param<'w, 's> = Query<'w, 's, &'static Done>; - - fn trigger(&self, entity: Entity, param: Self::Param<'_, '_>) -> bool { - param +/// Trigger that transitions if the entity has the [`Done`] component. Provide `Some(Done::Variant)` +/// to transition upon that particular variant, or `None` to transition upon either. +pub fn done(expected: Option) -> impl Trigger { + (move |In(entity): In, dones: Query<&Done>| { + dones .get(entity) - .map(|done| self.as_done() == *done) + .map(|&done| expected.is_none() || Some(done) == expected) .unwrap_or(false) - } -} - -impl DoneTrigger { - fn as_done(&self) -> Done { - match self { - Self::Success => Done::Success, - Self::Failure => Done::Failure, - } - } + }) + .into_trigger() } /// Trigger that transitions when it receives the associated event -#[derive(Debug, Default, Clone, Copy)] -pub struct EventTrigger(PhantomData); - -impl OptionTrigger for EventTrigger { - type Param<'w, 's> = EventReader<'w, 's, T>; - type Some = T; - - fn trigger( - &self, - _: Entity, - mut events: Self::Param<'_, '_>, - ) -> Option<::Some> { - events.read().next().cloned() - } +pub fn on_event(mut reader: EventReader) -> Option { + reader.read().last().cloned() } pub(crate) fn remove_done_markers(mut commands: Commands, dones: Query>) { diff --git a/src/trigger/input.rs b/src/trigger/input.rs index 9aef381..0156de6 100644 --- a/src/trigger/input.rs +++ b/src/trigger/input.rs @@ -1,4 +1,4 @@ -use std::any::type_name; +use std::{any::type_name, ops::Range}; use leafwing_input_manager::{ action_state::ActionData, axislike::DualAxisData, orientation::Rotation, @@ -6,23 +6,10 @@ use leafwing_input_manager::{ use crate::prelude::*; -/// Trigger that transitions if the given [`Actionlike`]'s value is within the given bounds -#[derive(Debug, Clone)] -pub struct ValueTrigger { - /// The action - pub action: A, - /// The minimum value. If no minimum is necessary, use [`f32::NEG_INFINITY`], or similar - pub min: f32, - /// The maximum value. If no maximum is necessary, use [`f32::INFINITY`], or similar - pub max: f32, -} - -impl Trigger for ValueTrigger { - type Param<'w, 's> = Query<'w, 's, &'static ActionState>; - type Ok = f32; - type Err = f32; - - fn trigger(&self, entity: Entity, actors: Self::Param<'_, '_>) -> Result { +/// Trigger that transitions if the given [`Actionlike`]'s value is within the given bounds. +/// Consider using `f32::NEG_INFINITY`/`f32::INFINITY` in the bounds. +pub fn value(action: A, bounds: Range) -> impl Trigger> { + (move |In(entity): In, actors: Query<&ActionState>| { let value = actors .get(entity) .unwrap_or_else(|_| { @@ -31,60 +18,38 @@ impl Trigger for ValueTrigger { type_name::() ) }) - .value(self.action.clone()); + .value(action.clone()); - (value >= self.min && value <= self.max) - .then_some(value) - .ok_or(value) - } + if bounds.contains(&value) { + Ok(value) + } else { + Err(value) + } + }) + .into_trigger() } -impl ValueTrigger { - /// Unbounded trigger - pub fn unbounded(action: A) -> Self { - Self { - action, - min: f32::NEG_INFINITY, - max: f32::INFINITY, - } - } - - /// Trigger with a minimum bound - pub fn min(action: A, min: f32) -> Self { - Self { - action, - min, - max: f32::INFINITY, - } - } - - /// Trigger with a maximum bound - pub fn max(action: A, max: f32) -> Self { - Self { - action, - min: f32::NEG_INFINITY, - max, - } - } +/// Unbounded [`value`] +pub fn value_unbounded(action: impl Actionlike) -> impl Trigger> { + value(action, f32::NEG_INFINITY..f32::INFINITY) } -/// Trigger that transitions if the given [`Actionlike`]'s value is within the given bounds -#[derive(Debug, Clone)] -pub struct ClampedValueTrigger { - /// The action - pub action: A, - /// The minimum value. If no minimum is necessary, use `f32::NEG_INFINITY`, or similar. - pub min: f32, - /// The maximum value. If no maximum is necessary, use `f32::INFINITY`, or similar. - pub max: f32, +/// [`value`] with only a minimum bound +pub fn value_min(action: impl Actionlike, min: f32) -> impl Trigger> { + value(action, min..f32::INFINITY) } -impl Trigger for ClampedValueTrigger { - type Param<'w, 's> = Query<'w, 's, &'static ActionState>; - type Ok = f32; - type Err = f32; +/// [`value`] with only a maximum bound +pub fn value_max(action: impl Actionlike, max: f32) -> impl Trigger> { + value(action, f32::NEG_INFINITY..max) +} - fn trigger(&self, entity: Entity, actors: Self::Param<'_, '_>) -> Result { +/// [`value`] clamped to [-1, 1] +pub fn clamped_value( + action: A, + bounds: Range, +) -> impl Trigger> { + (move |In(entity): In, actors: Query<&ActionState>| { let value = actors .get(entity) .unwrap_or_else(|_| { @@ -93,72 +58,49 @@ impl Trigger for ClampedValueTrigger { type_name::() ) }) - .clamped_value(self.action.clone()); + .clamped_value(action.clone()); - (value >= self.min && value <= self.max) - .then_some(value) - .ok_or(value) - } + if bounds.contains(&value) { + Ok(value) + } else { + Err(value) + } + }) + .into_trigger() } -impl ClampedValueTrigger { - /// Unbounded trigger - pub fn unbounded(action: A) -> Self { - Self { - action, - min: f32::NEG_INFINITY, - max: f32::INFINITY, - } - } - - /// Trigger with a minimum bound - pub fn min(action: A, min: f32) -> Self { - Self { - action, - min, - max: f32::INFINITY, - } - } - - /// Trigger with a maximum bound - pub fn max(action: A, max: f32) -> Self { - Self { - action, - min: f32::NEG_INFINITY, - max, - } - } +/// Unbounded [`clamped_value`] +pub fn clamped_value_unbounded(action: impl Actionlike) -> impl Trigger> { + clamped_value(action, f32::NEG_INFINITY..f32::INFINITY) } -/// Trigger that transitions if the given [`Actionlike`]'s [`DualAxisData`] is within the given -/// bounds -#[derive(Debug, Clone)] -pub struct AxisPairTrigger { - /// The action - pub action: A, - /// Minimum axis pair length. If no minimum is necessary, use `0.`. To exclude specifically - /// neutral axis pairs, use `f32::EPSILON`, or similar. - pub min_length: f32, - /// Maximum axis pair length. If no maximum is necessary, use `f32::INFINITY`, or similar. - pub max_length: f32, - /// Minimum rotation, measured clockwise from midnight. If rotation bounds are not necessary, - /// set this and `max_rotation` to the same value. - pub min_rotation: Rotation, - /// Maximum rotation, measured clockwise from midnight. If rotation bounds are not necessary, - /// set this and `min_rotation` to the same value. - pub max_rotation: Rotation, +/// [`clamped_value`] with only a minimum bound +pub fn clamped_value_min( + action: impl Actionlike, + min: f32, +) -> impl Trigger> { + clamped_value(action, min..f32::INFINITY) } -impl Trigger for AxisPairTrigger { - type Param<'w, 's> = Query<'w, 's, &'static ActionState>; - type Ok = DualAxisData; - type Err = Option; +/// [`clamped_value`] with only a maximum bound +pub fn clamped_value_max( + action: impl Actionlike, + max: f32, +) -> impl Trigger> { + clamped_value(action, f32::NEG_INFINITY..max) +} - fn trigger( - &self, - entity: Entity, - actors: Self::Param<'_, '_>, - ) -> Result> { +/// Trigger that transitions if the given [`Actionlike`]'s [`DualAxisData`] is within the given +/// bounds. If no minimum length is necessary, use `0.`. To exclude specifically neutral axis pairs, +/// use a small positive value. If no maximum length is necessary, use `f32::INFINITY`, or similar. +/// If rotation bounds are not necessary, use the same value for the minimum and maximum ex. +/// `Rotation::NORTH..Rotation::NORTH`. +pub fn axis_pair( + action: A, + length_bounds: Range, + rotation_bounds: Range, +) -> impl Trigger>> { + (move |In(entity): In, actors: Query<&ActionState>| { let axis_pair = actors .get(entity) .unwrap_or_else(|_| { @@ -167,208 +109,158 @@ impl Trigger for AxisPairTrigger { type_name::() ) }) - .axis_pair(self.action.clone()); + .axis_pair(action.clone()); axis_pair .and_then(|axis_pair| { let length = axis_pair.length(); let rotation = axis_pair.rotation(); - (length >= self.min_length - && length <= self.max_length + (length_bounds.contains(&length) && rotation - .map(|rotation| match self.min_rotation < self.max_rotation { - true => rotation >= self.min_rotation && rotation <= self.max_rotation, - false => rotation >= self.min_rotation || rotation <= self.max_rotation, + .map(|rotation| { + if rotation_bounds.start < rotation_bounds.end { + rotation >= rotation_bounds.start && rotation <= rotation_bounds.end + } else { + rotation >= rotation_bounds.start || rotation <= rotation_bounds.end + } }) .unwrap_or(true)) .then_some(axis_pair) }) .ok_or(axis_pair) - } + }) + .into_trigger() } -impl AxisPairTrigger { - /// Unbounded trigger - pub fn unbounded(action: A) -> Self { - Self { - action, - min_length: 0., - max_length: f32::INFINITY, - min_rotation: Rotation::NORTH, - max_rotation: Rotation::NORTH, - } - } - - /// Trigger with a minimum length bound - pub fn min_length(action: A, min_length: f32) -> Self { - Self { - action, - min_length, - max_length: f32::INFINITY, - min_rotation: Rotation::NORTH, - max_rotation: Rotation::NORTH, - } - } - - /// Trigger with a maximum length bound - pub fn max_length(action: A, max_length: f32) -> Self { - Self { - action, - min_length: 0., - max_length, - min_rotation: Rotation::NORTH, - max_rotation: Rotation::NORTH, - } - } - - /// Trigger with length bounds - pub fn length_bounds(action: A, min_length: f32, max_length: f32) -> Self { - Self { - action, - min_length, - max_length, - min_rotation: Rotation::NORTH, - max_rotation: Rotation::NORTH, - } - } - - /// Trigger with rotation bounds - pub fn rotation_bounds(action: A, min_rotation: Rotation, max_rotation: Rotation) -> Self { - Self { - action, - min_length: 0., - max_length: f32::INFINITY, - min_rotation, - max_rotation, - } - } +/// Unbounded [`axis_pair`] +pub fn axis_pair_unbounded( + action: impl Actionlike, +) -> impl Trigger>> { + axis_pair(action, 0.0..f32::INFINITY, Rotation::NORTH..Rotation::NORTH) } -/// Trigger that transitions if the given [`Actionlike`]'s [`DualAxisData`] is within the given -/// bounds -#[derive(Debug, Clone)] -pub struct ClampedAxisPairTrigger { - /// The action - pub action: A, - /// Minimum axis pair length. If no minimum is necessary, use `0.`. To exclude specifically - /// neutral axis pairs, use `f32::EPSILON`, or similar. - pub min_length: f32, - /// Maximum axis pair length. If no maximum is necessary, use `f32::INFINITY`, or similar. - pub max_length: f32, - /// Minimum rotation, measured clockwise from midnight. If rotation bounds are not necessary, - /// set this and `max_rotation` to the same value. - pub min_rotation: Rotation, - /// Maximum rotation, measured clockwise from midnight. If rotation bounds are not necessary, - /// set this and `min_rotation` to the same value. - pub max_rotation: Rotation, +/// [`axis_pair`] with only a minimum length bound +pub fn axis_pair_min_length( + action: impl Actionlike, + min_length: f32, +) -> impl Trigger>> { + axis_pair( + action, + min_length..f32::INFINITY, + Rotation::NORTH..Rotation::NORTH, + ) } -impl Trigger for ClampedAxisPairTrigger { - type Param<'w, 's> = Query<'w, 's, &'static ActionState>; - type Ok = DualAxisData; - type Err = Option; +/// [`axis_pair`] with only a maximum length bound +pub fn axis_pair_max_length( + action: impl Actionlike, + max_length: f32, +) -> impl Trigger>> { + axis_pair(action, 0.0..max_length, Rotation::NORTH..Rotation::NORTH) +} - fn trigger( - &self, - entity: Entity, - actors: Self::Param<'_, '_>, - ) -> Result> { +/// [`axis_pair`] with only length bounds +pub fn axis_pair_length_bounds( + action: impl Actionlike, + length_bounds: Range, +) -> impl Trigger>> { + axis_pair(action, length_bounds, Rotation::NORTH..Rotation::NORTH) +} + +/// [`axis_pair`] with only rotation bounds +pub fn axis_pair_rotation_bounds( + action: impl Actionlike, + rotation_bounds: Range, +) -> impl Trigger>> { + axis_pair(action, 0.0..f32::INFINITY, rotation_bounds) +} + +/// [`axis_pair`] with axes clamped to [-1, 1] +pub fn clamped_axis_pair( + action: A, + length_bounds: Range, + rotation_bounds: Range, +) -> impl Trigger>> { + (move |In(entity): In, actors: Query<&ActionState>| { let axis_pair = actors .get(entity) .unwrap_or_else(|_| { panic!( - "entity {entity:?} with `ClampedAxisPairTrigger<{0}>` is missing `ActionState<{0}>`", + "entity {entity:?} with `AxisPairTrigger<{0}>` is missing `ActionState<{0}>`", type_name::() ) }) - .axis_pair(self.action.clone()); + .clamped_axis_pair(action.clone()); axis_pair .and_then(|axis_pair| { let length = axis_pair.length(); let rotation = axis_pair.rotation(); - (length >= self.min_length - && length <= self.max_length + (length_bounds.contains(&length) && rotation - .map(|rotation| match self.min_rotation < self.max_rotation { - true => rotation >= self.min_rotation && rotation <= self.max_rotation, - false => rotation >= self.min_rotation || rotation <= self.max_rotation, + .map(|rotation| { + if rotation_bounds.start < rotation_bounds.end { + rotation >= rotation_bounds.start && rotation <= rotation_bounds.end + } else { + rotation >= rotation_bounds.start || rotation <= rotation_bounds.end + } }) .unwrap_or(true)) .then_some(axis_pair) }) .ok_or(axis_pair) - } + }) + .into_trigger() } -impl ClampedAxisPairTrigger { - /// Unbounded trigger - pub fn unbounded(action: A) -> Self { - Self { - action, - min_length: 0., - max_length: f32::INFINITY, - min_rotation: Rotation::NORTH, - max_rotation: Rotation::NORTH, - } - } - - /// Trigger with a minimum length bound - pub fn min_length(action: A, min_length: f32) -> Self { - Self { - action, - min_length, - max_length: f32::INFINITY, - min_rotation: Rotation::NORTH, - max_rotation: Rotation::NORTH, - } - } - - /// Trigger with a maximum length bound - pub fn max_length(action: A, max_length: f32) -> Self { - Self { - action, - min_length: 0., - max_length, - min_rotation: Rotation::NORTH, - max_rotation: Rotation::NORTH, - } - } - - /// Trigger with length bounds - pub fn length_bounds(action: A, min_length: f32, max_length: f32) -> Self { - Self { - action, - min_length, - max_length, - min_rotation: Rotation::NORTH, - max_rotation: Rotation::NORTH, - } - } - - /// Trigger with rotation bounds - pub fn rotation_bounds(action: A, min_rotation: Rotation, max_rotation: Rotation) -> Self { - Self { - action, - min_length: 0., - max_length: f32::INFINITY, - min_rotation, - max_rotation, - } - } +/// Unbounded [`clamped_axis_pair`] +pub fn clamped_axis_pair_unbounded( + action: impl Actionlike, +) -> impl Trigger>> { + clamped_axis_pair(action, 0.0..f32::INFINITY, Rotation::NORTH..Rotation::NORTH) } -/// Trigger that transitions upon pressing the given [`Actionlike`] -#[derive(Debug, Deref, DerefMut, Clone)] -pub struct JustPressedTrigger(pub A); +/// [`clamped_axis_pair`] with only a minimum length bound +pub fn clamped_axis_pair_min_length( + action: impl Actionlike, + min_length: f32, +) -> impl Trigger>> { + clamped_axis_pair( + action, + min_length..f32::INFINITY, + Rotation::NORTH..Rotation::NORTH, + ) +} -impl BoolTrigger for JustPressedTrigger { - type Param<'w, 's> = Query<'w, 's, &'static ActionState>; +/// [`clamped_axis_pair`] with only a maximum length bound +pub fn clamped_axis_pair_max_length( + action: impl Actionlike, + max_length: f32, +) -> impl Trigger>> { + clamped_axis_pair(action, 0.0..max_length, Rotation::NORTH..Rotation::NORTH) +} + +/// [`clamped_axis_pair`] with only length bounds +pub fn clamped_axis_pair_length_bounds( + action: impl Actionlike, + length_bounds: Range, +) -> impl Trigger>> { + clamped_axis_pair(action, length_bounds, Rotation::NORTH..Rotation::NORTH) +} - fn trigger(&self, entity: Entity, actors: Self::Param<'_, '_>) -> bool { - let Self(action) = self; +/// [`clamped_axis_pair`] with only rotation bounds +pub fn clamped_axis_pair_rotation_bounds( + action: impl Actionlike, + rotation_bounds: Range, +) -> impl Trigger>> { + clamped_axis_pair(action, 0.0..f32::INFINITY, rotation_bounds) +} + +/// Trigger that transitions upon pressing the given [`Actionlike`] +pub fn just_pressed(action: A) -> impl Trigger { + (move |In(entity): In, actors: Query<&ActionState>| { actors .get(entity) .unwrap_or_else(|_| { @@ -378,84 +270,45 @@ impl BoolTrigger for JustPressedTrigger { ) }) .just_pressed(action.clone()) - } + }) + .into_trigger() } /// Trigger that transitions while pressing the given [`Actionlike`] -#[derive(Debug, Deref, DerefMut, Clone)] -pub struct PressedTrigger(pub A); - -impl BoolTrigger for PressedTrigger { - type Param<'w, 's> = Query<'w, 's, &'static ActionState>; - - fn trigger(&self, entity: Entity, actors: Self::Param<'_, '_>) -> bool { - let Self(action) = self; +pub fn pressed(action: A) -> impl Trigger { + (move |In(entity): In, actors: Query<&ActionState>| { actors .get(entity) .unwrap_or_else(|_| { panic!( - "entity {entity:?} with `PressedTrigger<{0}>` is missing `ActionState<{0}>`", + "entity {entity:?} with `JustPressedTrigger<{0}>` is missing `ActionState<{0}>`", type_name::() ) }) .pressed(action.clone()) - } + }) + .into_trigger() } /// Trigger that transitions upon releasing the given [`Actionlike`] -#[derive(Debug, Deref, DerefMut, Clone)] -pub struct JustReleasedTrigger(pub A); - -#[cfg(feature = "leafwing_input")] -impl BoolTrigger for JustReleasedTrigger { - type Param<'w, 's> = Query<'w, 's, &'static ActionState>; - - fn trigger(&self, entity: Entity, actors: Self::Param<'_, '_>) -> bool { - let Self(action) = self; +pub fn just_released(action: A) -> impl Trigger { + (move |In(entity): In, actors: Query<&ActionState>| { actors .get(entity) .unwrap_or_else(|_| { panic!( - "entity {entity:?} with `JustReleasedTrigger<{0}>` is missing `ActionState<{0}>`", + "entity {entity:?} with `JustPressedTrigger<{0}>` is missing `ActionState<{0}>`", type_name::() ) }) .just_released(action.clone()) - } -} - -/// Trigger that transitions while the given [`Actionlike`] is released -#[derive(Debug, Deref, DerefMut, Clone)] -pub struct ReleasedTrigger(pub A); - -impl BoolTrigger for ReleasedTrigger { - type Param<'w, 's> = Query<'w, 's, &'static ActionState>; - - fn trigger(&self, entity: Entity, actors: Self::Param<'_, '_>) -> bool { - let Self(action) = self; - actors - .get(entity) - .unwrap_or_else(|_| { - panic!( - "entity {entity:?} with `ReleasedTrigger<{0}>` is missing `ActionState<{0}>`", - type_name::() - ) - }) - .just_pressed(action.clone()) - } + }) + .into_trigger() } /// Trigger that always transitions, providing the given [`Actionlike`]'s [`ActionData`] -#[derive(Debug, Deref, DerefMut, Clone)] -pub struct ActionDataTrigger(pub A); - -impl Trigger for ActionDataTrigger { - type Param<'w, 's> = Query<'w, 's, &'static ActionState>; - type Ok = ActionData; - type Err = Never; - - fn trigger(&self, entity: Entity, actors: Self::Param<'_, '_>) -> Result { - let Self(action) = self; +pub fn action_data(action: A) -> impl Trigger> { + (move |In(entity): In, actors: Query<&ActionState>| { Ok(actors .get(entity) .unwrap_or_else(|_| { @@ -466,5 +319,6 @@ impl Trigger for ActionDataTrigger { }) .action_data(action.clone()) .clone()) - } + }) + .into_trigger() }