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