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![
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}