diff --git a/Cargo.toml b/Cargo.toml index bfa559f..0f27b78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seldom_state" -version = "0.12.0" +version = "0.13.0-dev" edition = "2021" categories = ["game-development"] description = "Component-based state machine plugin for Bevy. Useful for AI, player state, and other entities that occupy various states." diff --git a/examples/done.rs b/examples/done.rs index f53ca7c..baa4669 100644 --- a/examples/done.rs +++ b/examples/done.rs @@ -21,11 +21,9 @@ fn init(mut commands: Commands, asset_server: Res) { Idle, StateMachine::default() // When the player clicks, go there - .trans_builder(click, |_: &AnyState, pos| { - Some(GoToSelection { - speed: 200., - target: pos, - }) + .trans_builder(click, |trans: Trans| GoToSelection { + speed: 200., + target: trans.out, }) // `done` triggers when the `Done` component is added to the entity. When they're done // going to the selection, idle. diff --git a/examples/input.rs b/examples/input.rs index d3362d7..f75d7ce 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -50,13 +50,18 @@ fn init(mut commands: Commands, asset_server: Res) { // When the player hits the ground, idle .trans::(grounded, Grounded::Idle) // When the player is grounded, set their movement direction - .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, - }) - }), + .trans_builder( + value_unbounded(Action::Move), + |trans: Trans| { + let value = trans.out; + + match value { + value if value > 0.5 => Grounded::Right, + value if value < -0.5 => Grounded::Left, + _ => Grounded::Idle, + } + }, + ), Sprite::from_image(asset_server.load("player.png")), Transform::from_xyz(500., 0., 0.), )); diff --git a/src/lib.rs b/src/lib.rs index 19cb4d2..67a0c3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ #![warn(missing_docs)] #![allow(clippy::type_complexity)] -mod machine; +pub mod machine; pub mod set; mod state; pub mod trigger; @@ -38,7 +38,7 @@ pub mod prelude { value_unbounded, }; pub use crate::{ - machine::StateMachine, + machine::{StateMachine, Trans}, state::{AnyState, EntityState}, trigger::{always, done, on_event, Done, EntityTrigger, IntoTrigger, Never}, StateMachinePlugin, diff --git a/src/machine.rs b/src/machine.rs index 23fa8c9..b34ecb5 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -1,3 +1,5 @@ +//! Module for the [`StateMachine`] component + use std::{ any::{type_name, Any, TypeId}, fmt::Debug, @@ -5,18 +7,14 @@ use std::{ }; use bevy::{ - ecs::{ - system::{EntityCommands, SystemState}, - world::Command, - }, - tasks::{ComputeTaskPool, ParallelSliceMut}, - utils::HashMap, + ecs::{system::EntityCommands, world::Command}, + utils::TypeIdMap, }; use crate::{ prelude::*, set::StateSet, - state::{Insert, OnEvent}, + state::OnEvent, trigger::{IntoTrigger, TriggerOut}, }; @@ -30,7 +28,11 @@ trait Transition: Debug + Send + Sync + 'static { fn init(&mut self, world: &mut World); /// Checks whether the transition should be taken. `entity` is the entity that contains the /// state machine. - fn check(&mut self, world: &World, entity: Entity) -> Option<(Box, TypeId)>; + fn check<'a>( + &'a mut self, + world: &World, + entity: Entity, + ) -> Option<(Box, TypeId)>; } /// An edge in the state machine. The type parameters are the [`EntityTrigger`] that causes this @@ -40,10 +42,7 @@ struct TransitionImpl where Trig: EntityTrigger, Prev: EntityState, - Build: 'static - + Fn(&Prev, <::Out as TriggerOut>::Ok) -> Option - + Send - + Sync, + Build: System::Ok>, Out = Next>, Next: Component + EntityState, { pub trigger: Trig, @@ -55,8 +54,7 @@ impl Debug for TransitionImpl where Trig: EntityTrigger, Prev: EntityState, - Build: - Fn(&Prev, <::Out as TriggerOut>::Ok) -> Option + Send + Sync, + Build: System::Ok>, Out = Next>, Next: Component + EntityState, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -72,21 +70,33 @@ impl Transition for TransitionImpl::Out as TriggerOut>::Ok) -> Option + Send + Sync, + Build: System::Ok>, Out = Next>, Next: Component + EntityState, { fn init(&mut self, world: &mut World) { self.trigger.init(world); + self.builder.initialize(world); } - 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::())) + fn check<'a>( + &'a mut self, + world: &World, + entity: Entity, + ) -> Option<(Box, TypeId)> { + self.trigger + .check(entity, world) + .into_result() + .map(|out| { + ( + Box::new(move |world: &mut World, curr: TypeId| { + let prev = Prev::remove(entity, world, curr); + let next = self.builder.run(TransCtx { prev, out, entity }, world); + world.entity_mut(entity).insert(next); + }) as Box, + TypeId::of::(), + ) + }) + .ok() } } @@ -94,8 +104,7 @@ impl TransitionImpl where Trig: EntityTrigger, Prev: EntityState, - Build: - Fn(&Prev, <::Out as TriggerOut>::Ok) -> Option + Send + Sync, + Build: System::Ok>, Out = Next>, Next: Component + EntityState, { pub fn new(trigger: Trig, builder: Build) -> Self { @@ -107,6 +116,19 @@ where } } +/// Context for a transition +pub struct TransCtx { + /// Previous state + pub prev: Prev, + /// Output from the trigger + pub out: Out, + /// The entity with this state machine + pub entity: Entity, +} + +/// Context for a transition, usable as a `SystemInput` +pub type Trans = In>; + /// Information about a state #[derive(Debug)] struct StateMetadata { @@ -119,11 +141,9 @@ struct StateMetadata { impl StateMetadata { fn new() -> Self { Self { - name: type_name::().to_owned(), - on_enter: default(), - on_exit: vec![OnEvent::Entity(Box::new(|entity: &mut EntityCommands| { - S::remove(entity); - }))], + name: type_name::().to_string(), + on_enter: Vec::new(), + on_exit: Vec::new(), } } } @@ -133,7 +153,7 @@ impl StateMetadata { /// `StateMachine::trans`, and other methods. #[derive(Component)] pub struct StateMachine { - states: HashMap, + states: TypeIdMap, /// Each transition and the state it should apply in (or [`AnyState`]). We store the transitions /// in a flat list so that we ensure we always check them in the right order; storing them in /// each StateMetadata would mean that e.g. we'd have to check every AnyState trigger before any @@ -147,16 +167,19 @@ pub struct StateMachine { impl Default for StateMachine { fn default() -> Self { + let mut states = TypeIdMap::default(); + states.insert( + TypeId::of::(), + StateMetadata { + name: "AnyState".to_owned(), + on_enter: vec![], + on_exit: vec![], + }, + ); + Self { - states: HashMap::from([( - TypeId::of::(), - StateMetadata { - name: "AnyState".to_owned(), - on_enter: vec![], - on_exit: vec![], - }, - )]), - transitions: vec![], + states, + transitions: Vec::new(), init_transitions: true, log_transitions: false, } @@ -179,7 +202,7 @@ impl StateMachine { trigger: impl IntoTrigger, state: impl Clone + Component, ) -> Self { - self.trans_builder(trigger, move |_: &S, _| Some(state.clone())) + self.trans_builder(trigger, move |_: Trans| state.clone()) } /// Get the metadata for the given state, creating it if necessary. @@ -194,21 +217,25 @@ impl StateMachine { /// `Some(Next)`, the machine will transition to that `Next` state. pub fn trans_builder< Prev: EntityState, - Trig: IntoTrigger, + Trig: IntoTrigger, Next: Clone + Component, - Marker, + TrigMarker, + BuildMarker, >( mut self, trigger: Trig, - builder: impl 'static - + Clone - + Fn(&Prev, <::Out as TriggerOut>::Ok) -> Option - + Send - + Sync, + builder: impl IntoSystem< + Trans::Out as TriggerOut>::Ok>, + Next, + BuildMarker, + >, ) -> Self { self.metadata_mut::(); self.metadata_mut::(); - let transition = TransitionImpl::<_, Prev, _, _>::new(trigger.into_trigger(), builder); + let transition = TransitionImpl::<_, Prev, _, _>::new( + trigger.into_trigger(), + IntoSystem::into_system(builder), + ); self.transitions.push(( TypeId::of::(), Box::new(transition) as Box, @@ -288,22 +315,25 @@ impl StateMachine { /// Runs all transitions until one is actually taken. If one is taken, logs the transition and /// runs `on_enter/on_exit` triggers. - fn run(&mut self, world: &World, entity: Entity, commands: &mut Commands) { + // TODO Defer the actual transition so this can be parallelized, and see if that improves perf + fn run(&mut self, world: &mut World, entity: Entity) { let mut states = self.states.keys(); let current = states.find(|&&state| world.entity(entity).contains_type_id(state)); let Some(¤t) = current else { - panic!("Entity {entity:?} is in no state"); + error!("Entity {entity:?} is in no state"); + return; }; let from = &self.states[¤t]; if let Some(&other) = states.find(|&&state| world.entity(entity).contains_type_id(state)) { let state = &from.name; let other = &self.states[&other].name; - panic!("{entity:?} is in multiple states: {state} and {other}"); + error!("{entity:?} is in multiple states: {state} and {other}"); + return; } - let Some((insert, next_state)) = self + let Some((trans, next_state)) = self .transitions .iter_mut() .filter(|(type_id, _)| *type_id == current || *type_id == TypeId::of::()) @@ -314,12 +344,13 @@ impl StateMachine { let to = &self.states[&next_state]; for event in from.on_exit.iter() { - event.trigger(entity, commands); + event.trigger(entity, &mut world.commands()); } - insert.insert(&mut commands.entity(entity)); + trans(world, current); + for event in to.on_enter.iter() { - event.trigger(entity, commands); + event.trigger(entity, &mut world.commands()); } if self.log_transitions { @@ -342,9 +373,10 @@ impl StateMachine { } /// Runs all transitions on all entities. +// There are comments here about parallelization, but this is not parallelized anymore. Leaving them +// here in case it gets parallelized again. pub(crate) fn transition( world: &mut World, - system_state: &mut SystemState, machine_query: &mut QueryState<(Entity, &mut StateMachine)>, ) { // Pull the machines out of the world so we can invoke mutable methods on them. The alternative @@ -366,14 +398,13 @@ pub(crate) fn transition( // `world` is not mutated here; the state machines are not in the world, and the Commands don't // mutate until application - let par_commands = system_state.get(world); - let task_pool = ComputeTaskPool::get(); + // let par_commands = system_state.get(world); + // let task_pool = ComputeTaskPool::get(); + // chunk size of None means to automatically pick - borrowed_machines.par_splat_map_mut(task_pool, None, |_, chunk| { - for (entity, machine) in chunk { - par_commands.command_scope(|mut commands| machine.run(world, *entity, &mut commands)); - } - }); + for &mut (entity, ref mut machine) in &mut borrowed_machines { + machine.run(world, entity); + } // put the borrowed machines back for (entity, machine) in borrowed_machines { @@ -381,7 +412,7 @@ pub(crate) fn transition( } // necessary to actually *apply* the commands we've enqueued - system_state.apply(world); + // system_state.apply(world); } #[cfg(test)] diff --git a/src/state.rs b/src/state.rs index 84d59b3..975f6c8 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,7 +1,4 @@ -use std::{ - any::TypeId, - fmt::{self, Debug, Formatter}, -}; +use std::fmt::{self, Debug, Formatter}; use bevy::ecs::{system::EntityCommands, world::Command}; @@ -10,31 +7,26 @@ use crate::prelude::*; use self::sealed::EntityStateSealed; mod sealed { - use bevy::ecs::system::EntityCommands; + use std::any::TypeId; use crate::prelude::*; - pub trait EntityStateSealed { - fn from_entity(entity: Entity, world: &World) -> &Self; - fn remove(entity: &mut EntityCommands); + pub trait EntityStateSealed: Sized { + fn remove(entity: Entity, world: &mut World, curr: TypeId) -> Self; } impl EntityStateSealed for T { - fn from_entity(entity: Entity, world: &World) -> &Self { - world.entity(entity).get().unwrap() - } - - fn remove(entity: &mut EntityCommands) { - entity.remove::(); + fn remove(entity: Entity, world: &mut World, _: TypeId) -> Self { + world.entity_mut(entity).take::().unwrap() } } impl EntityStateSealed for AnyState { - fn from_entity(_: Entity, _: &World) -> &Self { - &AnyState(()) + fn remove(entity: Entity, world: &mut World, curr: TypeId) -> Self { + let curr = world.components().get_id(curr).unwrap(); + world.entity_mut(entity).remove_by_id(curr); + AnyState(()) } - - fn remove(_: &mut EntityCommands) {} } } @@ -53,17 +45,6 @@ pub struct AnyState(pub(crate) ()); impl EntityState for AnyState {} -pub(crate) trait Insert: Send { - fn insert(self: Box, entity: &mut EntityCommands) -> TypeId; -} - -impl Insert for S { - fn insert(self: Box, entity: &mut EntityCommands) -> TypeId { - entity.insert(*self); - TypeId::of::() - } -} - #[derive(Debug)] pub(crate) enum OnEvent { Entity(Box),