diff --git a/Cargo.toml b/Cargo.toml index f6948c6f8c29f..5a1a4861858f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2494,6 +2494,17 @@ description = "Demonstrates a startup system (one that runs once when the app st category = "ECS (Entity Component System)" wasm = false +[[example]] +name = "delayed_commands" +path = "examples/ecs/delayed_commands.rs" +doc-scrape-examples = true + +[package.metadata.example.delayed_commands] +name = "Delayed Commands" +description = "Demonstrates how to schedule ECS commands with a delay" +category = "ECS (Entity Component System)" +wasm = true + [[example]] name = "states" path = "examples/state/states.rs" diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 2ba10fa79d0c4..d2da57458ad0c 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -268,6 +268,20 @@ impl<'w, 's> Commands<'w, 's> { } } + /// Returns a new [`Commands`] that writes commands to the provided [`CommandQueue`] instead of the one from `self`. + /// + /// This is useful if you have a `Commands` that writes to one queue and you want one that writes to another. + /// + /// Note that you're responsible for ensuring the queue eventually writes its commands to the world. One way to + /// do this is calling [`Commands::append`] on a `Commands` that writes to the world queue. Failure to write a + /// queue may result in entities being allocated but never spawned, which means those entity IDs are never + /// freed for reuse. + /// + /// The original `Commands` isn't mutated or borrowed after this returns, so you can keep using it. + pub fn rebound_to<'q>(&self, queue: &'q mut CommandQueue) -> Commands<'w, 'q> { + Commands::new_from_entities(queue, self.allocator, self.entities) + } + /// Returns a [`Commands`] with a smaller lifetime. /// /// This is useful if you have `&mut Commands` but need `Commands`. diff --git a/crates/bevy_time/src/delayed_commands.rs b/crates/bevy_time/src/delayed_commands.rs new file mode 100644 index 0000000000000..a66cec65f4599 --- /dev/null +++ b/crates/bevy_time/src/delayed_commands.rs @@ -0,0 +1,224 @@ +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; + +/// A wrapper over [`Commands`] that stores [`CommandQueue`]s to be applied with given delays. +/// +/// When dropped, the queues are spawned into the world as new entities with +/// [`DelayedCommandQueue`] components, and then checked by the +/// [`check_delayed_command_queues`] system. +pub struct DelayedCommands<'w, 's> { + /// Used to own queues and deduplicate them by their duration. + queues: HashMap, + + /// The wrapped `Commands` - used to provision out new `Commands` + /// and to spawn the queues as entities when the struct is dropped. + commands: Commands<'w, 's>, +} + +impl<'w, 's> DelayedCommands<'w, 's> { + /// Return a [`Commands`] whose commands will be delayed by `duration`. + #[must_use = "The returned Commands must be used to submit commands with this delay."] + pub fn duration(&mut self, duration: Duration) -> Commands<'w, '_> { + // Fetch a queue with the given duration or create one + let queue = self.queues.entry(duration).or_default(); + // Return a new `Commands` to write commands to the queue + self.commands.rebound_to(queue) + } + + /// Return a [`Commands`] whose commands will be delayed by `secs` seconds. + #[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)) + } + + /// Drains and spawns the contained command queues as [`DelayedCommandQueue`] entities. + fn submit(&mut self) { + let mut queues = self + .queues + .drain() + .map(|(submit_at, queue)| DelayedCommandQueue { submit_at, queue }) + .collect::>(); + + self.commands.queue(move |world: &mut World| { + // We use the default Time<()> here intentionally to support custom clocks + let time = world.resource::