Wait until FixedUpdate can see events before dropping them (#10077)

## Objective
 
Currently, events are dropped after two frames. This cadence wasn't
*chosen* for a specific reason, double buffering just lets events
persist for at least two frames. Events only need to be dropped at a
predictable point so that the event queues don't grow forever (i.e.
events should never cause a memory leak).
 
Events (and especially input events) need to be observable by systems in
`FixedUpdate`, but as-is events are dropped before those systems even
get a chance to see them.
 
## Solution
 
Instead of unconditionally dropping events in `First`, require
`FixedUpdate` to first queue the buffer swap (if the `TimePlugin` has
been installed). This way, events are only dropped after a frame that
runs `FixedUpdate`.
 
## Future Work

In the same way we have independent copies of `Time` for tracking time
in `Main` and `FixedUpdate`, we will need independent copies of `Input`
for tracking press/release status correctly in `Main` and `FixedUpdate`.

--
 
Every run of `FixedUpdate` covers a specific timespan. For example, if
the fixed timestep `Δt` is 10ms, the first three `FixedUpdate` runs
cover `[0ms, 10ms)`, `[10ms, 20ms)`, and `[20ms, 30ms)`.
 
`FixedUpdate` can run many times in one frame. For truly
framerate-independent behavior, each `FixedUpdate` should only see the
events that occurred in its covered timespan, but what happens right now
is the first step in the frame reads all pending events.

Fixing that will require timestamped events.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Cameron 2023-11-26 15:04:41 -08:00 committed by Carter Anderson
parent 9cfc48145b
commit 0c199dd74d
3 changed files with 33 additions and 7 deletions

View File

@ -190,6 +190,7 @@ impl Default for App {
app.init_resource::<AppTypeRegistry>();
app.add_plugins(MainSchedulePlugin);
app.add_event::<AppExit>();
#[cfg(feature = "bevy_ci_testing")]

View File

@ -749,8 +749,30 @@ impl<'a, E: Event> ExactSizeIterator for EventIteratorWithId<'a, E> {
}
}
/// A system that calls [`Events::update`] once per frame.
pub fn event_update_system<T: Event>(mut events: ResMut<Events<T>>) {
#[doc(hidden)]
#[derive(Resource, Default)]
pub struct EventUpdateSignal(bool);
/// A system that queues a call to [`Events::update`].
pub fn event_queue_update_system(signal: Option<ResMut<EventUpdateSignal>>) {
if let Some(mut s) = signal {
s.0 = true;
}
}
/// A system that calls [`Events::update`].
pub fn event_update_system<T: Event>(
signal: Option<ResMut<EventUpdateSignal>>,
mut events: ResMut<Events<T>>,
) {
if let Some(mut s) = signal {
// If we haven't got a signal to update the events, but we *could* get such a signal
// return early and update the events later.
if !std::mem::replace(&mut s.0, false) {
return;
}
}
events.update();
}

View File

@ -19,11 +19,6 @@ pub use time::*;
pub use timer::*;
pub use virt::*;
use bevy_ecs::system::{Res, ResMut};
use bevy_utils::{tracing::warn, Duration, Instant};
pub use crossbeam_channel::TrySendError;
use crossbeam_channel::{Receiver, Sender};
pub mod prelude {
//! The Bevy Time Prelude.
#[doc(hidden)]
@ -31,7 +26,11 @@ pub mod prelude {
}
use bevy_app::{prelude::*, RunFixedUpdateLoop};
use bevy_ecs::event::{event_queue_update_system, EventUpdateSignal};
use bevy_ecs::prelude::*;
use bevy_utils::{tracing::warn, Duration, Instant};
pub use crossbeam_channel::TrySendError;
use crossbeam_channel::{Receiver, Sender};
/// Adds time functionality to Apps.
#[derive(Default)]
@ -61,6 +60,10 @@ impl Plugin for TimePlugin {
)
.add_systems(RunFixedUpdateLoop, run_fixed_update_schedule);
// ensure the events are not dropped until `FixedUpdate` systems can observe them
app.init_resource::<EventUpdateSignal>()
.add_systems(FixedUpdate, event_queue_update_system);
#[cfg(feature = "bevy_ci_testing")]
if let Some(ci_testing_config) = app
.world