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# #[derive(Component)]
58# struct Time;
59# impl Resource for Time {}
60# impl Time { fn delta_secs(&self) -> f32 { 0.0 } }
61#
62pub struct WaitAction {
63 duration: f32, // Seconds
64 remaining: Option<f32>, // None
65}
66
67impl Action for WaitAction {
68 // By default, this method is called every frame in the Last schedule.
69 fn is_finished(&self, agent: Entity, world: &World) -> bool {
70 // Determine if wait timer has reached zero.
71 world.get::<WaitTimer>(agent).unwrap().0 <= 0.0
72 }
73
74 // This method is called when an action is started.
75 fn on_start(&mut self, agent: Entity, world: &mut World) -> bool {
76 // Take remaining time (if paused), or use full duration.
77 let duration = self.remaining.take().unwrap_or(self.duration);
78
79 // Run the wait timer system on the agent.
80 world.entity_mut(agent).insert(WaitTimer(duration));
81
82 // Is action already finished?
83 // Returning true here will immediately advance the action queue.
84 self.is_finished(agent, world)
85 }
86
87 // This method is called when an action is stopped.
88 fn on_stop(&mut self, agent: Option<Entity>, world: &mut World, reason: StopReason) {
89 // Do nothing if agent has been despawned.
90 let Some(agent) = agent else { return };
91
92 // Take the wait timer component from the agent.
93 let wait_timer = world.entity_mut(agent).take::<WaitTimer>();
94
95 // Store remaining time when paused.
96 if reason == StopReason::Paused {
97 self.remaining = Some(wait_timer.unwrap().0);
98 }
99 }
100
101 // Optional. This method is called when an action is added to the queue.
102 fn on_add(&mut self, agent: Entity, world: &mut World) {}
103
104 // Optional. This method is called when an action is removed from the queue.
105 fn on_remove(&mut self, agent: Option<Entity>, world: &mut World) {}
106
107 // Optional. The last method that is called with full ownership.
108 fn on_drop(self: Box<Self>, agent: Option<Entity>, world: &mut World, reason: DropReason) {}
109}
110
111#[derive(Component)]
112struct WaitTimer(f32);
113
114fn wait_system(mut wait_timer_q: Query<&mut WaitTimer>, time: Res<Time>) {
115 for mut wait_timer in &mut wait_timer_q {
116 wait_timer.0 -= time.delta_secs();
117 }
118}
119```
120
121#### Managing Actions
122
123Actions can be added to any [`Entity`] with the [`SequentialActions`] marker component.
124Adding and managing actions is done through the [`actions(agent)`](ActionsProxy::actions)
125extension method implemented for both [`Commands`] and [`World`].
126See the [`ManageActions`] trait for available methods.
127
128```rust,no_run
129# use bevy_app::AppExit;
130# use bevy_ecs::prelude::*;
131# use bevy_sequential_actions::*;
132#
133# struct EmptyAction;
134# impl Action for EmptyAction {
135# fn is_finished(&self, _a: Entity, _w: &World) -> bool { true }
136# fn on_start(&mut self, _a: Entity, _w: &mut World) -> bool { true }
137# fn on_stop(&mut self, _a: Option<Entity>, _w: &mut World, _r: StopReason) {}
138# }
139#
140fn setup(mut commands: Commands) {
141# let action_a = EmptyAction;
142# let action_b = EmptyAction;
143# let action_c = EmptyAction;
144# let action_d = EmptyAction;
145# let action_e = EmptyAction;
146# let action_f = EmptyAction;
147#
148 // Spawn entity with the marker component
149 let agent = commands.spawn(SequentialActions).id();
150 commands
151 .actions(agent)
152 // Add a single action
153 .add(action_a)
154 // Add more actions with a tuple
155 .add((action_b, action_c))
156 // Add a collection of actions
157 .add(actions![action_d, action_e, action_f])
158 // Add an anonymous action with a closure
159 .add(|_agent, world: &mut World| -> bool {
160 // on_start
161 world.write_message(AppExit::Success);
162 true
163 });
164}
165```
166
167#### ⚠️ Warning
168
169Since you are given a mutable [`World`], you can in practice do _anything_.
170Depending on what you do, the logic for advancing the action queue might not work properly.
171There are a few things you should keep in mind:
172
173* If you want to despawn an `agent` as an action, this should be done in [`on_start`](`Action::on_start`).
174* The [`execute`](`ManageActions::execute`) and [`next`](`ManageActions::next`) methods should not be used,
175 as that will immediately advance the action queue while inside any of the trait methods.
176 Instead, you should return `true` in [`on_start`](`Action::on_start`).
177* When adding new actions, you should set the [`start`](`ManageActions::start`) property to `false`.
178 Otherwise, you will effectively call [`execute`](`ManageActions::execute`) which, again, should not be used.
179 At worst, you will cause a **stack overflow** if the action adds itself.
180
181 ```rust,no_run
182 # use bevy_ecs::prelude::*;
183 # use bevy_sequential_actions::*;
184 # struct EmptyAction;
185 # impl Action for EmptyAction {
186 # fn is_finished(&self, _a: Entity, _w: &World) -> bool { true }
187 # fn on_start(&mut self, _a: Entity, _w: &mut World) -> bool { true }
188 # fn on_stop(&mut self, _a: Option<Entity>, _w: &mut World, _r: StopReason) {}
189 # }
190 # struct TestAction;
191 # impl Action for TestAction {
192 # fn is_finished(&self, _a: Entity, _w: &World) -> bool { true }
193 fn on_start(&mut self, agent: Entity, world: &mut World) -> bool {
194 # let action_a = EmptyAction;
195 # let action_b = EmptyAction;
196 # let action_c = EmptyAction;
197 world
198 .actions(agent)
199 .start(false) // Do not start next action
200 .add((action_a, action_b, action_c));
201
202 // Immediately advance the action queue
203 true
204 }
205 # fn on_stop(&mut self, _a: Option<Entity>, _w: &mut World, _r: StopReason) {}
206 # }
207 ```
208* You should not add new actions when the queue is being cleared or some actions are skipped.
209 In order to reuse the allocated queue, actions are removed one by one until the queue is empty.
210 Since you can add new actions to the queue between each removal, you can in practice do this forever.
211
212 ```rust,no_run
213 # use bevy_ecs::prelude::*;
214 # use bevy_sequential_actions::*;
215 # struct EmptyAction;
216 # impl Action for EmptyAction {
217 # fn is_finished(&self, _a: Entity, _w: &World) -> bool { true }
218 # fn on_start(&mut self, _a: Entity, _w: &mut World) -> bool { true }
219 # fn on_stop(&mut self, _a: Option<Entity>, _w: &mut World, _r: StopReason) {}
220 # }
221 # struct TestAction;
222 # impl Action for TestAction {
223 # fn is_finished(&self, _a: Entity, _w: &World) -> bool { true }
224 # fn on_start(&mut self, _a: Entity, _w: &mut World) -> bool { true }
225 # fn on_stop(&mut self, _a: Option<Entity>, _w: &mut World, _r: StopReason) {}
226 fn on_drop(self: Box<Self>, agent: Option<Entity>, world: &mut World, reason: DropReason) {
227 match reason {
228 DropReason::Done => {
229 // ...
230 }
231 DropReason::Skipped => {
232 // New actions added to the front of the queue here
233 // will also be skipped if more skips follow
234 }
235 DropReason::Cleared => {
236 // All new actions added to the queue here
237 // will also be cleared
238 }
239 }
240 }
241 # }
242 ```
243*/
244
245use std::{collections::VecDeque, fmt::Debug};
246
247use bevy_app::prelude::*;
248use bevy_derive::{Deref, DerefMut};
249use bevy_ecs::{lifecycle::HookContext, prelude::*, query::QueryFilter, world::DeferredWorld};
250use bevy_log::{debug, warn};
251
252mod commands;
253mod macros;
254mod plugin;
255mod traits;
256mod world;
257
258pub use commands::*;
259pub use plugin::*;
260pub use traits::*;
261pub use world::*;
262
263/// A boxed [`Action`].
264pub type BoxedAction = Box<dyn Action>;
265
266/// Marker component for entities with actions.
267///
268/// This component is all that is needed for spawning an agent that you can add actions to.
269/// Required components will bring in the necessary components,
270/// namely [CurrentAction] and [ActionQueue].
271///
272/// If you do not care for the marker,
273/// or perhaps don't want to use required components,
274/// there is still the [ActionsBundle] for spawning an agent as before.
275#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Component)]
276#[require(CurrentAction, ActionQueue)]
277pub struct SequentialActions;
278
279/// The component bundle that all entities with actions must have.
280#[derive(Default, Bundle)]
281pub struct ActionsBundle {
282 current: CurrentAction,
283 queue: ActionQueue,
284}
285
286impl ActionsBundle {
287 /// Creates a new [`Bundle`] that contains the necessary components
288 /// that all entities with actions must have.
289 pub const fn new() -> Self {
290 Self {
291 current: CurrentAction(None),
292 queue: ActionQueue(VecDeque::new()),
293 }
294 }
295
296 /// Creates a new [`Bundle`] with specified `capacity` for the action queue.
297 pub fn with_capacity(capacity: usize) -> Self {
298 Self {
299 current: CurrentAction(None),
300 queue: ActionQueue(VecDeque::with_capacity(capacity)),
301 }
302 }
303}
304
305/// The current action for an `agent`.
306#[derive(Debug, Default, Component, Deref, DerefMut)]
307pub struct CurrentAction(Option<BoxedAction>);
308
309impl CurrentAction {
310 /// The [`on_remove`](bevy_ecs::lifecycle::ComponentHooks::on_remove) component lifecycle hook
311 /// used by [`SequentialActionsPlugin`] for cleaning up the current action when an `agent` is despawned.
312 pub fn on_remove_hook(mut world: DeferredWorld, ctx: HookContext) {
313 let agent = ctx.entity;
314 let mut current_action = world.get_mut::<Self>(agent).unwrap();
315 if let Some(mut action) = current_action.take() {
316 world.commands().queue(move |world: &mut World| {
317 action.on_stop(None, world, StopReason::Canceled);
318 action.on_remove(None, world);
319 action.on_drop(None, world, DropReason::Done);
320 });
321 }
322 }
323
324 /// [`Observer`] for cleaning up the current action when an `agent` is despawned.
325 pub fn on_remove_trigger<F: QueryFilter>(
326 remove: On<Remove, Self>,
327 mut query: Query<&mut Self, F>,
328 mut commands: Commands,
329 ) {
330 let agent = remove.entity;
331 if let Ok(mut current_action) = query.get_mut(agent)
332 && let Some(mut action) = current_action.take()
333 {
334 commands.queue(move |world: &mut World| {
335 action.on_stop(None, world, StopReason::Canceled);
336 action.on_remove(None, world);
337 action.on_drop(None, world, DropReason::Done);
338 });
339 }
340 }
341}
342
343/// The action queue for an `agent`.
344#[derive(Debug, Default, Component, Deref, DerefMut)]
345pub struct ActionQueue(VecDeque<BoxedAction>);
346
347impl ActionQueue {
348 /// The [`on_remove`](bevy_ecs::lifecycle::ComponentHooks::on_remove) component lifecycle hook
349 /// used by [`SequentialActionsPlugin`] for cleaning up the action queue when an `agent` is despawned.
350 pub fn on_remove_hook(mut world: DeferredWorld, ctx: HookContext) {
351 let agent = ctx.entity;
352 let mut action_queue = world.get_mut::<Self>(agent).unwrap();
353 if !action_queue.is_empty() {
354 let actions = std::mem::take(&mut action_queue.0);
355 world.commands().queue(move |world: &mut World| {
356 for mut action in actions {
357 action.on_remove(None, world);
358 action.on_drop(None, world, DropReason::Cleared);
359 }
360 });
361 }
362 }
363
364 /// [`Observer`] for cleaning up the action queue when an `agent` is despawned.
365 pub fn on_remove_trigger<F: QueryFilter>(
366 remove: On<Remove, Self>,
367 mut query: Query<&mut Self, F>,
368 mut commands: Commands,
369 ) {
370 let agent = remove.entity;
371 if let Ok(mut action_queue) = query.get_mut(agent)
372 && !action_queue.is_empty()
373 {
374 let actions = std::mem::take(&mut action_queue.0);
375 commands.queue(move |world: &mut World| {
376 for mut action in actions {
377 action.on_remove(None, world);
378 action.on_drop(None, world, DropReason::Cleared);
379 }
380 });
381 }
382 }
383}
384
385/// Configuration for actions to be added.
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub struct AddConfig {
388 /// Start the next action in the queue if nothing is currently running.
389 pub start: bool,
390 /// The queue order for actions to be added.
391 pub order: AddOrder,
392}
393
394impl AddConfig {
395 /// Returns a new configuration for actions to be added.
396 pub const fn new(start: bool, order: AddOrder) -> Self {
397 Self { start, order }
398 }
399}
400
401impl Default for AddConfig {
402 fn default() -> Self {
403 Self::new(true, AddOrder::Back)
404 }
405}
406
407/// The queue order for actions to be added.
408#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
409pub enum AddOrder {
410 /// An action is added to the back of the queue.
411 #[default]
412 Back,
413 /// An action is added to the front of the queue.
414 Front,
415}
416
417/// The reason why an [`Action`] was stopped.
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
419pub enum StopReason {
420 /// The action was finished.
421 Finished,
422 /// The action was canceled.
423 Canceled,
424 /// The action was paused.
425 Paused,
426}
427
428/// The reason why an [`Action`] was dropped.
429#[derive(Debug, Clone, Copy, PartialEq, Eq)]
430pub enum DropReason {
431 /// The action is considered done as it was either finished or canceled
432 /// without being skipped or cleared from the action queue.
433 Done,
434 /// The action was skipped. This happens either deliberately,
435 /// or because an action was added to an `agent` that does not exist or is missing a component.
436 Skipped,
437 /// The action queue was cleared. This happens either deliberately,
438 /// or because an `agent` was despawned.
439 Cleared,
440}