├── .gitignore ├── Cargo.toml ├── src ├── macros.rs ├── world.rs ├── commands.rs ├── traits.rs ├── lib.rs └── plugin.rs ├── LICENSE-MIT ├── examples ├── sequence.rs ├── repeat.rs ├── pause.rs ├── parallel.rs ├── basic.rs └── custom.rs ├── README.md ├── CHANGELOG.md ├── LICENSE-APACHE └── tests └── tests.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy-sequential-actions" 3 | authors = ["hikikones"] 4 | version = "0.15.0-dev" 5 | edition = "2024" 6 | rust-version = "1.88.0" 7 | description = "A Bevy library for executing various actions in a sequence." 8 | readme = "README.md" 9 | homepage = "https://github.com/hikikones/bevy-sequential-actions" 10 | repository = "https://github.com/hikikones/bevy-sequential-actions" 11 | license = "MIT OR Apache-2.0" 12 | keywords = ["gamedev", "bevy", "action", "sequence", "command"] 13 | categories = ["game-development"] 14 | 15 | [dependencies] 16 | bevy_ecs = { version = "0.17", default-features = false } 17 | bevy_app = { version = "0.17", default-features = false } 18 | bevy_log = { version = "0.17", default-features = false } 19 | bevy_derive = { version = "0.17", default-features = false } 20 | variadics_please = { version = "1.1", default-features = false } 21 | downcast-rs = { version = "2.0", default-features = false } 22 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | /// Helper macro for creating an array of boxed actions. 2 | /// 3 | /// ```rust,no_run 4 | /// # use bevy_ecs::prelude::*; 5 | /// # use bevy_sequential_actions::*; 6 | /// # 7 | /// # struct EmptyAction; 8 | /// # impl Action for EmptyAction { 9 | /// # fn is_finished(&self, _a: Entity, _w: &World) -> bool { true } 10 | /// # fn on_start(&mut self, _a: Entity, _w: &mut World) -> bool { true } 11 | /// # fn on_stop(&mut self, _a: Option, _w: &mut World, _r: StopReason) {} 12 | /// # } 13 | /// # 14 | /// # let action_a = EmptyAction; 15 | /// # let action_b = EmptyAction; 16 | /// # 17 | /// let actions: [Box; 3] = actions![ 18 | /// action_a, 19 | /// action_b, 20 | /// |agent: Entity, world: &mut World| -> bool { 21 | /// // on_start 22 | /// true 23 | /// }, 24 | /// ]; 25 | /// ``` 26 | #[macro_export] 27 | macro_rules! actions { 28 | ( $( $action:expr ),+ $(,)? ) => { 29 | [ $( $crate::IntoBoxedAction::into_boxed_action($action) ),+ ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/world.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | impl ActionsProxy for World { 4 | fn actions(&mut self, agent: Entity) -> impl ManageActions { 5 | AgentActions { 6 | agent, 7 | config: AddConfig::default(), 8 | world: self, 9 | } 10 | } 11 | } 12 | 13 | /// Manage actions using [`World`]. 14 | pub struct AgentActions<'w> { 15 | agent: Entity, 16 | config: AddConfig, 17 | world: &'w mut World, 18 | } 19 | 20 | impl ManageActions for AgentActions<'_> { 21 | fn config(&mut self, config: AddConfig) -> &mut Self { 22 | self.config = config; 23 | self 24 | } 25 | 26 | fn start(&mut self, start: bool) -> &mut Self { 27 | self.config.start = start; 28 | self 29 | } 30 | 31 | fn order(&mut self, order: AddOrder) -> &mut Self { 32 | self.config.order = order; 33 | self 34 | } 35 | 36 | fn add(&mut self, actions: impl IntoBoxedActions) -> &mut Self { 37 | let mut actions = actions.into_boxed_actions(); 38 | match actions.len() { 39 | 0 => {} 40 | 1 => { 41 | SequentialActionsPlugin::add_action( 42 | self.agent, 43 | self.config, 44 | actions.next().unwrap(), 45 | self.world, 46 | ); 47 | } 48 | _ => { 49 | SequentialActionsPlugin::add_actions(self.agent, self.config, actions, self.world); 50 | } 51 | } 52 | self 53 | } 54 | 55 | fn execute(&mut self) -> &mut Self { 56 | SequentialActionsPlugin::execute_actions(self.agent, self.world); 57 | self 58 | } 59 | 60 | fn next(&mut self) -> &mut Self { 61 | SequentialActionsPlugin::stop_current_action(self.agent, StopReason::Canceled, self.world); 62 | SequentialActionsPlugin::start_next_action(self.agent, self.world); 63 | self 64 | } 65 | 66 | fn cancel(&mut self) -> &mut Self { 67 | SequentialActionsPlugin::stop_current_action(self.agent, StopReason::Canceled, self.world); 68 | self 69 | } 70 | 71 | fn pause(&mut self) -> &mut Self { 72 | SequentialActionsPlugin::stop_current_action(self.agent, StopReason::Paused, self.world); 73 | self 74 | } 75 | 76 | fn skip(&mut self, n: usize) -> &mut Self { 77 | SequentialActionsPlugin::skip_actions(self.agent, n, self.world); 78 | self 79 | } 80 | 81 | fn clear(&mut self) -> &mut Self { 82 | SequentialActionsPlugin::clear_actions(self.agent, self.world); 83 | self 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/sequence.rs: -------------------------------------------------------------------------------- 1 | use bevy_app::{AppExit, ScheduleRunnerPlugin, prelude::*}; 2 | use bevy_ecs::prelude::*; 3 | 4 | use bevy_sequential_actions::*; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins((ScheduleRunnerPlugin::default(), SequentialActionsPlugin)) 9 | .add_systems(Startup, setup) 10 | .run(); 11 | } 12 | 13 | fn setup(mut commands: Commands) { 14 | let agent = commands.spawn(SequentialActions).id(); 15 | commands.actions(agent).add(ActionSequence::new(actions![ 16 | PrintAction("see"), 17 | PrintAction("you"), 18 | PrintAction("in"), 19 | PrintAction("space"), 20 | PrintAction("cowboy"), 21 | |_agent, world: &mut World| -> bool { 22 | world.write_message(AppExit::Success); 23 | true 24 | } 25 | ])); 26 | } 27 | 28 | struct ActionSequence { 29 | actions: [BoxedAction; N], 30 | index: usize, 31 | } 32 | 33 | impl ActionSequence { 34 | fn new(actions: [BoxedAction; N]) -> Self { 35 | Self { actions, index: 0 } 36 | } 37 | } 38 | 39 | impl Action for ActionSequence { 40 | fn is_finished(&self, agent: Entity, world: &World) -> bool { 41 | self.actions[self.index].is_finished(agent, world) 42 | } 43 | 44 | fn on_add(&mut self, agent: Entity, world: &mut World) { 45 | self.actions 46 | .iter_mut() 47 | .for_each(|action| action.on_add(agent, world)); 48 | } 49 | 50 | fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { 51 | self.actions[self.index].on_start(agent, world) 52 | } 53 | 54 | fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { 55 | self.actions[self.index].on_stop(agent, world, reason); 56 | 57 | if reason == StopReason::Canceled { 58 | self.index = N; 59 | } 60 | } 61 | 62 | fn on_drop(mut self: Box, agent: Option, world: &mut World, reason: DropReason) { 63 | self.index += 1; 64 | 65 | if self.index >= N || reason != DropReason::Done { 66 | self.actions 67 | .iter_mut() 68 | .for_each(|action| action.on_remove(agent, world)); 69 | return; 70 | } 71 | 72 | let Some(agent) = agent else { return }; 73 | 74 | world 75 | .get_mut::(agent) 76 | .unwrap() 77 | .push_front(self); 78 | } 79 | } 80 | 81 | struct PrintAction(&'static str); 82 | 83 | impl Action for PrintAction { 84 | fn is_finished(&self, _agent: Entity, _world: &World) -> bool { 85 | true 86 | } 87 | 88 | fn on_start(&mut self, _agent: Entity, _world: &mut World) -> bool { 89 | println!("{}", self.0); 90 | false 91 | } 92 | 93 | fn on_stop(&mut self, _agent: Option, _world: &mut World, _reason: StopReason) {} 94 | } 95 | -------------------------------------------------------------------------------- /examples/repeat.rs: -------------------------------------------------------------------------------- 1 | use bevy_app::{AppExit, ScheduleRunnerPlugin, prelude::*}; 2 | use bevy_ecs::prelude::*; 3 | 4 | use bevy_sequential_actions::*; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins((ScheduleRunnerPlugin::default(), SequentialActionsPlugin)) 9 | .add_systems(Startup, setup) 10 | .run(); 11 | } 12 | 13 | fn setup(mut commands: Commands) { 14 | let agent = commands.spawn(SequentialActions).id(); 15 | commands.actions(agent).add(( 16 | RepeatAction { 17 | action: PrintAction("hello"), 18 | repeat: 3, 19 | }, 20 | RepeatAction { 21 | action: PrintAction("world"), 22 | repeat: 1, 23 | }, 24 | RepeatAction { 25 | action: |agent, world: &mut World| { 26 | // Exit app when action queue is empty 27 | if world.get::(agent).unwrap().is_empty() { 28 | world.write_message(AppExit::Success); 29 | } 30 | 31 | // Do not advance action queue immediately, 32 | // otherwise we get stuck in an infinite loop 33 | // as we keep readding this action 34 | false 35 | }, 36 | repeat: u32::MAX, 37 | }, 38 | )); 39 | } 40 | 41 | struct RepeatAction { 42 | action: A, 43 | repeat: u32, 44 | } 45 | 46 | impl Action for RepeatAction { 47 | fn is_finished(&self, agent: Entity, world: &World) -> bool { 48 | self.action.is_finished(agent, world) 49 | } 50 | 51 | fn on_add(&mut self, agent: Entity, world: &mut World) { 52 | self.action.on_add(agent, world); 53 | } 54 | 55 | fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { 56 | self.action.on_start(agent, world) 57 | } 58 | 59 | fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { 60 | self.action.on_stop(agent, world, reason); 61 | } 62 | 63 | fn on_remove(&mut self, agent: Option, world: &mut World) { 64 | self.action.on_remove(agent, world); 65 | } 66 | 67 | fn on_drop(mut self: Box, agent: Option, world: &mut World, reason: DropReason) { 68 | if self.repeat == 0 || reason != DropReason::Done { 69 | return; 70 | } 71 | 72 | let Some(agent) = agent else { return }; 73 | 74 | self.repeat -= 1; 75 | world.actions(agent).start(false).add(self as BoxedAction); 76 | } 77 | } 78 | 79 | struct PrintAction(&'static str); 80 | 81 | impl Action for PrintAction { 82 | fn is_finished(&self, _agent: Entity, _world: &World) -> bool { 83 | true 84 | } 85 | 86 | fn on_start(&mut self, _agent: Entity, _world: &mut World) -> bool { 87 | println!("{}", self.0); 88 | true 89 | } 90 | 91 | fn on_stop(&mut self, _agent: Option, _world: &mut World, _reason: StopReason) {} 92 | } 93 | -------------------------------------------------------------------------------- /examples/pause.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use bevy_app::{AppExit, ScheduleRunnerPlugin, prelude::*}; 4 | use bevy_ecs::prelude::*; 5 | 6 | use bevy_sequential_actions::*; 7 | 8 | fn main() { 9 | App::new() 10 | .add_plugins(( 11 | ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0 / 10.0)), 12 | SequentialActionsPlugin, 13 | )) 14 | .add_systems(Startup, setup) 15 | .add_systems(Update, (count, frame_logic).chain()) 16 | .run(); 17 | } 18 | 19 | fn setup(mut commands: Commands) { 20 | let agent = commands.spawn(SequentialActions).id(); 21 | commands.actions(agent).add(CountForeverAction); 22 | } 23 | 24 | fn frame_logic( 25 | mut frame: Local, 26 | mut commands: Commands, 27 | agent_q: Single>, 28 | ) { 29 | const PAUSE_FRAME: u32 = 10; 30 | const RESUME_FRAME: u32 = PAUSE_FRAME * 2; 31 | const EXIT_FRAME: u32 = PAUSE_FRAME * 3; 32 | const MISSING_FRAMES: u32 = RESUME_FRAME - PAUSE_FRAME; 33 | 34 | println!("Frame: {}", *frame); 35 | 36 | let agent = agent_q.entity(); 37 | 38 | if *frame == PAUSE_FRAME { 39 | println!("\nPAUSE\n"); 40 | commands.actions(agent).pause(); 41 | } 42 | if *frame == RESUME_FRAME { 43 | println!("\nRESUME\n"); 44 | commands.actions(agent).execute(); 45 | } 46 | if *frame == EXIT_FRAME { 47 | println!( 48 | "\nEXIT - Frame is now at {}, and Count should be {} (missing {} frames)", 49 | *frame, 50 | EXIT_FRAME - MISSING_FRAMES, 51 | MISSING_FRAMES, 52 | ); 53 | commands.queue(|world: &mut World| { 54 | world.write_message(AppExit::Success); 55 | }); 56 | } 57 | 58 | *frame += 1; 59 | } 60 | 61 | struct CountForeverAction; 62 | 63 | impl Action for CountForeverAction { 64 | fn is_finished(&self, _agent: Entity, _world: &World) -> bool { 65 | false 66 | } 67 | 68 | fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { 69 | let mut agent = world.entity_mut(agent); 70 | 71 | if agent.contains::() { 72 | agent.remove::(); 73 | } else { 74 | agent.insert(Count::default()); 75 | } 76 | 77 | false 78 | } 79 | 80 | fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { 81 | match reason { 82 | StopReason::Finished | StopReason::Canceled => { 83 | world.entity_mut(agent.unwrap()).remove::(); 84 | } 85 | StopReason::Paused => { 86 | world.entity_mut(agent.unwrap()).insert(Paused); 87 | } 88 | } 89 | } 90 | } 91 | 92 | #[derive(Default, Component)] 93 | struct Count(u32); 94 | 95 | #[derive(Component)] 96 | struct Paused; 97 | 98 | fn count(mut count_q: Query<&mut Count, Without>) { 99 | for mut count in &mut count_q { 100 | println!("Count: {}", count.0); 101 | count.0 += 1; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/commands.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | impl ActionsProxy for Commands<'_, '_> { 4 | fn actions(&mut self, agent: Entity) -> impl ManageActions { 5 | AgentCommands { 6 | agent, 7 | config: AddConfig::default(), 8 | commands: self, 9 | } 10 | } 11 | } 12 | 13 | /// Manage actions using [`Commands`]. 14 | pub struct AgentCommands<'c, 'w, 's> { 15 | agent: Entity, 16 | config: AddConfig, 17 | commands: &'c mut Commands<'w, 's>, 18 | } 19 | 20 | impl ManageActions for AgentCommands<'_, '_, '_> { 21 | fn config(&mut self, config: AddConfig) -> &mut Self { 22 | self.config = config; 23 | self 24 | } 25 | 26 | fn start(&mut self, start: bool) -> &mut Self { 27 | self.config.start = start; 28 | self 29 | } 30 | 31 | fn order(&mut self, order: AddOrder) -> &mut Self { 32 | self.config.order = order; 33 | self 34 | } 35 | 36 | fn add(&mut self, action: impl IntoBoxedActions) -> &mut Self { 37 | let mut actions = action.into_boxed_actions(); 38 | 39 | match actions.len() { 40 | 0 => {} 41 | 1 => { 42 | let agent = self.agent; 43 | let config = self.config; 44 | let action = actions.next().unwrap(); 45 | self.commands.queue(move |world: &mut World| { 46 | SequentialActionsPlugin::add_action(agent, config, action, world); 47 | }); 48 | } 49 | _ => { 50 | let agent = self.agent; 51 | let config = self.config; 52 | self.commands.queue(move |world: &mut World| { 53 | SequentialActionsPlugin::add_actions(agent, config, actions, world); 54 | }); 55 | } 56 | } 57 | 58 | self 59 | } 60 | 61 | fn execute(&mut self) -> &mut Self { 62 | let agent = self.agent; 63 | 64 | self.commands.queue(move |world: &mut World| { 65 | SequentialActionsPlugin::execute_actions(agent, world); 66 | }); 67 | 68 | self 69 | } 70 | 71 | fn next(&mut self) -> &mut Self { 72 | let agent = self.agent; 73 | 74 | self.commands.queue(move |world: &mut World| { 75 | SequentialActionsPlugin::stop_current_action(agent, StopReason::Canceled, world); 76 | SequentialActionsPlugin::start_next_action(agent, world); 77 | }); 78 | 79 | self 80 | } 81 | 82 | fn cancel(&mut self) -> &mut Self { 83 | let agent = self.agent; 84 | 85 | self.commands.queue(move |world: &mut World| { 86 | SequentialActionsPlugin::stop_current_action(agent, StopReason::Canceled, world); 87 | }); 88 | 89 | self 90 | } 91 | 92 | fn pause(&mut self) -> &mut Self { 93 | let agent = self.agent; 94 | 95 | self.commands.queue(move |world: &mut World| { 96 | SequentialActionsPlugin::stop_current_action(agent, StopReason::Paused, world); 97 | }); 98 | 99 | self 100 | } 101 | 102 | fn skip(&mut self, n: usize) -> &mut Self { 103 | let agent = self.agent; 104 | 105 | self.commands.queue(move |world: &mut World| { 106 | SequentialActionsPlugin::skip_actions(agent, n, world); 107 | }); 108 | 109 | self 110 | } 111 | 112 | fn clear(&mut self) -> &mut Self { 113 | let agent = self.agent; 114 | 115 | self.commands.queue(move |world: &mut World| { 116 | SequentialActionsPlugin::clear_actions(agent, world); 117 | }); 118 | 119 | self 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /examples/parallel.rs: -------------------------------------------------------------------------------- 1 | use bevy_app::{AppExit, ScheduleRunnerPlugin, prelude::*}; 2 | use bevy_ecs::prelude::*; 3 | 4 | use bevy_sequential_actions::*; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins((ScheduleRunnerPlugin::default(), SequentialActionsPlugin)) 9 | .add_systems(Startup, setup) 10 | .add_systems(Update, countdown) 11 | .run(); 12 | } 13 | 14 | fn setup(mut commands: Commands) { 15 | let agent = commands.spawn(SequentialActions).id(); 16 | commands.actions(agent).add(( 17 | ParallelActions::new(actions![ 18 | PrintAction("hello"), 19 | CountdownAction::new(2), 20 | PrintAction("world"), 21 | CountdownAction::new(4), 22 | ]), 23 | |_agent, world: &mut World| { 24 | world.write_message(AppExit::Success); 25 | false 26 | }, 27 | )); 28 | } 29 | 30 | struct ParallelActions { 31 | actions: [BoxedAction; N], 32 | } 33 | 34 | impl ParallelActions { 35 | const fn new(actions: [BoxedAction; N]) -> Self { 36 | Self { actions } 37 | } 38 | } 39 | 40 | impl Action for ParallelActions { 41 | fn is_finished(&self, agent: Entity, world: &World) -> bool { 42 | self.actions 43 | .iter() 44 | .all(|action| action.is_finished(agent, world)) 45 | } 46 | 47 | fn on_add(&mut self, agent: Entity, world: &mut World) { 48 | self.actions 49 | .iter_mut() 50 | .for_each(|action| action.on_add(agent, world)); 51 | } 52 | 53 | fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { 54 | std::array::from_fn::(|i| self.actions[i].on_start(agent, world)) 55 | .into_iter() 56 | .all(|b| b) 57 | } 58 | 59 | fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { 60 | self.actions 61 | .iter_mut() 62 | .for_each(|action| action.on_stop(agent, world, reason)); 63 | } 64 | 65 | fn on_remove(&mut self, agent: Option, world: &mut World) { 66 | self.actions 67 | .iter_mut() 68 | .for_each(|action| action.on_remove(agent, world)); 69 | } 70 | } 71 | 72 | struct PrintAction(&'static str); 73 | 74 | impl Action for PrintAction { 75 | fn is_finished(&self, _agent: Entity, _world: &World) -> bool { 76 | true 77 | } 78 | 79 | fn on_start(&mut self, _agent: Entity, _world: &mut World) -> bool { 80 | println!("{}", self.0); 81 | true 82 | } 83 | 84 | fn on_stop(&mut self, _agent: Option, _world: &mut World, _reason: StopReason) {} 85 | } 86 | 87 | struct CountdownAction { 88 | count: u32, 89 | entity: Entity, 90 | } 91 | 92 | impl CountdownAction { 93 | const fn new(count: u32) -> Self { 94 | Self { 95 | count, 96 | entity: Entity::PLACEHOLDER, 97 | } 98 | } 99 | } 100 | 101 | impl Action for CountdownAction { 102 | fn is_finished(&self, _agent: Entity, world: &World) -> bool { 103 | world.get::(self.entity).unwrap().0 == 0 104 | } 105 | 106 | fn on_add(&mut self, _agent: Entity, world: &mut World) { 107 | self.entity = world.spawn_empty().id(); 108 | } 109 | 110 | fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { 111 | let mut entity = world.entity_mut(self.entity); 112 | 113 | if entity.contains::() { 114 | entity.remove::(); 115 | } else { 116 | entity.insert(Countdown(self.count)); 117 | println!("Countdown({}): {}", self.entity, self.count); 118 | } 119 | 120 | self.is_finished(agent, world) 121 | } 122 | 123 | fn on_stop(&mut self, _agent: Option, world: &mut World, reason: StopReason) { 124 | if reason == StopReason::Paused { 125 | world.entity_mut(self.entity).insert(Paused); 126 | } 127 | } 128 | 129 | fn on_remove(&mut self, _agent: Option, world: &mut World) { 130 | world.despawn(self.entity); 131 | } 132 | } 133 | 134 | #[derive(Component)] 135 | struct Countdown(u32); 136 | 137 | #[derive(Component)] 138 | struct Paused; 139 | 140 | fn countdown(mut countdown_q: Query<(Entity, &mut Countdown), Without>) { 141 | for (entity, mut countdown) in &mut countdown_q { 142 | countdown.0 = countdown.0.saturating_sub(1); 143 | println!("Countdown({}): {}", entity, countdown.0); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use bevy_app::{AppExit, ScheduleRunnerPlugin, prelude::*}; 2 | use bevy_ecs::prelude::*; 3 | 4 | use bevy_sequential_actions::*; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins((ScheduleRunnerPlugin::default(), SequentialActionsPlugin)) 9 | .add_systems(Startup, setup) 10 | .add_systems(Update, countdown) 11 | .run(); 12 | } 13 | 14 | fn setup(mut commands: Commands) { 15 | // Spawn entity with the marker component 16 | let agent = commands.spawn(SequentialActions).id(); 17 | commands 18 | .actions(agent) 19 | // Add a single action 20 | .add(DemoAction) 21 | // Add more actions with a tuple 22 | .add(( 23 | PrintAction("hello"), 24 | PrintAction("there"), 25 | CountdownAction::new(5), 26 | )) 27 | // Add a collection of actions 28 | .add(actions![ 29 | PrintAction("it is possible to commit no mistakes and still lose"), 30 | PrintAction("that is not a weakness"), 31 | PrintAction("that is life"), 32 | CountdownAction::new(10), 33 | ]) 34 | // Add an anonymous action with a closure 35 | .add(|_agent, world: &mut World| -> bool { 36 | // on_start 37 | world.write_message(AppExit::Success); 38 | true 39 | }); 40 | } 41 | 42 | struct DemoAction; 43 | 44 | impl Action for DemoAction { 45 | // Required method 46 | fn is_finished(&self, _agent: Entity, _world: &World) -> bool { 47 | println!("is_finished: called every frame in the Last schedule"); 48 | true 49 | } 50 | 51 | // Required method 52 | fn on_start(&mut self, _agent: Entity, _world: &mut World) -> bool { 53 | println!("on_start: called when an action is started"); 54 | 55 | // Returning true here marks the action as already finished, 56 | // and will immediately advance the action queue. 57 | false 58 | } 59 | 60 | // Required method 61 | fn on_stop(&mut self, _agent: Option, _world: &mut World, _reason: StopReason) { 62 | println!("on_stop: called when an action is stopped"); 63 | } 64 | 65 | // Optional method (empty by default) 66 | fn on_add(&mut self, _agent: Entity, _world: &mut World) { 67 | println!("on_add: called when an action is added to the queue"); 68 | } 69 | 70 | // Optional method (empty by default) 71 | fn on_remove(&mut self, _agent: Option, _world: &mut World) { 72 | println!("on_remove: called when an action is removed from the queue"); 73 | } 74 | 75 | // Optional method (empty by default) 76 | fn on_drop(self: Box, _agent: Option, _world: &mut World, _reason: DropReason) { 77 | println!("on_drop: the last method to be called with full ownership"); 78 | } 79 | } 80 | 81 | struct PrintAction(&'static str); 82 | 83 | impl Action for PrintAction { 84 | fn is_finished(&self, _agent: Entity, _world: &World) -> bool { 85 | true 86 | } 87 | 88 | fn on_start(&mut self, _agent: Entity, _world: &mut World) -> bool { 89 | println!("{}", self.0); 90 | true 91 | } 92 | 93 | fn on_stop(&mut self, _agent: Option, _world: &mut World, _reason: StopReason) {} 94 | } 95 | 96 | struct CountdownAction { 97 | count: u32, 98 | current: Option, 99 | } 100 | 101 | impl CountdownAction { 102 | const fn new(count: u32) -> Self { 103 | Self { 104 | count, 105 | current: None, 106 | } 107 | } 108 | } 109 | 110 | impl Action for CountdownAction { 111 | fn is_finished(&self, agent: Entity, world: &World) -> bool { 112 | let current_count = world.get::(agent).unwrap().0; 113 | println!("Countdown: {current_count}"); 114 | 115 | // Determine if countdown has reached zero 116 | current_count == 0 117 | } 118 | 119 | fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { 120 | // Take current count (if paused), or use full count 121 | let count = self.current.take().unwrap_or(self.count); 122 | 123 | // Run the countdown system on the agent 124 | world.entity_mut(agent).insert(Countdown(count)); 125 | 126 | // Is action already finished? 127 | self.is_finished(agent, world) 128 | } 129 | 130 | fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { 131 | // Do nothing if agent has been despawned. 132 | let Some(agent) = agent else { return }; 133 | 134 | // Take the countdown component from the agent 135 | let countdown = world.entity_mut(agent).take::(); 136 | 137 | // Store current count when paused 138 | if reason == StopReason::Paused { 139 | self.current = countdown.unwrap().0.into(); 140 | } 141 | } 142 | } 143 | 144 | #[derive(Component)] 145 | struct Countdown(u32); 146 | 147 | fn countdown(mut countdown_q: Query<&mut Countdown>) { 148 | for mut countdown in &mut countdown_q { 149 | countdown.0 -= 1; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /examples/custom.rs: -------------------------------------------------------------------------------- 1 | use std::{marker::PhantomData, time::Duration}; 2 | 3 | use bevy_app::{AppExit, ScheduleRunnerPlugin, prelude::*}; 4 | use bevy_ecs::{prelude::*, query::QueryFilter, schedule::ScheduleLabel}; 5 | 6 | use bevy_sequential_actions::*; 7 | 8 | fn main() { 9 | App::new() 10 | .init_schedule(EvenSchedule) 11 | .init_schedule(OddSchedule) 12 | .add_plugins(( 13 | ScheduleRunnerPlugin::run_loop(Duration::from_millis(100)), 14 | // Add custom plugin for the even schedule 15 | CustomSequentialActionsPlugin::new(EvenSchedule) 16 | .with_cleanup() 17 | .with_filter::>(), 18 | // Add custom plugin for the odd schedule 19 | CustomSequentialActionsPlugin::new(OddSchedule) 20 | // No cleanup for odd agents 21 | .with_filter::>(), 22 | )) 23 | .add_systems(Startup, setup) 24 | .add_systems(Update, run_custom_schedules) 25 | .run(); 26 | } 27 | 28 | #[derive(Debug, Clone, PartialEq, Eq, Hash, ScheduleLabel)] 29 | struct EvenSchedule; 30 | 31 | #[derive(Debug, Clone, PartialEq, Eq, Hash, ScheduleLabel)] 32 | struct OddSchedule; 33 | 34 | #[derive(Component)] 35 | struct EvenMarker; 36 | 37 | #[derive(Component)] 38 | struct OddMarker; 39 | 40 | fn setup(mut commands: Commands) { 41 | // Spawn agent with even marker for even schedule 42 | let agent_even = commands.spawn((SequentialActions, EvenMarker)).id(); 43 | commands 44 | .actions(agent_even) 45 | .add(PrintForeverAction::new(format!( 46 | "Even: is_finished is called every even frame for agent {agent_even}." 47 | ))); 48 | 49 | // Spawn agent with odd marker for odd schedule 50 | let agent_odd = commands.spawn((SequentialActions, OddMarker)).id(); 51 | commands 52 | .actions(agent_odd) 53 | .add(PrintForeverAction::new(format!( 54 | "Odd: is_finished is called every odd frame for agent {agent_odd}." 55 | ))); 56 | } 57 | 58 | fn run_custom_schedules( 59 | world: &mut World, 60 | mut frame_count: Local, 61 | mut agent_q: Local>>, 62 | ) { 63 | if *frame_count % 2 == 0 { 64 | world.run_schedule(EvenSchedule); 65 | } else { 66 | world.run_schedule(OddSchedule); 67 | } 68 | 69 | if *frame_count == 10 { 70 | for agent in agent_q.iter(world).collect::>() { 71 | world.despawn(agent); 72 | } 73 | world.write_message(AppExit::Success); 74 | } 75 | 76 | *frame_count += 1; 77 | } 78 | 79 | struct PrintForeverAction { 80 | message: String, 81 | agent: Entity, 82 | } 83 | 84 | impl PrintForeverAction { 85 | fn new(message: String) -> Self { 86 | Self { 87 | message, 88 | agent: Entity::PLACEHOLDER, 89 | } 90 | } 91 | } 92 | 93 | impl Action for PrintForeverAction { 94 | fn is_finished(&self, _agent: Entity, _world: &World) -> bool { 95 | println!("{}", self.message); 96 | false 97 | } 98 | fn on_start(&mut self, agent: Entity, _world: &mut World) -> bool { 99 | self.agent = agent; 100 | false 101 | } 102 | fn on_stop(&mut self, _agent: Option, _world: &mut World, _reason: StopReason) {} 103 | fn on_drop(self: Box, _agent: Option, _world: &mut World, _reason: DropReason) { 104 | // Notice that this is not called for odd agents when despawned... 105 | println!("Dropping action for agent {}...", self.agent); 106 | } 107 | } 108 | 109 | /// Custom plugin for sequential actions. 110 | /// 111 | /// Action queue advancement will run in the specified schedule `S`, 112 | /// and only for agents matching the specified query filter `F`. 113 | /// With `cleanup` enabled, an observer will trigger for despawned agents 114 | /// that ensures any remaining action is cleaned up. 115 | struct CustomSequentialActionsPlugin { 116 | schedule: S, 117 | cleanup: bool, 118 | filter: PhantomData, 119 | } 120 | 121 | impl CustomSequentialActionsPlugin { 122 | const fn new(schedule: S) -> Self { 123 | Self { 124 | schedule, 125 | cleanup: false, 126 | filter: PhantomData, 127 | } 128 | } 129 | 130 | const fn with_cleanup(mut self) -> Self { 131 | self.cleanup = true; 132 | self 133 | } 134 | 135 | fn with_filter(self) -> CustomSequentialActionsPlugin { 136 | CustomSequentialActionsPlugin { 137 | schedule: self.schedule, 138 | cleanup: self.cleanup, 139 | filter: PhantomData, 140 | } 141 | } 142 | } 143 | 144 | impl CustomSequentialActionsPlugin { 145 | fn check_actions_exclusive( 146 | world: &mut World, 147 | mut finished: Local>, 148 | mut agent_q: Local>, 149 | ) { 150 | // Collect all agents with finished action 151 | finished.extend(agent_q.iter(world).filter_map(|(agent, current_action)| { 152 | current_action 153 | .as_ref() 154 | .and_then(|action| action.is_finished(agent, world).then_some(agent)) 155 | })); 156 | 157 | // Do something with the finished list if you want. 158 | // Perhaps sort by some identifier for deterministic behavior. 159 | 160 | // Advance the action queue 161 | for agent in finished.drain(..) { 162 | SequentialActionsPlugin::stop_current_action(agent, StopReason::Finished, world); 163 | SequentialActionsPlugin::start_next_action(agent, world); 164 | } 165 | } 166 | } 167 | 168 | impl Default for CustomSequentialActionsPlugin { 169 | fn default() -> Self { 170 | Self::new(Last).with_cleanup() 171 | } 172 | } 173 | 174 | impl Plugin 175 | for CustomSequentialActionsPlugin 176 | { 177 | fn build(&self, app: &mut App) { 178 | // Add system for advancing action queue to specified schedule 179 | app.add_systems(self.schedule.clone(), Self::check_actions_exclusive); 180 | 181 | // Add observers for cleanup of actions when despawning agents 182 | if self.cleanup { 183 | app.add_observer(CurrentAction::on_remove_trigger::) 184 | .add_observer(ActionQueue::on_remove_trigger::); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Bevy Sequential Actions 4 | 5 | [![crates.io](https://img.shields.io/crates/v/bevy-sequential-actions?style=flat-square)](https://crates.io/crates/bevy-sequential-actions) 6 | [![docs.rs](https://img.shields.io/docsrs/bevy-sequential-actions?style=flat-square)](https://docs.rs/bevy_sequential_actions) 7 | [![MIT/Apache 2.0](https://img.shields.io/crates/l/bevy-sequential-actions?style=flat-square)](https://github.com/hikikones/bevy-sequential-actions#license) 8 | 9 | 10 | A simple library for managing and sequencing various actions in [Bevy](https://bevyengine.org). 11 | 12 |
13 | 14 |

