Skip to main content

bevy_sequential_actions/
lib.rs

1#![warn(missing_docs, rustdoc::all)]
2
3/*!
4<div align="center">
5
6# Bevy Sequential Actions
7
8[![crates.io](https://img.shields.io/crates/v/bevy-sequential-actions?style=flat-square)](https://crates.io/crates/bevy-sequential-actions)
9[![github.com](https://img.shields.io/github/stars/hikikones/bevy-sequential-actions?logo=github&style=flat-square)](https://github.com/hikikones/bevy-sequential-actions)
10[![MIT/Apache 2.0](https://img.shields.io/crates/l/bevy-sequential-actions?style=flat-square)](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}