Skip to main content

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}