An entity with a queue of repeating actions

15 |
16 | 17 |
18 | 19 | ## 📜 Getting Started 20 | 21 | #### Plugin 22 | 23 | The quickest way for getting started is adding the `SequentialActionsPlugin` to your `App`. 24 | 25 | ```rust 26 | use bevy_sequential_actions::*; 27 | 28 | fn main() { 29 | App::new() 30 | .add_plugins((DefaultPlugins, SequentialActionsPlugin)) 31 | .run(); 32 | } 33 | ``` 34 | 35 | #### Implementing an Action 36 | 37 | An action is anything that implements the `Action` trait. 38 | The trait contains various methods that together defines the _lifecycle_ of an action. 39 | From this, you can create any action that can last as long as you like, 40 | and do as much as you like. 41 | 42 | An entity with actions is referred to as an `agent`. 43 | 44 | A simple wait action follows. 45 | 46 | ```rust 47 | pub struct WaitAction { 48 | duration: f32, // Seconds 49 | current: Option, // None 50 | } 51 | 52 | impl Action for WaitAction { 53 | // By default, this method is called every frame in the Last schedule. 54 | fn is_finished(&self, agent: Entity, world: &World) -> bool { 55 | // Determine if wait timer has reached zero. 56 | world.get::(agent).unwrap().0 <= 0.0 57 | } 58 | 59 | // This method is called when an action is started. 60 | fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { 61 | // Take current time (if paused), or use full duration. 62 | let duration = self.current.take().unwrap_or(self.duration); 63 | 64 | // Run the wait timer system on the agent. 65 | world.entity_mut(agent).insert(WaitTimer(duration)); 66 | 67 | // Is action already finished? 68 | // Returning true here will immediately advance the action queue. 69 | self.is_finished(agent, world) 70 | } 71 | 72 | // This method is called when an action is stopped. 73 | fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { 74 | // Do nothing if agent has been despawned. 75 | let Some(agent) = agent else { return }; 76 | 77 | // Take the wait timer component from the agent. 78 | let wait_timer = world.entity_mut(agent).take::(); 79 | 80 | // Store current time when paused. 81 | if reason == StopReason::Paused { 82 | self.current = Some(wait_timer.unwrap().0); 83 | } 84 | } 85 | 86 | // Optional. This method is called when an action is added to the queue. 87 | fn on_add(&mut self, agent: Entity, world: &mut World) {} 88 | 89 | // Optional. This method is called when an action is removed from the queue. 90 | fn on_remove(&mut self, agent: Option, world: &mut World) {} 91 | 92 | // Optional. The last method that is called with full ownership. 93 | fn on_drop(self: Box, agent: Option, world: &mut World, reason: DropReason) {} 94 | } 95 | 96 | #[derive(Component)] 97 | struct WaitTimer(f32); 98 | 99 | fn wait_system(mut wait_timer_q: Query<&mut WaitTimer>, time: Res
::default(), 601 | MarkerAction::::default(), 602 | MarkerAction::::default(), 603 | )); 604 | 605 | assert_eq!(app.entity(a).contains::(), true); 606 | assert_eq!(app.entity(a).contains::(), false); 607 | assert_eq!(app.entity(a).contains::(), false); 608 | 609 | app.update(); 610 | 611 | assert_eq!(app.entity(a).contains::(), false); 612 | assert_eq!(app.entity(a).contains::(), true); 613 | assert_eq!(app.entity(a).contains::(), false); 614 | 615 | app.update(); 616 | 617 | assert_eq!(app.entity(a).contains::(), false); 618 | assert_eq!(app.entity(a).contains::(), false); 619 | assert_eq!(app.entity(a).contains::(), true); 620 | 621 | // Front 622 | app.actions(a) 623 | .clear() 624 | .start(false) 625 | .add(|_a, _w: &mut World| false) 626 | .order(AddOrder::Front) 627 | .add(( 628 | MarkerAction::::default(), 629 | MarkerAction::::default(), 630 | MarkerAction::::default(), 631 | )) 632 | .execute(); 633 | 634 | assert_eq!(app.entity(a).contains::(), true); 635 | assert_eq!(app.entity(a).contains::(), false); 636 | assert_eq!(app.entity(a).contains::(), false); 637 | 638 | app.update(); 639 | 640 | assert_eq!(app.entity(a).contains::(), false); 641 | assert_eq!(app.entity(a).contains::(), true); 642 | assert_eq!(app.entity(a).contains::(), false); 643 | 644 | app.update(); 645 | 646 | assert_eq!(app.entity(a).contains::(), false); 647 | assert_eq!(app.entity(a).contains::(), false); 648 | assert_eq!(app.entity(a).contains::(), true); 649 | } 650 | 651 | #[test] 652 | fn pause_resume() { 653 | let mut app = TestApp::new(); 654 | let a = app.spawn_agent(); 655 | 656 | app.actions(a).add(CountdownAction::new(10)); 657 | 658 | assert_eq!(app.entity(a).get::().unwrap().0, 10); 659 | 660 | app.update(); 661 | 662 | assert_eq!(app.entity(a).get::().unwrap().0, 9); 663 | 664 | app.actions(a) 665 | .pause() 666 | .order(AddOrder::Front) 667 | .add(CountdownAction::new(1)); 668 | 669 | assert_eq!(app.entity(a).get::().unwrap().0, 1); 670 | 671 | app.update(); 672 | 673 | assert_eq!(app.entity(a).get::().unwrap().0, 9); 674 | } 675 | 676 | #[test] 677 | fn despawn() { 678 | let mut app = TestApp::new(); 679 | let a = app.spawn_agent(); 680 | 681 | app.actions(a) 682 | .add((CountdownAction::new(10), CountupAction::new(10))); 683 | app.update(); 684 | app.despawn(a); 685 | 686 | assert!(app.get_entity(a).is_none()); 687 | assert_eq!( 688 | app.hooks().deref().clone(), 689 | vec![ 690 | Hook::Add(Name::Countdown, a), 691 | Hook::Add(Name::Countup, a), 692 | Hook::Start(Name::Countdown, a), 693 | Hook::Stop(Name::Countdown, None, StopReason::Canceled), 694 | Hook::Remove(Name::Countdown, None), 695 | Hook::Drop(Name::Countdown, None, DropReason::Done), 696 | Hook::Remove(Name::Countup, None), 697 | Hook::Drop(Name::Countup, None, DropReason::Cleared) 698 | ] 699 | ); 700 | } 701 | 702 | #[test] 703 | fn despawn_action() { 704 | struct DespawnAction; 705 | impl Action for DespawnAction { 706 | fn is_finished(&self, _agent: Entity, _world: &World) -> bool { 707 | true 708 | } 709 | fn on_add(&mut self, agent: Entity, world: &mut World) { 710 | Name::Despawn.on_add(agent, world); 711 | } 712 | fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { 713 | Name::Despawn.on_start(agent, world); 714 | world.despawn(agent); 715 | B 716 | } 717 | fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { 718 | Name::Despawn.on_stop(agent, world, reason); 719 | } 720 | fn on_remove(&mut self, agent: Option, world: &mut World) { 721 | Name::Despawn.on_remove(agent, world); 722 | } 723 | fn on_drop(self: Box, agent: Option, world: &mut World, reason: DropReason) { 724 | Name::Despawn.on_drop(agent, world, reason); 725 | } 726 | } 727 | 728 | let mut app = TestApp::new(); 729 | 730 | let a = app.spawn_agent(); 731 | app.actions(a).add(DespawnAction::); 732 | 733 | assert!(app.get_entity(a).is_none()); 734 | assert_eq!( 735 | app.hooks().deref().clone(), 736 | vec![ 737 | Hook::Add(Name::Despawn, a), 738 | Hook::Start(Name::Despawn, a), 739 | Hook::Stop(Name::Despawn, None, StopReason::Finished), 740 | Hook::Remove(Name::Despawn, None), 741 | Hook::Drop(Name::Despawn, None, DropReason::Done) 742 | ] 743 | ); 744 | 745 | let a = app.reset().spawn_agent(); 746 | app.actions(a).add(DespawnAction::); 747 | 748 | assert!(app.get_entity(a).is_none()); 749 | assert_eq!( 750 | app.hooks().deref().clone(), 751 | vec![ 752 | Hook::Add(Name::Despawn, a), 753 | Hook::Start(Name::Despawn, a), 754 | Hook::Stop(Name::Despawn, None, StopReason::Canceled), 755 | Hook::Remove(Name::Despawn, None), 756 | Hook::Drop(Name::Despawn, None, DropReason::Done) 757 | ] 758 | ); 759 | 760 | let a = app.reset().spawn_agent(); 761 | app.actions(a).add(( 762 | DespawnAction::, 763 | CountdownAction::new(1), 764 | CountupAction::new(1), 765 | )); 766 | 767 | assert!(app.get_entity(a).is_none()); 768 | assert_eq!( 769 | app.hooks().deref().clone(), 770 | vec![ 771 | Hook::Add(Name::Despawn, a), 772 | Hook::Add(Name::Countdown, a), 773 | Hook::Add(Name::Countup, a), 774 | Hook::Start(Name::Despawn, a), 775 | // After despawn, the bevy ecs on_remove component hook is triggered 776 | Hook::Remove(Name::Countdown, None), 777 | Hook::Drop(Name::Countdown, None, DropReason::Cleared), 778 | Hook::Remove(Name::Countup, None), 779 | Hook::Drop(Name::Countup, None, DropReason::Cleared), 780 | // Back to DespawnAction 781 | Hook::Stop(Name::Despawn, None, StopReason::Finished), 782 | Hook::Remove(Name::Despawn, None), 783 | Hook::Drop(Name::Despawn, None, DropReason::Done) 784 | ] 785 | ); 786 | 787 | let a = app.reset().spawn_agent(); 788 | app.actions(a).add(( 789 | DespawnAction::, 790 | CountdownAction::new(1), 791 | CountupAction::new(1), 792 | )); 793 | 794 | assert!(app.get_entity(a).is_none()); 795 | assert_eq!( 796 | app.hooks().deref().clone(), 797 | vec![ 798 | Hook::Add(Name::Despawn, a), 799 | Hook::Add(Name::Countdown, a), 800 | Hook::Add(Name::Countup, a), 801 | Hook::Start(Name::Despawn, a), 802 | // After despawn, the bevy ecs on_remove component hook is triggered 803 | Hook::Remove(Name::Countdown, None), 804 | Hook::Drop(Name::Countdown, None, DropReason::Cleared), 805 | Hook::Remove(Name::Countup, None), 806 | Hook::Drop(Name::Countup, None, DropReason::Cleared), 807 | // Back to DespawnAction 808 | Hook::Stop(Name::Despawn, None, StopReason::Canceled), 809 | Hook::Remove(Name::Despawn, None), 810 | Hook::Drop(Name::Despawn, None, DropReason::Done) 811 | ] 812 | ); 813 | } 814 | 815 | #[test] 816 | fn good_add_action() { 817 | struct GoodAddAction; 818 | impl Action for GoodAddAction { 819 | fn is_finished(&self, _agent: Entity, _world: &World) -> bool { 820 | true 821 | } 822 | fn on_add(&mut self, agent: Entity, world: &mut World) { 823 | Name::GoodAdd.on_add(agent, world); 824 | } 825 | fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { 826 | Name::GoodAdd.on_start(agent, world); 827 | world 828 | .actions(agent) 829 | .start(false) 830 | .add(CountdownAction::new(1)); 831 | true 832 | } 833 | fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { 834 | Name::GoodAdd.on_stop(agent, world, reason); 835 | } 836 | fn on_remove(&mut self, agent: Option, world: &mut World) { 837 | Name::GoodAdd.on_remove(agent, world); 838 | } 839 | fn on_drop(self: Box, agent: Option, world: &mut World, reason: DropReason) { 840 | Name::GoodAdd.on_drop(agent, world, reason); 841 | } 842 | } 843 | 844 | let mut app = TestApp::new(); 845 | 846 | let a = app.spawn_agent(); 847 | app.actions(a).add(GoodAddAction); 848 | 849 | assert_eq!( 850 | app.hooks().deref().clone(), 851 | vec![ 852 | Hook::Add(Name::GoodAdd, a), 853 | Hook::Start(Name::GoodAdd, a), 854 | Hook::Add(Name::Countdown, a), 855 | Hook::Stop(Name::GoodAdd, Some(a), StopReason::Finished), 856 | Hook::Remove(Name::GoodAdd, Some(a)), 857 | Hook::Drop(Name::GoodAdd, Some(a), DropReason::Done), 858 | Hook::Start(Name::Countdown, a) 859 | ] 860 | ); 861 | } 862 | 863 | #[test] 864 | fn bad_add_action() { 865 | struct BadAddAction; 866 | impl Action for BadAddAction { 867 | fn is_finished(&self, _agent: Entity, _world: &World) -> bool { 868 | true 869 | } 870 | fn on_add(&mut self, agent: Entity, world: &mut World) { 871 | Name::BadAdd.on_add(agent, world); 872 | } 873 | fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { 874 | Name::BadAdd.on_start(agent, world); 875 | true 876 | } 877 | fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { 878 | Name::BadAdd.on_stop(agent, world, reason); 879 | world.actions(agent.unwrap()).add(CountdownAction::new(1)); 880 | } 881 | fn on_remove(&mut self, agent: Option, world: &mut World) { 882 | Name::BadAdd.on_remove(agent, world); 883 | } 884 | fn on_drop(self: Box, agent: Option, world: &mut World, reason: DropReason) { 885 | Name::BadAdd.on_drop(agent, world, reason); 886 | } 887 | } 888 | 889 | let mut app = TestApp::new(); 890 | 891 | let a = app.spawn_agent(); 892 | app.actions(a).add(BadAddAction); 893 | 894 | assert_eq!( 895 | app.hooks().deref().clone(), 896 | vec![ 897 | Hook::Add(Name::BadAdd, a), 898 | Hook::Start(Name::BadAdd, a), 899 | Hook::Stop(Name::BadAdd, Some(a), StopReason::Finished), 900 | Hook::Add(Name::Countdown, a), 901 | Hook::Start(Name::Countdown, a), 902 | Hook::Remove(Name::BadAdd, Some(a)), 903 | Hook::Drop(Name::BadAdd, Some(a), DropReason::Done) 904 | ] 905 | ); 906 | 907 | app.hooks_mut().clear(); 908 | app.update(); 909 | 910 | assert_eq!( 911 | app.hooks().deref().clone(), 912 | vec![ 913 | Hook::Stop(Name::Countdown, Some(a), StopReason::Finished), 914 | Hook::Remove(Name::Countdown, Some(a)), 915 | Hook::Drop(Name::Countdown, Some(a), DropReason::Done) 916 | ] 917 | ); 918 | } 919 | 920 | #[test] 921 | #[should_panic] 922 | #[cfg(debug_assertions)] 923 | fn forever_action() { 924 | struct ForeverAction; 925 | impl Action for ForeverAction { 926 | fn is_finished(&self, _agent: Entity, _world: &World) -> bool { 927 | true 928 | } 929 | fn on_start(&mut self, _agent: Entity, _world: &mut World) -> bool { 930 | true 931 | } 932 | fn on_stop(&mut self, _agent: Option, _world: &mut World, _reason: StopReason) {} 933 | fn on_drop(self: Box, agent: Option, world: &mut World, _reason: DropReason) { 934 | world 935 | .actions(agent.unwrap()) 936 | .start(false) 937 | .add(self as BoxedAction); 938 | } 939 | } 940 | 941 | let mut app = TestApp::new(); 942 | let a = app.spawn_agent(); 943 | app.actions(a).add(ForeverAction); 944 | } 945 | --------------------------------------------------------------------------------