Add a timer-powered DelayedCommand for delayed ECS actions#20155
Add a timer-powered DelayedCommand for delayed ECS actions#20155alice-i-cecile wants to merge 1 commit intobevyengine:mainfrom
DelayedCommand for delayed ECS actions#20155Conversation
|
Having written up a problem description, I'm reasonably confident that the generic is the least fragile and complex option. And also the most likely to work! It is extremely annoying that boxed commands were broken by error handling (or maybe they never worked), but that's not this PR's problem to solve TBH. |
| impl Command for DelayedCommand { | ||
| /// Spawns a new entity with the [`DelayedCommand`] as a component. | ||
| fn apply(self, world: &mut World) { | ||
| world.spawn(self); | ||
| } | ||
| } |
There was a problem hiding this comment.
This feels weird to me. Why not have users spawn/insert it like other components? We don't impl Command for Observer, for example.
There was a problem hiding this comment.
Oh, that's an interesting API idea 🤔
|
We'll also want to support delayed commands in both |
| impl Command for DelayedCommand { | ||
| /// Spawns a new entity with the [`DelayedCommand`] as a component. | ||
| fn apply(self, world: &mut World) { | ||
| world.spawn(self); |
There was a problem hiding this comment.
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.
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.
| ) { | ||
| 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.
In my similar implementation it does not tick down but has the delay as a point in the future (initialized as: timer.elapsed() + duration). So instead of ticking down it just compares
let current_time = time.elapsed();
for (entity, delayed_command) in &delayed_commands {
if delayed_command.delay <= current_time {
commands.entity(entity).queue(EvaluateDelayedCommand);
}
}
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.
Oh that's super clever. Good idea!
There was a problem hiding this comment.
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.
In case it helps: you can convert an fn command_to_boxed_fn(command: impl Command) -> Box<dyn FnOnce(&mut World) + Send + 'static> {
Box::new(|world| command.apply(world))
}And
I think they never worked, and the issue is that The blanket |
|
I've had a look at this, and i don't really like the API it provides. Having to manually instantiate and pass command structs to it is a bit clunky. Instead, i came up with a draft which allows using the same The idea stems from the fact that all commands which Usage: Implementation: struct DelayedCommands<C: FnOnce(Commands) + Send + Sync + 'static> {
delay: Duration,
write_commands: C,
}
trait DelayedCommandsExt {
fn delayed(
&mut self,
delay: Duration,
write_commands: impl FnOnce(Commands) + Send + Sync + 'static,
);
}
impl DelayedCommandsExt for Commands<'_, '_> {
fn delayed(
&mut self,
delay: Duration,
write_commands: impl FnOnce(Commands) + Send + Sync + 'static,
) {
self.queue(DelayedCommands {
delay,
write_commands,
});
}
}
impl<C: FnOnce(Commands) + Send + Sync + 'static> Command for DelayedCommands<C> {
fn apply(self, world: &mut World) {
let mut command_queue = CommandQueue::default();
(self.write_commands)(Commands::new(&mut command_queue, world));
world.spawn(DelayedCommandQueue(
Timer::new(self.delay, TimerMode::Once),
command_queue,
));
println!("spawned delayed command queue");
}
}
#[derive(Component)]
struct DelayedCommandQueue(Timer, CommandQueue);
fn command_after(
cmds: Query<(Entity, &mut DelayedCommandQueue)>,
time: Res<Time>,
mut commands: Commands,
) {
for (e, mut cmd) in cmds {
if cmd.0.tick(time.delta()).just_finished() {
commands.append(&mut cmd.1);
commands.entity(e).despawn();
}
}
} |
|
It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note. Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes. |
|
@alice-i-cecile are the changed labels a way to ask/nudge me to make a new PR with my version or...? |
This would be nice! It's also a way to signal that I'm not immediately going to return to this work, so others are welcome to pick it up. |
…evyengine#23090) # Objective - A generalized mechanism for "doing something later" is desirable for many games, especially when it comes to gameplay logic and VFX. - Fixes bevyengine#15129 - Closes bevyengine#20155 ## Solution - Build off the work in bevyengine#20155 (comment), especially @laundmo's comment. - Add a `DelayedCommands` helper obtainable via `commands.delayed()` that owns `CommandQueue`s and hands out new `Commands` bound to them. - When the `DelayedCommands` helper is dropped, push spawn commands onto the host `Commands` to spawn the queues as `DelayedCommandQueue` entities. - The entities are ticked by a new system added by `TimePlugin`. When the timer fires, the queue is submitted onto that system's `Commands`. ## Testing - Added a new test in `bevy_time` and it seems to work. - I'm not very familiar with doing hacky things like using `Drop` like this and would therefore appreciate careful review and guidance if changes are requested. --- ## Showcase ```rust fn my_cool_system(mut commands: Commands) { // fairly unobtrusive one-line delayed spawn commands.delayed().secs(0.1).spawn(DummyComponent); // the DelayedCommands can be stored to reuse more tersely let mut delayed = commands.delayed(); // allocation happens immediately so you can even queue // further operations on entities that aren't spawned yet let entity = delayed.secs(0.5).spawn_empty().id(); delayed.secs(0.7).entity(entity).insert(DummyComponent); // `delayed.secs` and `delayed.duration` both simply return a // `Commands` rebound to the stored `CommandQueue`, so you can additionally // just store that and reuse it to queue multiple commands with the same delay let mut in_1_sec = delayed.duration(Duration::from_secs_f32(1.0)); in_1_sec.spawn(DummyComponent); in_1_sec.spawn(DummyComponent); in_1_sec.spawn(DummyComponent); } ``` --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
…evyengine#23090) # Objective - A generalized mechanism for "doing something later" is desirable for many games, especially when it comes to gameplay logic and VFX. - Fixes bevyengine#15129 - Closes bevyengine#20155 ## Solution - Build off the work in bevyengine#20155 (comment), especially @laundmo's comment. - Add a `DelayedCommands` helper obtainable via `commands.delayed()` that owns `CommandQueue`s and hands out new `Commands` bound to them. - When the `DelayedCommands` helper is dropped, push spawn commands onto the host `Commands` to spawn the queues as `DelayedCommandQueue` entities. - The entities are ticked by a new system added by `TimePlugin`. When the timer fires, the queue is submitted onto that system's `Commands`. ## Testing - Added a new test in `bevy_time` and it seems to work. - I'm not very familiar with doing hacky things like using `Drop` like this and would therefore appreciate careful review and guidance if changes are requested. --- ## Showcase ```rust fn my_cool_system(mut commands: Commands) { // fairly unobtrusive one-line delayed spawn commands.delayed().secs(0.1).spawn(DummyComponent); // the DelayedCommands can be stored to reuse more tersely let mut delayed = commands.delayed(); // allocation happens immediately so you can even queue // further operations on entities that aren't spawned yet let entity = delayed.secs(0.5).spawn_empty().id(); delayed.secs(0.7).entity(entity).insert(DummyComponent); // `delayed.secs` and `delayed.duration` both simply return a // `Commands` rebound to the stored `CommandQueue`, so you can additionally // just store that and reuse it to queue multiple commands with the same delay let mut in_1_sec = delayed.duration(Duration::from_secs_f32(1.0)); in_1_sec.spawn(DummyComponent); in_1_sec.spawn(DummyComponent); in_1_sec.spawn(DummyComponent); } ``` --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
…evyengine#23090) # Objective - A generalized mechanism for "doing something later" is desirable for many games, especially when it comes to gameplay logic and VFX. - Fixes bevyengine#15129 - Closes bevyengine#20155 ## Solution - Build off the work in bevyengine#20155 (comment), especially @laundmo's comment. - Add a `DelayedCommands` helper obtainable via `commands.delayed()` that owns `CommandQueue`s and hands out new `Commands` bound to them. - When the `DelayedCommands` helper is dropped, push spawn commands onto the host `Commands` to spawn the queues as `DelayedCommandQueue` entities. - The entities are ticked by a new system added by `TimePlugin`. When the timer fires, the queue is submitted onto that system's `Commands`. ## Testing - Added a new test in `bevy_time` and it seems to work. - I'm not very familiar with doing hacky things like using `Drop` like this and would therefore appreciate careful review and guidance if changes are requested. --- ## Showcase ```rust fn my_cool_system(mut commands: Commands) { // fairly unobtrusive one-line delayed spawn commands.delayed().secs(0.1).spawn(DummyComponent); // the DelayedCommands can be stored to reuse more tersely let mut delayed = commands.delayed(); // allocation happens immediately so you can even queue // further operations on entities that aren't spawned yet let entity = delayed.secs(0.5).spawn_empty().id(); delayed.secs(0.7).entity(entity).insert(DummyComponent); // `delayed.secs` and `delayed.duration` both simply return a // `Commands` rebound to the stored `CommandQueue`, so you can additionally // just store that and reuse it to queue multiple commands with the same delay let mut in_1_sec = delayed.duration(Duration::from_secs_f32(1.0)); in_1_sec.spawn(DummyComponent); in_1_sec.spawn(DummyComponent); in_1_sec.spawn(DummyComponent); } ``` --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Objective
TimedCommandsSystemParamfor easier delayed operations #15129Solution
Create a
DelayedCommandcommand, which wraps another command, moves it into the world as an entity, and then ticks down until it's time to evaluate, despawning itself.An equivalent
DelayedEntityCommandis also provided to allow for easier usage with entity commands.TODO
Commands::queuerequiresHandleErrorHandleErroris not dyn compatible, so we cannot simply add it to the box via a helper traitFnOnceclosures, because then you can't use this for commands that store dataHandleErrordyn compatible?queue_delayedAPI for usabilityEntityCommandvariant