-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Description
What problem does this solve or what need does it fill?
Provides a built-in way to despawn entities after a given period.
Currently there is no mechanism for this in Bevy, and while it's easy enough to implement manually it's likely something that many users will reach for in the engine.
What solution would you like?
I've split this up into two sections
DespawnAfter
, which describes a straight forward component that can be added which will be ticked by a system within Bevy and despawn the entity it's on once the time has elapsedGenericTimer
, which is a slightly more general timer that can be observed. The "despawn after" mechanism would build on this
Additional context
Discord context: https://discord.com/channels/691052431525675048/692572690833473578/1397088235427205140
Loosely inspired by Godot's timer.
I'm not strongly attached to names of any of these constructs, I just picked the first thing off the top of my head.
DespawnAfter
A simple struct component that counts down to zero from some initial duration. Upon reaching zero it despawns the entity it's on.
I lean toward just using a f32
for such a simple component, as Timer
is a relatively heavy 48 bytes compared to just 4 for the f32
, for negligible if any real advantage in this context. If this component had any more functionality I could see it being more useful, but right now it seems like pure overhead.
I haven't included any logic to allow ignoring the timescale, useful if the user is doing some kind of slow-motion effect by modifying Time<Virtual>
, or for ignoring Time<Virtual>
being paused. Both of these options are on the Godot timer and I think they are there for good reason, and the actual implementation should include them. The functionality of a general timer should be able to "ignore" gameplay circumstances like time scaling/pausing, otherwise users will have to build their own workarounds.
Usage
Could be put into a method on Commands
that would default to one schedule or the other, but this is the most basic use.
fn user_system(mut c: Commands) {
let user_entity: Entity = c.spawn_empty().id();
c.entity(user_entity)
.insert(DespawnAfter::<Update>::new(5.));
}
Per Alice's suggestion of configuring which schedule it should run within it uses a generic parameter to differentiate. I didn't know if there is an existing way to indicate Fixed vs Update, and using something like a ScheduleLabel is too general.
If we make the despawn_after_tick
function public then users can implement the trait for their own schedules if they want to re-use this DespawnAfter
functionality.
pub fn despawn_after_plugin(app: &mut App) {
app.add_systems(PostUpdate, despawn_after_tick::<Update>);
app.add_systems(FixedPostUpdate, despawn_after_tick::<FixedUpdate>);
}
pub trait BevyScheduleStage: Send + Sync + 'static {}
impl BevyScheduleStage for Update {}
impl BevyScheduleStage for FixedUpdate {}
#[derive(Component)]
pub struct DespawnAfter<Stage: BevyScheduleStage> {
time_remaining: f32,
_stage: PhantomData<Stage>,
}
impl<Stage: BevyScheduleStage> DespawnAfter<Stage> {
pub fn new(seconds: f32) -> Self {
Self {
time_remaining: seconds,
_stage: PhantomData,
}
}
}
pub fn despawn_after_tick<Stage: BevyScheduleStage>(
mut q: Query<(&mut DespawnAfter<Stage>, Entity)>,
time: Res<Time>,
mut c: Commands,
) {
q.iter_mut().for_each(|(mut despawn_after, entity)| {
despawn_after.time_remaining -= time.delta_secs();
if despawn_after.time_remaining <= 0. {
c.entity(entity).despawn();
}
});
}
GenericTimer
Implementing DespawnAfter
got me thinking about a slightly more useful timer that could be used for repeated events as well as one shot events like despawning on a timer. It is not much more code for vastly more flexibility.
It would function similarly in practice to DespawnAfter
, although it would trigger an event instead of just despawning the entity it's on. Then DespawnAfter
can be implemented as a thin observer over the GenericTimer
.
The use of a generic for indicating the schedule to run in is computationally optimal vs a bool to be checked in the ticking system, but it does make the observer slightly more awkward to use, and users might need to be aware of which schedule the timer is in in-order to register the observer properly. Though I assume it could be handled better, I haven't thought too much about that particular aspect.
Usage
fn user_system(mut c: Commands) {
let my_entity = c.spawn_empty().id();
c.despawn_after(my_entity, 5.);
}
// Defined in Bevy, Imagine this is a method on `Commands`
fn despawn_after(c: &mut Commands, target: Entity, seconds: f32) {
c.spawn((
GenericTimer::<FixedUpdate>::new(Timer::from_seconds(seconds, TimerMode::Once)),
Observer::new(
move |_: Trigger<GenericTimerFinished<FixedUpdate>>, mut c: Commands| {
if let Ok(mut target) = c.get_entity(target) {
target.despawn();
}
},
)
.with_entity(target),
));
}
The use of an Event
to propagate the timer finishing allows flexible use by observers. If the user wants to get some information from the underlying timer, such as how many ticks elapsed this frame, or to modify it, they can use the event.target
on the observer trigger.
#[derive(Component)]
pub struct GenericTimer<Stage: BevyScheduleStage> {
timer: Timer,
despawn_on_finish: bool,
_stage: PhantomData<Stage>,
}
impl<Stage: BevyScheduleStage> GenericTimer<Stage> {
pub fn new(timer: Timer) -> Self {
let despawn_on_finish = timer.mode() == TimerMode::Once;
Self {
timer,
despawn_on_finish,
_stage: PhantomData,
}
}
pub fn with_despawn_on_finish(mut self, despawn: bool) -> Self {
self.despawn_on_finish = despawn;
self
}
}
#[derive(Event)]
pub struct GenericTimerFinished<Stage: BevyScheduleStage>(PhantomData<Stage>);
fn generic_timer_tick<Stage: BevyScheduleStage>(
mut q: Query<(&mut GenericTimer<Stage>, Entity)>,
time: Res<Time>,
mut c: Commands,
) {
q.iter_mut().for_each(|(mut generic_timer, entity)| {
generic_timer.timer.tick(time.delta());
if generic_timer.timer.just_finished() {
c.trigger_targets(GenericTimerFinished::<Stage>(PhantomData), entity);
if generic_timer.despawn_on_finish {
c.entity(entity).despawn();
}
}
});
}