Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 14 additions & 0 deletions crates/bevy_ecs/src/system/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
224 changes: 224 additions & 0 deletions crates/bevy_time/src/delayed_commands.rs
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
alice-i-cecile marked this conversation as resolved.
///
/// 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<Duration, CommandQueue>,
Comment thread
Runi-c marked this conversation as resolved.

/// 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, '_> {
Comment thread
Runi-c marked this conversation as resolved.
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::<Vec<_>>();

self.commands.queue(move |world: &mut World| {
// We use the default Time<()> here intentionally to support custom clocks
let time = world.resource::<Time>();
let elapsed = time.elapsed();
for queue in queues.iter_mut() {
// Turn relative delays into absolute elapsed times
queue.submit_at += elapsed;
}
spawn_batch(queues).apply(world);
});
}
}

/// Extension trait for [`Commands`] that provides delayed command functionality.
pub trait DelayedCommandsExt<'w> {
/// Returns a [`DelayedCommands`] instance that can be used to queue
/// commands to be submitted at a later point in time.
///
/// When dropped, the [`DelayedCommands`] submits spawn commands that will
/// spawn [`DelayedCommandQueue`] entities. The entities are checked
/// by the [`check_delayed_command_queues`] system, and their queues are
/// submitted when the specified time has elapsed.
///
/// # Usage
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # use bevy_time::DelayedCommandsExt;
/// fn my_system(mut commands: Commands) {
/// // Spawn an entity after one second
/// commands.delayed().secs(1.0).spawn_empty();
/// }
/// # bevy_ecs::system::assert_is_system(my_system);
/// ```
///
/// Entity allocation happens immediately even if the spawn command is delayed.
/// This allows you to queue delayed commands on an entity that hasn't been spawned yet.
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # use bevy_time::DelayedCommandsExt;
/// fn my_system(mut commands: Commands) {
/// let mut delayed = commands.delayed();
/// // spawn an entity after 1 second, then despawn it a second later
/// let entity = delayed.secs(1.0).spawn_empty().id();
/// delayed.secs(2.0).entity(entity).despawn();
/// }
/// # bevy_ecs::system::assert_is_system(my_system);
/// ```
///
/// # Timing
///
/// Delayed commands are currently checked against the default clock in the [`PreUpdate`]
/// schedule. There's currently no way to specify different clocks for different
/// delayed commands - this is a limitation of the system and if you need this behavior
/// you'll likely have to implement your own delay system.
///
/// [`PreUpdate`]: bevy_app::PreUpdate
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();
}
}

/// A component with a [`CommandQueue`] to be submitted later.
///
/// Queues in these components are checked automatically by the
/// [`check_delayed_command_queues`] added by [`TimePlugin`] and submitted when
/// the default clock's elapsed time exceeds `submit_at`.
///
/// [`TimePlugin`]: crate::TimePlugin
#[derive(Component)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]
pub struct DelayedCommandQueue {
/// The elapsed time from startup when `queue` should be submitted.
pub submit_at: Duration,

/// The queue to be submitted when time is up.
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub queue: CommandQueue,
}

/// The system used to check [`DelayedCommandQueue`]s, which are usually spawned
/// by [`DelayedCommands`]. When the elapsed time exceeds a queue's `submit_at` time,
/// the contained `queue` is appended to the system's [`Commands`].
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 {
// Write the contained delayed commands to the world.
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);
}
}
}
}
}
5 changes: 4 additions & 1 deletion crates/bevy_time/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ extern crate alloc;

/// Common run conditions
pub mod common_conditions;
mod delayed_commands;
mod fixed;
mod real;
mod stopwatch;
mod time;
mod timer;
mod virt;

pub use delayed_commands::*;
pub use fixed::*;
pub use real::*;
pub use stopwatch::*;
Expand All @@ -33,7 +35,7 @@ pub use virt::*;
/// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude {
#[doc(hidden)]
pub use crate::{Fixed, Real, Time, Timer, TimerMode, Virtual};
pub use crate::{DelayedCommandsExt, Fixed, Real, Time, Timer, TimerMode, Virtual};
}

