bevy_time/delayed_commands.rs
1use alloc::vec::Vec;
2use bevy_ecs::{prelude::*, system::command::spawn_batch, world::CommandQueue};
3use bevy_platform::collections::HashMap;
4#[cfg(feature = "bevy_reflect")]
5use bevy_reflect::Reflect;
6use core::time::Duration;
7
8use crate::Time;
9
10/// A wrapper over [`Commands`] that stores [`CommandQueue`]s to be applied with given delays.
11///
12/// When dropped, the queues are spawned into the world as new entities with
13/// [`DelayedCommandQueue`] components, and then checked by the
14/// [`check_delayed_command_queues`] system.
15pub struct DelayedCommands<'w, 's> {
16 /// Used to own queues and deduplicate them by their duration.
17 queues: HashMap<Duration, CommandQueue>,
18
19 /// The wrapped `Commands` - used to provision out new `Commands`
20 /// and to spawn the queues as entities when the struct is dropped.
21 commands: Commands<'w, 's>,
22}
23
24impl<'w, 's> DelayedCommands<'w, 's> {
25 /// Return a [`Commands`] whose commands will be delayed by `duration`.
26 #[must_use = "The returned Commands must be used to submit commands with this delay."]
27 pub fn duration(&mut self, duration: Duration) -> Commands<'w, '_> {
28 // Fetch a queue with the given duration or create one
29 let queue = self.queues.entry(duration).or_default();
30 // Return a new `Commands` to write commands to the queue
31 self.commands.rebound_to(queue)
32 }
33
34 /// Return a [`Commands`] whose commands will be delayed by `secs` seconds.
35 #[inline]
36 #[must_use = "The returned Commands must be used to submit commands with this delay."]
37 pub fn secs(&mut self, secs: f32) -> Commands<'w, '_> {
38 self.duration(Duration::from_secs_f32(secs))
39 }
40
41 /// Drains and spawns the contained command queues as [`DelayedCommandQueue`] entities.
42 fn submit(&mut self) {
43 let mut queues = self
44 .queues
45 .drain()
46 .map(|(submit_at, queue)| DelayedCommandQueue { submit_at, queue })
47 .collect::<Vec<_>>();
48
49 self.commands.queue(move |world: &mut World| {
50 // We use the default Time<()> here intentionally to support custom clocks
51 let time = world.resource::<Time>();
52 let elapsed = time.elapsed();
53 for queue in queues.iter_mut() {
54 // Turn relative delays into absolute elapsed times
55 queue.submit_at += elapsed;
56 }
57 spawn_batch(queues).apply(world);
58 });
59 }
60}
61
62/// Extension trait for [`Commands`] that provides delayed command functionality.
63pub trait DelayedCommandsExt<'w> {
64 /// Returns a [`DelayedCommands`] instance that can be used to queue
65 /// commands to be submitted at a later point in time.
66 ///
67 /// When dropped, the [`DelayedCommands`] submits spawn commands that will
68 /// spawn [`DelayedCommandQueue`] entities. The entities are checked
69 /// by the [`check_delayed_command_queues`] system, and their queues are
70 /// submitted when the specified time has elapsed.
71 ///
72 /// # Usage
73 ///
74 /// ```
75 /// # use bevy_ecs::prelude::*;
76 /// # use bevy_time::DelayedCommandsExt;
77 /// fn my_system(mut commands: Commands) {
78 /// // Spawn an entity after one second
79 /// commands.delayed().secs(1.0).spawn_empty();
80 /// }
81 /// # bevy_ecs::system::assert_is_system(my_system);
82 /// ```
83 ///
84 /// Entity allocation happens immediately even if the spawn command is delayed.
85 /// This allows you to queue delayed commands on an entity that hasn't been spawned yet.
86 ///
87 /// ```
88 /// # use bevy_ecs::prelude::*;
89 /// # use bevy_time::DelayedCommandsExt;
90 /// fn my_system(mut commands: Commands) {
91 /// let mut delayed = commands.delayed();
92 /// // spawn an entity after 1 second, then despawn it a second later
93 /// let entity = delayed.secs(1.0).spawn_empty().id();
94 /// delayed.secs(2.0).entity(entity).despawn();
95 /// }
96 /// # bevy_ecs::system::assert_is_system(my_system);
97 /// ```
98 ///
99 /// # Timing
100 ///
101 /// Delayed commands are currently checked against the default clock in the [`PreUpdate`]
102 /// schedule. There's currently no way to specify different clocks for different
103 /// delayed commands - this is a limitation of the system and if you need this behavior
104 /// you'll likely have to implement your own delay system.
105 ///
106 /// [`PreUpdate`]: bevy_app::PreUpdate
107 fn delayed(&mut self) -> DelayedCommands<'w, '_>;
108}
109
110impl<'w, 's> DelayedCommandsExt<'w> for Commands<'w, 's> {
111 fn delayed(&mut self) -> DelayedCommands<'w, '_> {
112 DelayedCommands {
113 commands: self.reborrow(),
114 queues: HashMap::default(),
115 }
116 }
117}
118
119impl<'w, 's> Drop for DelayedCommands<'w, 's> {
120 fn drop(&mut self) {
121 self.submit();
122 }
123}
124
125/// A component with a [`CommandQueue`] to be submitted later.
126///
127/// Queues in these components are checked automatically by the
128/// [`check_delayed_command_queues`] added by [`TimePlugin`] and submitted when
129/// the default clock's elapsed time exceeds `submit_at`.
130///
131/// [`TimePlugin`]: crate::TimePlugin
132#[derive(Component)]
133#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]
134pub struct DelayedCommandQueue {
135 /// The elapsed time from startup when `queue` should be submitted.
136 pub submit_at: Duration,
137
138 /// The queue to be submitted when time is up.
139 #[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
140 pub queue: CommandQueue,
141}
142
143/// The system used to check [`DelayedCommandQueue`]s, which are usually spawned
144/// by [`DelayedCommands`]. When the elapsed time exceeds a queue's `submit_at` time,
145/// the contained `queue` is appended to the system's [`Commands`].
146pub fn check_delayed_command_queues(
147 queues: Query<(Entity, &mut DelayedCommandQueue)>,
148 time: Res<Time>,
149 mut commands: Commands,
150) {
151 let elapsed = time.elapsed();
152 for (e, mut queue) in queues {
153 if queue.submit_at <= elapsed {
154 // Write the contained delayed commands to the world.
155 commands.append(&mut queue.queue);
156 commands.entity(e).despawn();
157 }
158 }
159}
160
161#[cfg(test)]
162#[expect(clippy::print_stdout, reason = "Allowed in tests.")]
163mod tests {
164 use core::time::Duration;
165 use std::println;
166
167 use bevy_app::{App, Startup};
168 use bevy_ecs::{component::Component, system::Commands};
169
170 use crate::{DelayedCommandsExt, TimePlugin, TimeUpdateStrategy};
171
172 #[derive(Component)]
173 struct DummyComponent;
174
175 #[test]
176 fn delayed_queues_should_run_with_time_plugin_enabled() {
177 fn queue_commands(mut commands: Commands) {
178 commands.delayed().secs(0.1).spawn(DummyComponent);
179
180 commands.spawn(DummyComponent);
181
182 let mut delayed_cmds = commands.delayed();
183 delayed_cmds.secs(0.5).spawn(DummyComponent);
184
185 let mut in_1_sec = delayed_cmds.duration(Duration::from_secs_f32(1.0));
186 in_1_sec.spawn(DummyComponent);
187 in_1_sec.spawn(DummyComponent);
188 in_1_sec.spawn(DummyComponent);
189 }
190
191 let mut app = App::new();
192 app.add_plugins(TimePlugin)
193 .add_systems(Startup, queue_commands)
194 .insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f32(
195 0.2,
196 )));
197
198 for frame in 0..10 {
199 app.update();
200 let dummy_count = app
201 .world_mut()
202 .query::<&DummyComponent>()
203 .iter(app.world())
204 .count();
205
206 println!("Frame {frame}, {dummy_count} dummies spawned");
207
208 match frame {
209 0 => {
210 assert_eq!(dummy_count, 1);
211 }
212 1 | 2 => {
213 assert_eq!(dummy_count, 2);
214 }
215 3 | 4 => {
216 assert_eq!(dummy_count, 3);
217 }
218 _ => {
219 assert_eq!(dummy_count, 6);
220 }
221 }
222 }
223 }
224}