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# 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![action_d, action_e, action_f])
157        // Add an anonymous action with a closure
158        .add(|_agent, world: &mut World| -> bool {
159            // on_start
160            world.send_event(AppExit::Success);
161            true
162        });
163}
164```
165
166#### ⚠️ Warning
167
168Since you are given a mutable [`World`], you can in practice do _anything_.
169Depending on what you do, the logic for advancing the action queue might not work properly.
170There are a few things you should keep in mind:
171
172* If you want to despawn an `agent` as an action, this should be done in [`on_start`](`Action::on_start`).
173* The [`execute`](`ModifyActions::execute`) and [`next`](`ModifyActions::next`) methods should not be used,
174    as that will immediately advance the action queue while inside any of the trait methods.
175    Instead, you should return `true` in [`on_start`](`Action::on_start`).
176* When adding new actions, you should set the [`start`](`ModifyActions::start`) property to `false`.
177    Otherwise, you will effectively call [`execute`](`ModifyActions::execute`) which, again, should not be used.
178    At worst, you will cause a **stack overflow** if the action adds itself.
179
180    ```rust,no_run
181    # use bevy_ecs::prelude::*;
182    # use bevy_sequential_actions::*;
183    # struct EmptyAction;
184    # impl Action for EmptyAction {
185    #   fn is_finished(&self, _a: Entity, _w: &World) -> bool { true }
186    #   fn on_start(&mut self, _a: Entity, _w: &mut World) -> bool { true }
187    #   fn on_stop(&mut self, _a: Option<Entity>, _w: &mut World, _r: StopReason) {}
188    # }
189    # struct TestAction;
190    # impl Action for TestAction {
191    #   fn is_finished(&self, _a: Entity, _w: &World) -> bool { true }
192        fn on_start(&mut self, agent: Entity, world: &mut World) -> bool {
193    #       let action_a = EmptyAction;
194    #       let action_b = EmptyAction;
195    #       let action_c = EmptyAction;
196            world
197                .actions(agent)
198                .start(false) // Do not start next action
199                .add((action_a, action_b, action_c));
200
201            // Immediately advance the action queue
202            true
203        }
204    #   fn on_stop(&mut self, _a: Option<Entity>, _w: &mut World, _r: StopReason) {}
205    # }
206    ```
207*/
208
209use std::{collections::VecDeque, fmt::Debug};
210
211use bevy_app::prelude::*;
212use bevy_derive::{Deref, DerefMut};
213use bevy_ecs::{component::HookContext, prelude::*, query::QueryFilter, world::DeferredWorld};
214use bevy_log::{debug, warn};
215
216mod commands;
217mod macros;
218mod plugin;
219mod traits;
220mod world;
221
222pub use commands::*;
223pub use plugin::*;
224pub use traits::*;
225pub use world::*;
226
227/// A boxed [`Action`].
228pub type BoxedAction = Box<dyn Action>;
229
230/// Marker component for entities with actions.
231///
232/// This component is all that is needed for spawning an agent that you can add actions to.
233/// Required components will bring in the necessary components,
234/// namely [CurrentAction] and [ActionQueue].
235///
236/// If you do not care for the marker,
237/// or perhaps don't want to use required components,
238/// there is still the [ActionsBundle] for spawning an agent as before.
239#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Component)]
240#[require(CurrentAction, ActionQueue)]
241pub struct SequentialActions;
242
243/// The component bundle that all entities with actions must have.
244#[derive(Default, Bundle)]
245pub struct ActionsBundle {
246    current: CurrentAction,
247    queue: ActionQueue,
248}
249
250impl ActionsBundle {
251    /// Creates a new [`Bundle`] that contains the necessary components
252    /// that all entities with actions must have.
253    pub const fn new() -> Self {
254        Self {
255            current: CurrentAction(None),
256            queue: ActionQueue(VecDeque::new()),
257        }
258    }
259
260    /// Creates a new [`Bundle`] with specified `capacity` for the action queue.
261    pub fn with_capacity(capacity: usize) -> Self {
262        Self {
263            current: CurrentAction(None),
264            queue: ActionQueue(VecDeque::with_capacity(capacity)),
265        }
266    }
267}
268
269/// The current action for an `agent`.
270#[derive(Debug, Default, Component, Deref, DerefMut)]
271pub struct CurrentAction(Option<BoxedAction>);
272
273impl CurrentAction {
274    /// The [`on_remove`](bevy_ecs::component::ComponentHooks::on_remove) component lifecycle hook
275    /// used by [`SequentialActionsPlugin`] for cleaning up the current action when an `agent` is despawned.
276    pub fn on_remove_hook(mut world: DeferredWorld, ctx: HookContext) {
277        let agent = ctx.entity;
278        let mut current_action = world.get_mut::<Self>(agent).unwrap();
279        if let Some(mut action) = current_action.take() {
280            world.commands().queue(move |world: &mut World| {
281                action.on_stop(None, world, StopReason::Canceled);
282                action.on_remove(None, world);
283                action.on_drop(None, world, DropReason::Done);
284            });
285        }
286    }
287
288    /// Observer for cleaning up the current action when an `agent` is despawned.
289    pub fn on_remove_trigger<F: QueryFilter>(
290        trigger: Trigger<OnRemove, Self>,
291        mut query: Query<&mut Self, F>,
292        mut commands: Commands,
293    ) {
294        let agent = trigger.target();
295        if let Ok(mut current_action) = query.get_mut(agent) {
296            if let Some(mut action) = current_action.take() {
297                commands.queue(move |world: &mut World| {
298                    action.on_stop(None, world, StopReason::Canceled);
299                    action.on_remove(None, world);
300                    action.on_drop(None, world, DropReason::Done);
301                });
302            }
303        }
304    }
305}
306
307/// The action queue for an `agent`.
308#[derive(Debug, Default, Component, Deref, DerefMut)]
309pub struct ActionQueue(VecDeque<BoxedAction>);
310
311impl ActionQueue {
312    /// The [`on_remove`](bevy_ecs::component::ComponentHooks::on_remove) component lifecycle hook
313    /// used by [`SequentialActionsPlugin`] for cleaning up the action queue when an `agent` is despawned.
314    pub fn on_remove_hook(mut world: DeferredWorld, ctx: HookContext) {
315        let agent = ctx.entity;
316        let mut action_queue = world.get_mut::<Self>(agent).unwrap();
317        if !action_queue.is_empty() {
318            let actions = std::mem::take(&mut action_queue.0);
319            world.commands().queue(move |world: &mut World| {
320                for mut action in actions {
321                    action.on_remove(None, world);
322                    action.on_drop(None, world, DropReason::Cleared);
323                }
324            });
325        }
326    }
327
328    /// Observer for cleaning up the action queue when an `agent` is despawned.
329    pub fn on_remove_trigger<F: QueryFilter>(
330        trigger: Trigger<OnRemove, Self>,
331        mut query: Query<&mut Self, F>,
332        mut commands: Commands,
333    ) {
334        let agent = trigger.target();
335        if let Ok(mut action_queue) = query.get_mut(agent) {
336            if !action_queue.is_empty() {
337                let actions = std::mem::take(&mut action_queue.0);
338                commands.queue(move |world: &mut World| {
339                    for mut action in actions {
340                        action.on_remove(None, world);
341                        action.on_drop(None, world, DropReason::Cleared);
342                    }
343                });
344            }
345        }
346    }
347}
348
349/// Configuration for actions to be added.
350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
351pub struct AddConfig {
352    /// Start the next action in the queue if nothing is currently running.
353    pub start: bool,
354    /// The queue order for actions to be added.
355    pub order: AddOrder,
356}
357
358impl AddConfig {
359    /// Returns a new configuration for actions to be added.
360    pub const fn new(start: bool, order: AddOrder) -> Self {
361        Self { start, order }
362    }
363}
364
365impl Default for AddConfig {
366    fn default() -> Self {
367        Self::new(true, AddOrder::Back)
368    }
369}
370
371/// The queue order for actions to be added.
372#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
373pub enum AddOrder {
374    /// An action is added to the back of the queue.
375    #[default]
376    Back,
377    /// An action is added to the front of the queue.
378    Front,
379}
380
381/// The reason why an [`Action`] was stopped.
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
383pub enum StopReason {
384    /// The action was finished.
385    Finished,
386    /// The action was canceled.
387    Canceled,
388    /// The action was paused.
389    Paused,
390}
391
392/// The reason why an [`Action`] was dropped.
393#[derive(Debug, Clone, Copy, PartialEq, Eq)]
394pub enum DropReason {
395    /// The action is considered done as it was either finished or canceled
396    /// without being skipped or cleared from the action queue.
397    Done,
398    /// The action was skipped. This happens either deliberately,
399    /// or because an action was added to an `agent` that does not exist or is missing a component.
400    Skipped,
401    /// The action queue was cleared. This happens either deliberately,
402    /// or because an `agent` was despawned.
403    Cleared,
404}