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
10pub struct DelayedCommands<'w, 's> {
16 queues: HashMap<Duration, CommandQueue>,
18
19 commands: Commands<'w, 's>,
22}
23
24impl<'w, 's> DelayedCommands<'w, 's> {
25 #[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 let queue = self.queues.entry(duration).or_default();
30 self.commands.rebound_to(queue)
32 }
33
34 #[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 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 let time = world.resource::<Time>();
52 let elapsed = time.elapsed();
53 for queue in queues.iter_mut() {
54 queue.submit_at += elapsed;
56 }
57 spawn_batch(queues).apply(world);
58 });
59 }
60}
61
62pub trait DelayedCommandsExt<'w> {
64 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#[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 pub submit_at: Duration,
137
138 #[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
140 pub queue: CommandQueue,
141}
142
143pub 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 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}