-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Add a timer-powered DelayedCommand
for delayed ECS actions
#20155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
use crate::Time; | ||
use alloc::boxed::Box; | ||
use bevy_ecs::prelude::*; | ||
use bevy_log::warn; | ||
use core::time::Duration; | ||
|
||
/// A [`Command`] that will be executed after a specified delay has elapsed. | ||
/// | ||
/// This can be helpful for scheduling actions at some point in the future. | ||
/// | ||
/// This works by moving the supplied command into a component that is spawned on an entity. | ||
/// Delayed command entities are ticked via [`tick_delayed_commands`], | ||
/// which is typically run in [`First`] as part of [`TimePlugin`]. | ||
#[derive(Component)] | ||
pub struct DelayedCommand { | ||
pub delay: Duration, | ||
pub command: Box<dyn Command + Send + Sync + 'static>, | ||
} | ||
|
||
impl DelayedCommand { | ||
pub fn new(delay: Duration, command: impl Command + Send + Sync + 'static) -> Self { | ||
Self { | ||
delay, | ||
command: Box::new(command), | ||
} | ||
} | ||
} | ||
|
||
impl Command for DelayedCommand { | ||
/// Spawns a new entity with the [`DelayedCommand`] as a component. | ||
fn apply(self, world: &mut World) { | ||
world.spawn(self); | ||
} | ||
} | ||
Comment on lines
+29
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels weird to me. Why not have users spawn/insert it like other components? We don't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, that's an interesting API idea 🤔 |
||
|
||
pub fn tick_delayed_commands( | ||
mut commands: Commands, | ||
time: Res<Time>, | ||
mut delayed_commands: Query<(Entity, &mut DelayedCommand)>, | ||
) { | ||
let delta = time.delta(); | ||
for (entity, mut delayed_command) in delayed_commands.iter_mut() { | ||
delayed_command.delay -= delta; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my similar implementation it does not tick down but has the delay as a point in the future (initialized as:
upside, you can have the query as none mut and have no math operations. Not sure there are any downsides. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh that's super clever. Good idea! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you make it an immutable component, you could even index it using a priority queue so that you only need to check the time of the next delayed command! Although if we're expecting these commands to frequently be despawned or have their timers changed then you might have to remove them from the queue early. |
||
if delayed_command.delay <= Duration::ZERO { | ||
commands.entity(entity).queue(EvaluateDelayedCommand); | ||
} | ||
} | ||
} | ||
|
||
/// An [`EntityCommand`] that causes a delayed command to be evaluated. | ||
/// | ||
/// This will send the command to the [`CommandQueue`] for execution, | ||
/// and clean up the entity that held the delayed command. | ||
struct EvaluateDelayedCommand; | ||
|
||
impl EntityCommand for EvaluateDelayedCommand { | ||
fn apply(self, mut entity_world_mut: EntityWorldMut) -> () { | ||
// Take the DelayedCommand component from the entity, | ||
// allowing us to execute the command and clean up the entity | ||
// without cloning the command. | ||
let Some(delayed_command) = entity_world_mut.take::<DelayedCommand>() else { | ||
warn!( | ||
"Entity {} does not have a DelayedCommand component at the time of evaluation", | ||
entity_world_mut.id() | ||
); | ||
entity_world_mut.despawn(); | ||
|
||
return; | ||
}; | ||
|
||
// Clean up the entity that held the delayed command | ||
let entity = entity_world_mut.id(); | ||
let world = entity_world_mut.into_world_mut(); | ||
world.despawn(entity); | ||
|
||
// Execute the delayed command | ||
world.commands().queue(delayed_command.command); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a perf cost to this; maybe we could have a pre-existing entities that gets re-used?
(and eventually it would be on a resource when resource-as-components lands)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm alright with the performance cost here. Better inspectability + cancellation + a simpler implementation is worth it IMO.
I don't expect this to be super hot, and users can comfortably add their own abstraction if it is.