bevy_sequential_actions/lib.rs
1#![warn(missing_docs, rustdoc::all)]
2
3/*!
4<div align="center">
5
6# Bevy Sequential Actions
7
8[](https://crates.io/crates/bevy-sequential-actions)
9[](https://github.com/hikikones/bevy-sequential-actions)
10[](https://github.com/hikikones/bevy-sequential-actions#license)
11
12A simple library for managing and sequencing various actions in [Bevy](https://bevyengine.org).
13
14<figure>
15 <img src="https://github.com/user-attachments/assets/66b5b15e-96af-47bd-9371-eee8809d1294"/>
16 <p><em>An entity with a queue of repeating actions</em></p>
17</figure>
18
19</div>
20
21## 📜 Getting Started
22
23#### Plugin
24
25The quickest way for getting started is adding the [`SequentialActionsPlugin`] to your [`App`].
26
27```rust,no_run
28# use bevy_ecs::prelude::*;
29# use bevy_app::prelude::*;
30use bevy_sequential_actions::*;
31#
32# struct DefaultPlugins;
33# impl Plugin for DefaultPlugins { fn build(&self, _app: &mut App) {} }
34
35fn main() {
36 App::new()
37 .add_plugins((DefaultPlugins, SequentialActionsPlugin))
38 .run();
39}
40```
41
42#### Implementing an Action
43
44An action is anything that implements the [`Action`] trait.
45The trait contains various methods that together defines the _lifecycle_ of an action.
46From this, you can create any action that can last as long as you like,
47and do as much as you like.
48
49An entity with actions is referred to as an `agent`.
50
51A simple wait action follows.
52
53```rust,no_run
54# use bevy_ecs::prelude::*;
55# use bevy_sequential_actions::*;
56#
57# struct Time;
58# impl Resource for Time {}
59# impl Time { fn delta_seconds(&self) -> f32 { 0.0 } }
60#
61pub struct WaitAction {
62 duration: f32, // Seconds
63 current: Option<f32>, // None
64}
65
66impl Action for WaitAction {
67 // By default, this method is called every frame in the Last schedule.
68 fn is_finished(&self, agent: Entity, world: &World) -> bool {
69 // Determine if wait timer has reached zero.
70 world.get::<WaitTimer>(agent).unwrap().0 <= 0.0
71 }
72
73 // This method is called when an action is started.
74 fn on_start(&mut self, agent: Entity, world: &mut World) -> bool {
75 // Take current time (if paused), or use full duration.
76 let duration = self.current.take().unwrap_or(self.duration);
77
78 // Run the wait timer system on the agent.
79 world.entity_mut(agent).insert(WaitTimer(duration));
80
81 // Is action already finished?
82 // Returning true here will immediately advance the action queue.
83 self.is_finished(agent, world)
84 }
85
86 // This method is called when an action is stopped.
87 fn on_stop(&mut self, agent: Option<Entity>, world: &mut World, reason: StopReason) {
88 // Do nothing if agent has been despawned.
89 let Some(agent) = agent else { return };
90
91 // Take the wait timer component from the agent.
92 let wait_timer = world.entity_mut(agent).take::<WaitTimer>();
93
94 // Store current time when paused.
95 if reason == StopReason::Paused {
96 self.current = Some(wait_timer.unwrap().0);
97 }
98 }
99
100 // Optional. This method is called when an action is added to the queue.
101 fn on_add(&mut self, agent: Entity, world: &mut World) {}
102
103 // Optional. This method is called when an action is removed from the queue.
104 fn on_remove(&mut self, agent: Option<Entity>, world: &mut World) {}
105
106 // Optional. The last method that is called with full ownership.
107 fn on_drop(self: Box<Self>, agent: Option<Entity>, world: &mut World, reason: DropReason) {}
108}
109
110#[derive(Component)]
111struct WaitTimer(f32);
112
113fn wait_system(mut wait_timer_q: Query<&mut WaitTimer>, time: Res<Time>) {
114 for mut wait_timer in &mut wait_timer_q {
115 wait_timer.0 -= time.delta_seconds();
116 }
117}
118```
119
120#### Modifying Actions
121
122Actions can be added to any [`Entity`] with the [`SequentialActions`] marker component.
123Adding and modifying actions is done through the [`actions(agent)`](ActionsProxy::actions)
124extension method implemented for both [`Commands`] and [`World`].
125See the [`ModifyActions`] trait for available methods.
126
127```rust,no_run
128# use bevy_app::AppExit;
129# use bevy_ecs::prelude::*;
130# use bevy_sequential_actions::*;
131#
132# struct EmptyAction;
133# impl Action for EmptyAction {
134# fn is_finished(&self, _a: Entity, _w: &World) -> bool { true }
135# fn on_start(&mut self, _a: Entity, _w: &mut World) -> bool { true }
136# fn on_stop(&mut self, _a: Option<Entity>, _w: &mut World, _r: StopReason) {}
137# }
138#
139fn setup(mut commands: Commands) {
140# let action_a = EmptyAction;
141# let action_b = EmptyAction;
142# let action_c = EmptyAction;
143# let action_d = EmptyAction;
144# let action_e = EmptyAction;
145# let action_f = EmptyAction;
146#
147 // Spawn entity with the marker component
148 let agent = commands.spawn(SequentialActions).id();
149 commands
150 .actions(agent)
151 // Add a single action
152 .add(action_a)
153 // Add more actions with a tuple
154 .add((action_b, action_c))
155 // Add a collection of actions
156 .add(actions![
157 action_d,
158 action_e,
159 action_f,
160 ])
161 // Add an anonymous action with a closure
162 .add(|_agent, world: &mut World| -> bool {
163 // on_start
164 world.send_event(AppExit::Success);
165 true
166 });
167}
168```
169
170#### ⚠️ Warning
171
172Since you are given a mutable [`World`], you can in practice do _anything_.
173Depending on what you do, the logic for advancing the action queue might not work properly.
174There are a few things you should keep in mind:
175
176* If you want to despawn an `agent` as an action, this should be done in [`on_start`](`Action::on_start`).
177* The [`execute`](`ModifyActions::execute`) and [`next`](`ModifyActions::next`) methods should not be used,
178 as that will immediately advance the action queue while inside any of the trait methods.
179 Instead, you should return `true` in [`on_start`](`Action::on_start`).
180* When adding new actions, you should set the [`start`](`ModifyActions::start`) property to `false`.
181 Otherwise, you will effectively call [`execute`](`ModifyActions::execute`) which, again, should not be used.
182 At worst, you will cause a **stack overflow** if the action adds itself.
183
184 ```rust,no_run
185 # use bevy_ecs::prelude::*;
186 # use bevy_sequential_actions::*;
187 # struct EmptyAction;
188 # impl Action for EmptyAction {
189 # fn is_finished(&self, _a: Entity, _w: &World) -> bool { true }
190 # fn on_start(&mut self, _a: Entity, _w: &mut World) -> bool { true }
191 # fn on_stop(&mut self, _a: Option<Entity>, _w: &mut World, _r: StopReason) {}
192 # }
193 # struct TestAction;
194 # impl Action for TestAction {
195 # fn is_finished(&self, _a: Entity, _w: &World) -> bool { true }
196 fn on_start(&mut self, agent: Entity, world: &mut World) -> bool {
197 # let action_a = EmptyAction;
198 # let action_b = EmptyAction;
199 # let action_c = EmptyAction;
200 world
201 .actions(agent)
202 .start(false) // Do not start next action
203 .add((action_a, action_b, action_c));
204
205 // Immediately advance the action queue
206 true
207 }
208 # fn on_stop(&mut self, _a: Option<Entity>, _w: &mut World, _r: StopReason) {}
209 # }
210 ```
211*/
212
213use std::{collections::VecDeque, fmt::Debug};
214
215use bevy_app::prelude::*;
216use bevy_derive::{Deref, DerefMut};
217use bevy_ecs::{component::ComponentId, prelude::*, query::QueryFilter, world::DeferredWorld};
218use bevy_log::{debug, warn};
219
220mod commands;
221mod macros;
222mod plugin;
223mod traits;
224mod world;
225
226pub use commands::*;
227pub use plugin::*;
228pub use traits::*;
229pub use world::*;
230
231/// A boxed [`Action`].
232pub type BoxedAction = Box<dyn Action>;
233
234/// Marker component for entities with actions.
235///
236/// This component is all that is needed for spawning an agent that you can add actions to.
237/// Required components will bring in the necessary components,
238/// namely [CurrentAction] and [ActionQueue].
239///
240/// If you do not care for the marker,
241/// or perhaps don't want to use required components,
242/// there is still the [ActionsBundle] for spawning an agent as before.
243#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Component)]
244#[require(CurrentAction, ActionQueue)]
245pub struct SequentialActions;
246
247/// The component bundle that all entities with actions must have.
248#[derive(Default, Bundle)]
249pub struct ActionsBundle {
250 current: CurrentAction,
251 queue: ActionQueue,
252}
253
254impl ActionsBundle {
255 /// Creates a new [`Bundle`] that contains the necessary components
256 /// that all entities with actions must have.
257 pub const fn new() -> Self {
258 Self {
259 current: CurrentAction(None),
260 queue: ActionQueue(VecDeque::new()),
261 }
262 }
263
264 /// Creates a new [`Bundle`] with specified `capacity` for the action queue.
265 pub fn with_capacity(capacity: usize) -> Self {
266 Self {
267 current: CurrentAction(None),
268 queue: ActionQueue(VecDeque::with_capacity(capacity)),
269 }
270 }
271}
272
273/// The current action for an `agent`.
274#[derive(Debug, Default, Component, Deref, DerefMut)]
275pub struct CurrentAction(Option<BoxedAction>);
276
277impl CurrentAction {
278 /// The [`on_remove`](bevy_ecs::component::ComponentHooks::on_remove) component lifecycle hook
279 /// used by [`SequentialActionsPlugin`] for cleaning up the current action when an `agent` is despawned.
280 pub fn on_remove_hook(mut world: DeferredWorld, agent: Entity, _component_id: ComponentId) {
281 let mut current_action = world.get_mut::<Self>(agent).unwrap();
282 if let Some(mut action) = current_action.take() {
283 world.commands().queue(move |world: &mut World| {
284 action.on_stop(None, world, StopReason::Canceled);
285 action.on_remove(None, world);
286 action.on_drop(None, world, DropReason::Done);
287 });
288 }
289 }
290
291 /// Observer for cleaning up the current action when an `agent` is despawned.
292 pub fn on_remove_trigger<F: QueryFilter>(
293 trigger: Trigger<OnRemove, Self>,
294 mut query: Query<&mut Self, F>,
295 mut commands: Commands,
296 ) {
297 let agent = trigger.entity();
298 if let Ok(mut current_action) = query.get_mut(agent) {
299 if let Some(mut action) = current_action.take() {
300 commands.queue(move |world: &mut World| {
301 action.on_stop(None, world, StopReason::Canceled);
302 action.on_remove(None, world);
303 action.on_drop(None, world, DropReason::Done);
304 });
305 }
306 }
307 }
308}
309
310/// The action queue for an `agent`.
311#[derive(Debug, Default, Component, Deref, DerefMut)]
312pub struct ActionQueue(VecDeque<BoxedAction>);
313
314impl ActionQueue {
315 /// The [`on_remove`](bevy_ecs::component::ComponentHooks::on_remove) component lifecycle hook
316 /// used by [`SequentialActionsPlugin`] for cleaning up the action queue when an `agent` is despawned.
317 pub fn on_remove_hook(mut world: DeferredWorld, agent: Entity, _component_id: ComponentId) {
318 let mut action_queue = world.get_mut::<Self>(agent).unwrap();
319 if !action_queue.is_empty() {
320 let actions = std::mem::take(&mut action_queue.0);
321 world.commands().queue(move |world: &mut World| {
322 for mut action in actions {
323 action.on_remove(None, world);
324 action.on_drop(None, world, DropReason::Cleared);
325 }
326 });
327 }
328 }
329
330 /// Observer for cleaning up the action queue when an `agent` is despawned.
331 pub fn on_remove_trigger<F: QueryFilter>(
332 trigger: Trigger<OnRemove, Self>,
333 mut query: Query<&mut Self, F>,
334 mut commands: Commands,
335 ) {
336 let agent = trigger.entity();
337 if let Ok(mut action_queue) = query.get_mut(agent) {
338 if !action_queue.is_empty() {
339 let actions = std::mem::take(&mut action_queue.0);
340 commands.queue(move |world: &mut World| {
341 for mut action in actions {
342 action.on_remove(None, world);
343 action.on_drop(None, world, DropReason::Cleared);
344 }
345 });
346 }
347 }
348 }
349}
350
351/// Configuration for actions to be added.
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub struct AddConfig {
354 /// Start the next action in the queue if nothing is currently running.
355 pub start: bool,
356 /// The queue order for actions to be added.
357 pub order: AddOrder,
358}
359
360impl AddConfig {
361 /// Returns a new configuration for actions to be added.
362 pub const fn new(start: bool, order: AddOrder) -> Self {
363 Self { start, order }
364 }
365}
366
367impl Default for AddConfig {
368 fn default() -> Self {
369 Self::new(true, AddOrder::Back)
370 }
371}
372
373/// The queue order for actions to be added.
374#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
375pub enum AddOrder {
376 /// An action is added to the back of the queue.
377 #[default]
378 Back,
379 /// An action is added to the front of the queue.
380 Front,
381}
382
383/// The reason why an [`Action`] was stopped.
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385pub enum StopReason {
386 /// The action was finished.
387 Finished,
388 /// The action was canceled.
389 Canceled,
390 /// The action was paused.
391 Paused,
392}
393
394/// The reason why an [`Action`] was dropped.
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub enum DropReason {
397 /// The action is considered done as it was either finished or canceled
398 /// without being skipped or cleared from the action queue.
399 Done,
400 /// The action was skipped. This happens either deliberately,
401 /// or because an action was added to an `agent` that does not exist or is missing a component.
402 Skipped,
403 /// The action queue was cleared. This happens either deliberately,
404 /// or because an `agent` was despawned.
405 Cleared,
406}