use alloc::vec::Vec;
use bevy_ecs::{prelude::*, system::command::spawn_batch, world::CommandQueue};
use bevy_platform::collections::HashMap;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
use core::time::Duration;
use crate::Time;
pub struct DelayedCommands<'w, 's> {
queues: HashMap<Duration, CommandQueue>,
commands: Commands<'w, 's>,
}
impl<'w, 's> DelayedCommands<'w, 's> {
#[must_use = "The returned Commands must be used to submit commands with this delay."]
pub fn duration(&mut self, duration: Duration) -> Commands<'w, '_> {
let queue = self.queues.entry(duration).or_default();
self.commands.rebound_to(queue)
}
#[inline]
#[must_use = "The returned Commands must be used to submit commands with this delay."]
pub fn secs(&mut self, secs: f32) -> Commands<'w, '_> {
self.duration(Duration::from_secs_f32(secs))
}
fn submit(&mut self) {
let mut queues = self
.queues
.drain()
.map(|(submit_at, queue)| DelayedCommandQueue { submit_at, queue })
.collect::<Vec<_>>();
self.commands.queue(move |world: &mut World| {
let time = world.resource::<Time>();
let elapsed = time.elapsed();
for queue in queues.iter_mut() {
queue.submit_at += elapsed;
}
spawn_batch(queues).apply(world);
});
}
}
pub trait DelayedCommandsExt<'w> {
fn delayed(&mut self) -> DelayedCommands<'w, '_>;
}
impl<'w, 's> DelayedCommandsExt<'w> for Commands<'w, 's> {
fn delayed(&mut self) -> DelayedCommands<'w, '_> {
DelayedCommands {
commands: self.reborrow(),
queues: HashMap::default(),
}
}
}
impl<'w, 's> Drop for DelayedCommands<'w, 's> {
fn drop(&mut self) {
self.submit();
}
}
#[derive(Component)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]
pub struct DelayedCommandQueue {
pub submit_at: Duration,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub queue: CommandQueue,
}
pub fn check_delayed_command_queues(
queues: Query<(Entity, &mut DelayedCommandQueue)>,
time: Res<Time>,
mut commands: Commands,
) {
let elapsed = time.elapsed();
for (e, mut queue) in queues {
if queue.submit_at <= elapsed {
commands.append(&mut queue.queue);
commands.entity(e).despawn();
}
}
}
#[cfg(test)]
#[expect(clippy::print_stdout, reason = "Allowed in tests.")]
mod tests {
use core::time::Duration;
use std::println;
use bevy_app::{App, Startup};
use bevy_ecs::{component::Component, system::Commands};
use crate::{DelayedCommandsExt, TimePlugin, TimeUpdateStrategy};
#[derive(Component)]
struct DummyComponent;
#[test]
fn delayed_queues_should_run_with_time_plugin_enabled() {
fn queue_commands(mut commands: Commands) {
commands.delayed().secs(0.1).spawn(DummyComponent);
commands.spawn(DummyComponent);
let mut delayed_cmds = commands.delayed();
delayed_cmds.secs(0.5).spawn(DummyComponent);
let mut in_1_sec = delayed_cmds.duration(Duration::from_secs_f32(1.0));
in_1_sec.spawn(DummyComponent);
in_1_sec.spawn(DummyComponent);
in_1_sec.spawn(DummyComponent);
}
let mut app = App::new();
app.add_plugins(TimePlugin)
.add_systems(Startup, queue_commands)
.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f32(
0.2,
)));
for frame in 0..10 {
app.update();
let dummy_count = app
.world_mut()
.query::<&DummyComponent>()
.iter(app.world())
.count();
println!("Frame {frame}, {dummy_count} dummies spawned");
match frame {
0 => {
assert_eq!(dummy_count, 1);
}
1 | 2 => {
assert_eq!(dummy_count, 2);
}
3 | 4 => {
assert_eq!(dummy_count, 3);
}
_ => {
assert_eq!(dummy_count, 6);
}
}
}
}
}