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(impl bevy_ecs::component::Component for DelayedCommandQueue where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {
    const STORAGE_TYPE: bevy_ecs::component::StorageType =
        bevy_ecs::component::StorageType::Table;
    type Mutability = bevy_ecs::component::Mutable;
    fn register_required_components(_requiree:
            bevy_ecs::component::ComponentId,
        required_components:
            &mut bevy_ecs::component::RequiredComponentsRegistrator) {}
    fn clone_behavior() -> bevy_ecs::component::ComponentCloneBehavior {
        use bevy_ecs::component::{
            DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone,
        };
        (&&&bevy_ecs::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
    }
    fn relationship_accessor()
        ->
            ::core::option::Option<bevy_ecs::relationship::ComponentRelationshipAccessor<Self>> {
        ::core::option::Option::None
    }
}Component)]
133#[cfg_attr(feature = "bevy_reflect", derive(const _: () =
    {
        impl bevy_reflect::GetTypeRegistration for DelayedCommandQueue where
            {
            fn get_type_registration() -> bevy_reflect::TypeRegistration {
                let mut registration =
                    bevy_reflect::TypeRegistration::of::<Self>();
                registration.insert::<bevy_reflect::ReflectFromPtr>(bevy_reflect::FromType::<Self>::from_type());
                registration.insert::<bevy_reflect::ReflectFromReflect>(bevy_reflect::FromType::<Self>::from_type());
                registration.register_type_data::<ReflectComponent, Self>();
                registration
            }
            #[inline(never)]
            fn register_type_dependencies(registry:
                    &mut bevy_reflect::TypeRegistry) {
                <Duration as
                        bevy_reflect::__macro_exports::RegisterForReflection>::__register(registry);
            }
        }
        impl bevy_reflect::Typed for DelayedCommandQueue where  {
            #[inline]
            fn type_info() -> &'static bevy_reflect::TypeInfo {
                static CELL: bevy_reflect::utility::NonGenericTypeInfoCell =
                    bevy_reflect::utility::NonGenericTypeInfoCell::new();
                CELL.get_or_set(||
                        {
                            bevy_reflect::TypeInfo::Struct(bevy_reflect::structs::StructInfo::new::<Self>(&[bevy_reflect::NamedField::new::<Duration>("submit_at")]))
                        })
            }
        }
        #[allow(deprecated, reason =
        "derives on a deprecated type shouldn't be considered a usage")]
        impl bevy_reflect::TypePath for DelayedCommandQueue where  {
            fn type_path() -> &'static str {
                "bevy_time::delayed_commands::DelayedCommandQueue"
            }
            fn short_type_path() -> &'static str { "DelayedCommandQueue" }
            fn type_ident() -> ::core::option::Option<&'static str> {
                ::core::option::Option::Some("DelayedCommandQueue")
            }
            fn crate_name() -> ::core::option::Option<&'static str> {
                ::core::option::Option::Some("bevy_time::delayed_commands".split(':').next().unwrap())
            }
            fn module_path() -> ::core::option::Option<&'static str> {
                ::core::option::Option::Some("bevy_time::delayed_commands")
            }
        }
        impl bevy_reflect::Reflect for DelayedCommandQueue where  {
            #[inline]
            fn into_any(self:
                    bevy_reflect::__macro_exports::alloc_utils::Box<Self>)
                ->
                    bevy_reflect::__macro_exports::alloc_utils::Box<dyn ::core::any::Any> {
                self
            }
            #[inline]
            fn as_any(&self) -> &dyn ::core::any::Any { self }
            #[inline]
            fn as_any_mut(&mut self) -> &mut dyn ::core::any::Any { self }
            #[inline]
            fn into_reflect(self:
                    bevy_reflect::__macro_exports::alloc_utils::Box<Self>)
                ->
                    bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::Reflect> {
                self
            }
            #[inline]
            fn as_reflect(&self) -> &dyn bevy_reflect::Reflect { self }
            #[inline]
            fn as_reflect_mut(&mut self) -> &mut dyn bevy_reflect::Reflect {
                self
            }
            #[inline]
            fn set(&mut self,
                value:
                    bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::Reflect>)
                ->
                    ::core::result::Result<(),
                    bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::Reflect>> {
                *self = <dyn bevy_reflect::Reflect>::take(value)?;
                ::core::result::Result::Ok(())
            }
        }
        impl bevy_reflect::structs::Struct for DelayedCommandQueue where  {
            fn field(&self, name: &str)
                -> ::core::option::Option<&dyn bevy_reflect::PartialReflect> {
                match name {
                    "submit_at" =>
                        ::core::option::Option::Some(&self.submit_at),
                    _ => ::core::option::Option::None,
                }
            }
            fn field_mut(&mut self, name: &str)
                ->
                    ::core::option::Option<&mut dyn bevy_reflect::PartialReflect> {
                match name {
                    "submit_at" =>
                        ::core::option::Option::Some(&mut self.submit_at),
                    _ => ::core::option::Option::None,
                }
            }
            fn field_at(&self, index: usize)
                -> ::core::option::Option<&dyn bevy_reflect::PartialReflect> {
                match index {
                    0usize => ::core::option::Option::Some(&self.submit_at),
                    _ => ::core::option::Option::None,
                }
            }
            fn field_at_mut(&mut self, index: usize)
                ->
                    ::core::option::Option<&mut dyn bevy_reflect::PartialReflect> {
                match index {
                    0usize => ::core::option::Option::Some(&mut self.submit_at),
                    _ => ::core::option::Option::None,
                }
            }
            fn name_at(&self, index: usize) -> ::core::option::Option<&str> {
                match index {
                    0usize => ::core::option::Option::Some("submit_at"),
                    _ => ::core::option::Option::None,
                }
            }
            fn index_of_name(&self, name: &str)
                -> ::core::option::Option<usize> {
                match name {
                    "submit_at" => ::core::option::Option::Some(0usize),
                    _ => ::core::option::Option::None,
                }
            }
            fn field_len(&self) -> usize { 1usize }
            fn iter_fields(&self) -> bevy_reflect::structs::FieldIter {
                bevy_reflect::structs::FieldIter::new(self)
            }
            fn to_dynamic_struct(&self)
                -> bevy_reflect::structs::DynamicStruct {
                let mut dynamic: bevy_reflect::structs::DynamicStruct =
                    ::core::default::Default::default();
                dynamic.set_represented_type(bevy_reflect::PartialReflect::get_represented_type_info(self));
                dynamic.insert_boxed("submit_at",
                    bevy_reflect::PartialReflect::to_dynamic(&self.submit_at));
                dynamic
            }
        }
        impl bevy_reflect::PartialReflect for DelayedCommandQueue where  {
            #[inline]
            fn get_represented_type_info(&self)
                -> ::core::option::Option<&'static bevy_reflect::TypeInfo> {
                ::core::option::Option::Some(<Self as
                            bevy_reflect::Typed>::type_info())
            }
            #[inline]
            fn try_apply(&mut self, value: &dyn bevy_reflect::PartialReflect)
                -> ::core::result::Result<(), bevy_reflect::ApplyError> {
                if let bevy_reflect::ReflectRef::Struct(struct_value) =
                        bevy_reflect::PartialReflect::reflect_ref(value) {
                    for (name, value) in
                        bevy_reflect::structs::Struct::iter_fields(struct_value) {
                        if let ::core::option::Option::Some(v) =
                                bevy_reflect::structs::Struct::field_mut(self, name) {
                            bevy_reflect::PartialReflect::try_apply(v, value)?;
                        }
                    }
                } else {
                    return ::core::result::Result::Err(bevy_reflect::ApplyError::MismatchedKinds {
                                from_kind: bevy_reflect::PartialReflect::reflect_kind(value),
                                to_kind: bevy_reflect::ReflectKind::Struct,
                            });
                }
                ::core::result::Result::Ok(())
            }
            #[inline]
            fn reflect_kind(&self) -> bevy_reflect::ReflectKind {
                bevy_reflect::ReflectKind::Struct
            }
            #[inline]
            fn reflect_ref(&self) -> bevy_reflect::ReflectRef {
                bevy_reflect::ReflectRef::Struct(self)
            }
            #[inline]
            fn reflect_mut(&mut self) -> bevy_reflect::ReflectMut {
                bevy_reflect::ReflectMut::Struct(self)
            }
            #[inline]
            fn reflect_owned(self:
                    bevy_reflect::__macro_exports::alloc_utils::Box<Self>)
                -> bevy_reflect::ReflectOwned {
                bevy_reflect::ReflectOwned::Struct(self)
            }
            #[inline]
            fn try_into_reflect(self:
                    bevy_reflect::__macro_exports::alloc_utils::Box<Self>)
                ->
                    ::core::result::Result<bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::Reflect>,
                    bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::PartialReflect>> {
                ::core::result::Result::Ok(self)
            }
            #[inline]
            fn try_as_reflect(&self)
                -> ::core::option::Option<&dyn bevy_reflect::Reflect> {
                ::core::option::Option::Some(self)
            }
            #[inline]
            fn try_as_reflect_mut(&mut self)
                -> ::core::option::Option<&mut dyn bevy_reflect::Reflect> {
                ::core::option::Option::Some(self)
            }
            #[inline]
            fn into_partial_reflect(self:
                    bevy_reflect::__macro_exports::alloc_utils::Box<Self>)
                ->
                    bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::PartialReflect> {
                self
            }
            #[inline]
            fn as_partial_reflect(&self)
                -> &dyn bevy_reflect::PartialReflect {
                self
            }
            #[inline]
            fn as_partial_reflect_mut(&mut self)
                -> &mut dyn bevy_reflect::PartialReflect {
                self
            }
            fn reflect_partial_eq(&self,
                value: &dyn bevy_reflect::PartialReflect)
                -> ::core::option::Option<bool> {
                (bevy_reflect::structs::struct_partial_eq)(self, value)
            }
            fn reflect_partial_cmp(&self,
                value: &dyn bevy_reflect::PartialReflect)
                -> ::core::option::Option<::core::cmp::Ordering> {
                (bevy_reflect::structs::struct_partial_cmp)(self, value)
            }
            #[inline]
            #[allow(unreachable_code, reason =
            "Ignored fields without a `clone` attribute will early-return with an error")]
            fn reflect_clone(&self)
                ->
                    ::core::result::Result<bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::Reflect>,
                    bevy_reflect::ReflectCloneError> {
                ::core::result::Result::Ok(bevy_reflect::__macro_exports::alloc_utils::Box::new(Self {
                            submit_at: <Duration as
                                        bevy_reflect::PartialReflect>::reflect_clone_and_take(&self.submit_at)?,
                            queue: return ::core::result::Result::Err(bevy_reflect::ReflectCloneError::FieldNotCloneable {
                                        field: bevy_reflect::FieldId::Named(bevy_reflect::__macro_exports::alloc_utils::Cow::Borrowed("queue")),
                                        variant: ::core::option::Option::None,
                                        container_type_path: bevy_reflect::__macro_exports::alloc_utils::Cow::Borrowed(<Self
                                                    as bevy_reflect::TypePath>::type_path()),
                                    }),
                        }))
            }
        }
        impl bevy_reflect::FromReflect for DelayedCommandQueue where  {
            fn from_reflect(reflect: &dyn bevy_reflect::PartialReflect)
                -> ::core::option::Option<Self> {
                if let bevy_reflect::ReflectRef::Struct(__ref_struct) =
                        bevy_reflect::PartialReflect::reflect_ref(reflect) {
                    let __this =
                        Self {
                            submit_at: <Duration as
                                        bevy_reflect::FromReflect>::from_reflect(bevy_reflect::structs::Struct::field(__ref_struct,
                                            "submit_at")?)?,
                            queue: ::core::default::Default::default(),
                        };
                    ::core::option::Option::Some(__this)
                } else { ::core::option::Option::None }
            }
        }
    };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}