use bevy_app::{prelude::*, RunFixedMainLoop};
Expand Down Expand Up @@ -83,6 +85,7 @@ impl Plugin for TimePlugin {
.in_set(TimeSystems)
.ambiguous_with(message_update_system),
)
.add_systems(PreUpdate, check_delayed_command_queues)
.add_systems(
RunFixedMainLoop,
run_fixed_main_schedule.in_set(RunFixedMainLoopSystems::FixedMainLoop),
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ Example | Description
[Contiguous Query](../examples/ecs/contiguous_query.rs) | Demonstrates contiguous queries
[Custom Query Parameters](../examples/ecs/custom_query_param.rs) | Groups commonly used compound queries and query filters into a single type
[Custom Schedule](../examples/ecs/custom_schedule.rs) | Demonstrates how to add custom schedules
[Delayed Commands](../examples/ecs/delayed_commands.rs) | Demonstrates how to schedule ECS commands with a delay
[Dynamic ECS](../examples/ecs/dynamic.rs) | Dynamically create components, spawn entities with those components and query those components
[ECS Guide](../examples/ecs/ecs_guide.rs) | Full guide to Bevy's ECS
[Entity disabling](../examples/ecs/entity_disabling.rs) | Demonstrates how to hide entities from the ECS without deleting them
Expand Down
60 changes: 60 additions & 0 deletions examples/ecs/delayed_commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//! This example demonstrates how to send commands which will take effect after a period of time.
//!
//! We've chosen to demonstrate this effect through the creation of a grid of clickable,
//! with "ripples" created when you click.

use bevy::prelude::*;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, spawn)
.add_observer(click)
.run();
}

#[derive(Component)]
struct BlinkySquare;

const SQUARE_SIZE: Vec2 = Vec2::splat(45.0);

fn spawn(mut commands: Commands) {
commands.spawn(Camera2d);
for x in -5..=5 {
for y in -5..=5 {
commands.spawn((
BlinkySquare,
Transform::from_xyz(x as f32 * 50.0, y as f32 * 50.0, 0.0),
Sprite::from_color(Color::BLACK, SQUARE_SIZE),
));
}
}
}

fn click(
click: On<Pointer<Click>>,
mut commands: Commands,
squares: Query<(Entity, &Transform), With<BlinkySquare>>,
cameras: Query<(&Camera, &GlobalTransform)>,
) {
let (camera, camera_transform) = cameras.single().unwrap();
let mut delayed = commands.delayed();
for (entity, transform) in squares.iter() {
// convert the pointer position to world position
let mouse_world_pos = camera
.viewport_to_world_2d(camera_transform, click.pointer_location.position)
.unwrap();

// delay the blinkiness by distance to cursor
let dist = mouse_world_pos.distance(transform.translation.truncate());
let delay = dist / 1000.0;
delayed
.secs(delay)
.entity(entity)
.insert(Sprite::from_color(Color::WHITE, SQUARE_SIZE));
delayed
.secs(delay + 0.1)
.entity(entity)
.insert(Sprite::from_color(Color::BLACK, SQUARE_SIZE));
}
}
28 changes: 28 additions & 0 deletions release-content/release-notes/delayed_commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: Delayed Commands
authors: ["@Runi-c"]
pull_requests: [23090]
---

Scheduling things to happen some time in the future is a common and useful tool in game development
for everything from gameplay logic to VFX. To support common use-cases, Bevy now has a general mechanism
for delaying commands to be executed after a specified duration.

```rust
fn delayed_spawn(mut commands: Commands) {
commands.delayed().secs(1.0).spawn(DummyComponent);
}

fn delayed_spawn_then_insert(mut commands: Commands) {
let mut delayed = commands.delayed();
let entity = delayed.secs(0.5).spawn_empty().id();
delayed.secs(1.5).entity(entity).insert(DummyComponent);
}
```

Our goal for this mechanism is to provide a "good-enough" system for simple use-cases. As a result,
there are certain limitations - for example, delayed commands are currently always ticked by the default
clock during `PreUpdate` (typically `Time<Virtual>`).

If you need something more sophisticated, you can always roll your own version of delayed commands using
the new helpers added for this feature.