Unify `FixedTime` and `Time` while fixing several problems (#8964)
# Objective
Current `FixedTime` and `Time` have several problems. This pull aims to
fix many of them at once.
- If there is a longer pause between app updates, time will jump forward
a lot at once and fixed time will iterate on `FixedUpdate` for a large
number of steps. If the pause is merely seconds, then this will just
mean jerkiness and possible unexpected behaviour in gameplay. If the
pause is hours/days as with OS suspend, the game will appear to freeze
until it has caught up with real time.
- If calculating a fixed step takes longer than specified fixed step
period, the game will enter a death spiral where rendering each frame
takes longer and longer due to more and more fixed step updates being
run per frame and the game appears to freeze.
- There is no way to see current fixed step elapsed time inside fixed
steps. In order to track this, the game designer needs to add a custom
system inside `FixedUpdate` that calculates elapsed or step count in a
resource.
- Access to delta time inside fixed step is `FixedStep::period` rather
than `Time::delta`. This, coupled with the issue that `Time::elapsed`
isn't available at all for fixed steps, makes it that time requiring
systems are either implemented to be run in `FixedUpdate` or `Update`,
but rarely work in both.
- Fixes #8800
- Fixes #8543
- Fixes #7439
- Fixes #5692
## Solution
- Create a generic `Time<T>` clock that has no processing logic but
which can be instantiated for multiple usages. This is also exposed for
users to add custom clocks.
- Create three standard clocks, `Time<Real>`, `Time<Virtual>` and
`Time<Fixed>`, all of which contain their individual logic.
- Create one "default" clock, which is just `Time` (or `Time<()>`),
which will be overwritten from `Time<Virtual>` on each update, and
`Time<Fixed>` inside `FixedUpdate` schedule. This way systems that do
not care specifically which time they track can work both in `Update`
and `FixedUpdate` without changes and the behaviour is intuitive.
- Add `max_delta` to virtual time update, which limits how much can be
added to virtual time by a single update. This fixes both the behaviour
after a long freeze, and also the death spiral by limiting how many
fixed timestep iterations there can be per update. Possible future work
could be adding `max_accumulator` to add a sort of "leaky bucket" time
processing to possibly smooth out jumps in time while keeping frame rate
stable.
- Many minor tweaks and clarifications to the time functions and their
documentation.
## Changelog
- `Time::raw_delta()`, `Time::raw_elapsed()` and related methods are
moved to `Time<Real>::delta()` and `Time<Real>::elapsed()` and now match
`Time` API
- `FixedTime` is now `Time<Fixed>` and matches `Time` API.
- `Time<Fixed>` default timestep is now 64 Hz, or 15625 microseconds.
- `Time` inside `FixedUpdate` now reflects fixed timestep time, making
systems portable between `Update ` and `FixedUpdate`.
- `Time::pause()`, `Time::set_relative_speed()` and related methods must
now be called as `Time<Virtual>::pause()` etc.
- There is a new `max_delta` setting in `Time<Virtual>` that limits how
much the clock can jump by a single update. The default value is 0.25
seconds.
- Removed `on_fixed_timer()` condition as `on_timer()` does the right
thing inside `FixedUpdate` now.
## Migration Guide
- Change all `Res<Time>` instances that access `raw_delta()`,
`raw_elapsed()` and related methods to `Res<Time<Real>>` and `delta()`,
`elapsed()`, etc.
- Change access to `period` from `Res<FixedTime>` to `Res<Time<Fixed>>`
and use `delta()`.
- The default timestep has been changed from 60 Hz to 64 Hz. If you wish
to restore the old behaviour, use
`app.insert_resource(Time::<Fixed>::from_hz(60.0))`.
- Change `app.insert_resource(FixedTime::new(duration))` to
`app.insert_resource(Time::<Fixed>::from_duration(duration))`
- Change `app.insert_resource(FixedTime::new_from_secs(secs))` to
`app.insert_resource(Time::<Fixed>::from_seconds(secs))`
- Change `system.on_fixed_timer(duration)` to
`system.on_timer(duration)`. Timers in systems placed in `FixedUpdate`
schedule automatically use the fixed time clock.
- Change `ResMut<Time>` calls to `pause()`, `is_paused()`,
`set_relative_speed()` and related methods to `ResMut<Time<Virtual>>`
calls. The API is the same, with the exception that `relative_speed()`
will return the actual last ste relative speed, while
`effective_relative_speed()` returns 0.0 if the time is paused and
corresponds to the speed that was set when the update for the current
frame started.
## Todo
- [x] Update pull name and description
- [x] Top level documentation on usage
- [x] Fix examples
- [x] Decide on default `max_delta` value
- [x] Decide naming of the three clocks: is `Real`, `Virtual`, `Fixed`
good?
- [x] Decide if the three clock inner structures should be in prelude
- [x] Decide on best way to configure values at startup: is manually
inserting a new clock instance okay, or should there be config struct
separately?
- [x] Fix links in docs
- [x] Decide what should be public and what not
- [x] Decide how `wrap_period` should be handled when it is changed
- [x] ~~Add toggles to disable setting the clock as default?~~ No,
separate pull if needed.
- [x] Add tests
- [x] Reformat, ensure adheres to conventions etc.
- [x] Build documentation and see that it looks correct
## Contributors
Huge thanks to @alice-i-cecile and @maniwani while building this pull.
It was a shared effort!
---------
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Cameron <51241057+maniwani@users.noreply.github.com>
Co-authored-by: Jerome Humbert <djeedai@gmail.com>
2023-10-16 01:57:55 +00:00
|
|
|
use bevy_ecs::system::{Res, ResMut};
|
|
|
|
use bevy_reflect::Reflect;
|
2023-10-16 19:18:39 +00:00
|
|
|
use bevy_utils::{tracing::debug, Duration};
|
Unify `FixedTime` and `Time` while fixing several problems (#8964)
# Objective
Current `FixedTime` and `Time` have several problems. This pull aims to
fix many of them at once.
- If there is a longer pause between app updates, time will jump forward
a lot at once and fixed time will iterate on `FixedUpdate` for a large
number of steps. If the pause is merely seconds, then this will just
mean jerkiness and possible unexpected behaviour in gameplay. If the
pause is hours/days as with OS suspend, the game will appear to freeze
until it has caught up with real time.
- If calculating a fixed step takes longer than specified fixed step
period, the game will enter a death spiral where rendering each frame
takes longer and longer due to more and more fixed step updates being
run per frame and the game appears to freeze.
- There is no way to see current fixed step elapsed time inside fixed
steps. In order to track this, the game designer needs to add a custom
system inside `FixedUpdate` that calculates elapsed or step count in a
resource.
- Access to delta time inside fixed step is `FixedStep::period` rather
than `Time::delta`. This, coupled with the issue that `Time::elapsed`
isn't available at all for fixed steps, makes it that time requiring
systems are either implemented to be run in `FixedUpdate` or `Update`,
but rarely work in both.
- Fixes #8800
- Fixes #8543
- Fixes #7439
- Fixes #5692
## Solution
- Create a generic `Time<T>` clock that has no processing logic but
which can be instantiated for multiple usages. This is also exposed for
users to add custom clocks.
- Create three standard clocks, `Time<Real>`, `Time<Virtual>` and
`Time<Fixed>`, all of which contain their individual logic.
- Create one "default" clock, which is just `Time` (or `Time<()>`),
which will be overwritten from `Time<Virtual>` on each update, and
`Time<Fixed>` inside `FixedUpdate` schedule. This way systems that do
not care specifically which time they track can work both in `Update`
and `FixedUpdate` without changes and the behaviour is intuitive.
- Add `max_delta` to virtual time update, which limits how much can be
added to virtual time by a single update. This fixes both the behaviour
after a long freeze, and also the death spiral by limiting how many
fixed timestep iterations there can be per update. Possible future work
could be adding `max_accumulator` to add a sort of "leaky bucket" time
processing to possibly smooth out jumps in time while keeping frame rate
stable.
- Many minor tweaks and clarifications to the time functions and their
documentation.
## Changelog
- `Time::raw_delta()`, `Time::raw_elapsed()` and related methods are
moved to `Time<Real>::delta()` and `Time<Real>::elapsed()` and now match
`Time` API
- `FixedTime` is now `Time<Fixed>` and matches `Time` API.
- `Time<Fixed>` default timestep is now 64 Hz, or 15625 microseconds.
- `Time` inside `FixedUpdate` now reflects fixed timestep time, making
systems portable between `Update ` and `FixedUpdate`.
- `Time::pause()`, `Time::set_relative_speed()` and related methods must
now be called as `Time<Virtual>::pause()` etc.
- There is a new `max_delta` setting in `Time<Virtual>` that limits how
much the clock can jump by a single update. The default value is 0.25
seconds.
- Removed `on_fixed_timer()` condition as `on_timer()` does the right
thing inside `FixedUpdate` now.
## Migration Guide
- Change all `Res<Time>` instances that access `raw_delta()`,
`raw_elapsed()` and related methods to `Res<Time<Real>>` and `delta()`,
`elapsed()`, etc.
- Change access to `period` from `Res<FixedTime>` to `Res<Time<Fixed>>`
and use `delta()`.
- The default timestep has been changed from 60 Hz to 64 Hz. If you wish
to restore the old behaviour, use
`app.insert_resource(Time::<Fixed>::from_hz(60.0))`.
- Change `app.insert_resource(FixedTime::new(duration))` to
`app.insert_resource(Time::<Fixed>::from_duration(duration))`
- Change `app.insert_resource(FixedTime::new_from_secs(secs))` to
`app.insert_resource(Time::<Fixed>::from_seconds(secs))`
- Change `system.on_fixed_timer(duration)` to
`system.on_timer(duration)`. Timers in systems placed in `FixedUpdate`
schedule automatically use the fixed time clock.
- Change `ResMut<Time>` calls to `pause()`, `is_paused()`,
`set_relative_speed()` and related methods to `ResMut<Time<Virtual>>`
calls. The API is the same, with the exception that `relative_speed()`
will return the actual last ste relative speed, while
`effective_relative_speed()` returns 0.0 if the time is paused and
corresponds to the speed that was set when the update for the current
frame started.
## Todo
- [x] Update pull name and description
- [x] Top level documentation on usage
- [x] Fix examples
- [x] Decide on default `max_delta` value
- [x] Decide naming of the three clocks: is `Real`, `Virtual`, `Fixed`
good?
- [x] Decide if the three clock inner structures should be in prelude
- [x] Decide on best way to configure values at startup: is manually
inserting a new clock instance okay, or should there be config struct
separately?
- [x] Fix links in docs
- [x] Decide what should be public and what not
- [x] Decide how `wrap_period` should be handled when it is changed
- [x] ~~Add toggles to disable setting the clock as default?~~ No,
separate pull if needed.
- [x] Add tests
- [x] Reformat, ensure adheres to conventions etc.
- [x] Build documentation and see that it looks correct
## Contributors
Huge thanks to @alice-i-cecile and @maniwani while building this pull.
It was a shared effort!
---------
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Cameron <51241057+maniwani@users.noreply.github.com>
Co-authored-by: Jerome Humbert <djeedai@gmail.com>
2023-10-16 01:57:55 +00:00
|
|
|
|
|
|
|
use crate::{real::Real, time::Time};
|
|
|
|
|
|
|
|
/// The virtual game clock representing game time.
|
|
|
|
///
|
|
|
|
/// A specialization of the [`Time`] structure. **For method documentation, see
|
|
|
|
/// [`Time<Virtual>#impl-Time<Virtual>`].**
|
|
|
|
///
|
|
|
|
/// Normally used as `Time<Virtual>`. It is automatically inserted as a resource
|
|
|
|
/// by [`TimePlugin`](crate::TimePlugin) and updated based on
|
|
|
|
/// [`Time<Real>`](Real). The virtual clock is automatically set as the default
|
|
|
|
/// generic [`Time`] resource for the update.
|
|
|
|
///
|
|
|
|
/// The virtual clock differs from real time clock in that it can be paused, sped up
|
|
|
|
/// and slowed down. It also limits how much it can advance in a single update
|
|
|
|
/// in order to prevent unexpected behavior in cases where updates do not happen
|
|
|
|
/// at regular intervals (e.g. coming back after the program was suspended a long time).
|
|
|
|
///
|
|
|
|
/// The virtual clock can be paused by calling [`pause()`](Time::pause) and
|
|
|
|
/// unpaused by calling [`unpause()`](Time::unpause). When the game clock is
|
|
|
|
/// paused [`delta()`](Time::delta) will be zero on each update, and
|
|
|
|
/// [`elapsed()`](Time::elapsed) will not grow.
|
|
|
|
/// [`effective_speed()`](Time::effective_speed) will return `0.0`. Calling
|
|
|
|
/// [`pause()`](Time::pause) will not affect value the [`delta()`](Time::delta)
|
|
|
|
/// value for the update currently being processed.
|
|
|
|
///
|
|
|
|
/// The speed of the virtual clock can be changed by calling
|
|
|
|
/// [`set_relative_speed()`](Time::set_relative_speed). A value of `2.0` means
|
|
|
|
/// that virtual clock should advance twice as fast as real time, meaning that
|
|
|
|
/// [`delta()`](Time::delta) values will be double of what
|
|
|
|
/// [`Time<Real>::delta()`](Time::delta) reports and
|
|
|
|
/// [`elapsed()`](Time::elapsed) will go twice as fast as
|
|
|
|
/// [`Time<Real>::elapsed()`](Time::elapsed). Calling
|
|
|
|
/// [`set_relative_speed()`](Time::set_relative_speed) will not affect the
|
|
|
|
/// [`delta()`](Time::delta) value for the update currently being processed.
|
|
|
|
///
|
|
|
|
/// The maximum amount of delta time that can be added by a single update can be
|
|
|
|
/// set by [`set_max_delta()`](Time::set_max_delta). This value serves a dual
|
|
|
|
/// purpose in the virtual clock.
|
|
|
|
///
|
|
|
|
/// If the game temporarily freezes due to any reason, such as disk access, a
|
|
|
|
/// blocking system call, or operating system level suspend, reporting the full
|
|
|
|
/// elapsed delta time is likely to cause bugs in game logic. Usually if a
|
|
|
|
/// laptop is suspended for an hour, it doesn't make sense to try to simulate
|
|
|
|
/// the game logic for the elapsed hour when resuming. Instead it is better to
|
|
|
|
/// lose the extra time and pretend a shorter duration of time passed. Setting
|
|
|
|
/// [`max_delta()`](Time::max_delta) to a relatively short time means that the
|
|
|
|
/// impact on game logic will be minimal.
|
|
|
|
///
|
|
|
|
/// If the game lags for some reason, meaning that it will take a longer time to
|
|
|
|
/// compute a frame than the real time that passes during the computation, then
|
|
|
|
/// we would fall behind in processing virtual time. If this situation persists,
|
|
|
|
/// and computing a frame takes longer depending on how much virtual time has
|
|
|
|
/// passed, the game would enter a "death spiral" where computing each frame
|
|
|
|
/// takes longer and longer and the game will appear to freeze. By limiting the
|
|
|
|
/// maximum time that can be added at once, we also limit the amount of virtual
|
|
|
|
/// time the game needs to compute for each frame. This means that the game will
|
|
|
|
/// run slow, and it will run slower than real time, but it will not freeze and
|
|
|
|
/// it will recover as soon as computation becomes fast again.
|
|
|
|
///
|
|
|
|
/// You should set [`max_delta()`](Time::max_delta) to a value that is
|
|
|
|
/// approximately the minimum FPS your game should have even if heavily lagged
|
|
|
|
/// for a moment. The actual FPS when lagged will be somewhat lower than this,
|
|
|
|
/// depending on how much more time it takes to compute a frame compared to real
|
|
|
|
/// time. You should also consider how stable your FPS is, as the limit will
|
|
|
|
/// also dictate how big of an FPS drop you can accept without losing time and
|
|
|
|
/// falling behind real time.
|
|
|
|
#[derive(Debug, Copy, Clone, Reflect)]
|
|
|
|
pub struct Virtual {
|
|
|
|
max_delta: Duration,
|
|
|
|
paused: bool,
|
|
|
|
relative_speed: f64,
|
|
|
|
effective_speed: f64,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Time<Virtual> {
|
|
|
|
/// The default amount of time that can added in a single update.
|
|
|
|
///
|
|
|
|
/// Equal to 250 milliseconds.
|
|
|
|
const DEFAULT_MAX_DELTA: Duration = Duration::from_millis(250);
|
|
|
|
|
|
|
|
/// Create new virtual clock with given maximum delta step [`Duration`]
|
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
///
|
|
|
|
/// Panics if `max_delta` is zero.
|
|
|
|
pub fn from_max_delta(max_delta: Duration) -> Self {
|
|
|
|
let mut ret = Self::default();
|
|
|
|
ret.set_max_delta(max_delta);
|
|
|
|
ret
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the maximum amount of time that can be added to this clock by a
|
|
|
|
/// single update, as [`Duration`].
|
|
|
|
///
|
|
|
|
/// This is the maximum value [`Self::delta()`] will return and also to
|
|
|
|
/// maximum time [`Self::elapsed()`] will be increased by in a single
|
|
|
|
/// update.
|
|
|
|
///
|
|
|
|
/// This ensures that even if no updates happen for an extended amount of time,
|
|
|
|
/// the clock will not have a sudden, huge advance all at once. This also indirectly
|
|
|
|
/// limits the maximum number of fixed update steps that can run in a single update.
|
|
|
|
///
|
|
|
|
/// The default value is 250 milliseconds.
|
|
|
|
#[inline]
|
|
|
|
pub fn max_delta(&self) -> Duration {
|
|
|
|
self.context().max_delta
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Sets the maximum amount of time that can be added to this clock by a
|
|
|
|
/// single update, as [`Duration`].
|
|
|
|
///
|
|
|
|
/// This is the maximum value [`Self::delta()`] will return and also to
|
|
|
|
/// maximum time [`Self::elapsed()`] will be increased by in a single
|
|
|
|
/// update.
|
|
|
|
///
|
|
|
|
/// This is used to ensure that even if the game freezes for a few seconds,
|
|
|
|
/// or is suspended for hours or even days, the virtual clock doesn't
|
|
|
|
/// suddenly jump forward for that full amount, which would likely cause
|
|
|
|
/// gameplay bugs or having to suddenly simulate all the intervening time.
|
|
|
|
///
|
|
|
|
/// If no updates happen for an extended amount of time, this limit prevents
|
|
|
|
/// having a sudden, huge advance all at once. This also indirectly limits
|
|
|
|
/// the maximum number of fixed update steps that can run in a single
|
|
|
|
/// update.
|
|
|
|
///
|
|
|
|
/// The default value is 250 milliseconds. If you want to disable this
|
|
|
|
/// feature, set the value to [`Duration::MAX`].
|
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
///
|
|
|
|
/// Panics if `max_delta` is zero.
|
|
|
|
#[inline]
|
|
|
|
pub fn set_max_delta(&mut self, max_delta: Duration) {
|
|
|
|
assert_ne!(max_delta, Duration::ZERO, "tried to set max delta to zero");
|
|
|
|
self.context_mut().max_delta = max_delta;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the speed the clock advances relative to your system clock, as [`f32`].
|
|
|
|
/// This is known as "time scaling" or "time dilation" in other engines.
|
|
|
|
#[inline]
|
|
|
|
pub fn relative_speed(&self) -> f32 {
|
|
|
|
self.relative_speed_f64() as f32
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the speed the clock advances relative to your system clock, as [`f64`].
|
|
|
|
/// This is known as "time scaling" or "time dilation" in other engines.
|
|
|
|
#[inline]
|
|
|
|
pub fn relative_speed_f64(&self) -> f64 {
|
|
|
|
self.context().relative_speed
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the speed the clock advanced relative to your system clock in
|
|
|
|
/// this update, as [`f32`].
|
|
|
|
///
|
|
|
|
/// Returns `0.0` if the game was paused or what the `relative_speed` value
|
|
|
|
/// was at the start of this update.
|
|
|
|
#[inline]
|
|
|
|
pub fn effective_speed(&self) -> f32 {
|
|
|
|
self.context().effective_speed as f32
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the speed the clock advanced relative to your system clock in
|
|
|
|
/// this update, as [`f64`].
|
|
|
|
///
|
|
|
|
/// Returns `0.0` if the game was paused or what the `relative_speed` value
|
|
|
|
/// was at the start of this update.
|
|
|
|
#[inline]
|
|
|
|
pub fn effective_speed_f64(&self) -> f64 {
|
|
|
|
self.context().effective_speed
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Sets the speed the clock advances relative to your system clock, given as an [`f32`].
|
|
|
|
///
|
|
|
|
/// For example, setting this to `2.0` will make the clock advance twice as fast as your system
|
|
|
|
/// clock.
|
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
///
|
|
|
|
/// Panics if `ratio` is negative or not finite.
|
|
|
|
#[inline]
|
|
|
|
pub fn set_relative_speed(&mut self, ratio: f32) {
|
|
|
|
self.set_relative_speed_f64(ratio as f64);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Sets the speed the clock advances relative to your system clock, given as an [`f64`].
|
|
|
|
///
|
|
|
|
/// For example, setting this to `2.0` will make the clock advance twice as fast as your system
|
|
|
|
/// clock.
|
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
///
|
|
|
|
/// Panics if `ratio` is negative or not finite.
|
|
|
|
#[inline]
|
|
|
|
pub fn set_relative_speed_f64(&mut self, ratio: f64) {
|
|
|
|
assert!(ratio.is_finite(), "tried to go infinitely fast");
|
|
|
|
assert!(ratio >= 0.0, "tried to go back in time");
|
|
|
|
self.context_mut().relative_speed = ratio;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Stops the clock, preventing it from advancing until resumed.
|
|
|
|
#[inline]
|
|
|
|
pub fn pause(&mut self) {
|
|
|
|
self.context_mut().paused = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Resumes the clock if paused.
|
|
|
|
#[inline]
|
|
|
|
pub fn unpause(&mut self) {
|
|
|
|
self.context_mut().paused = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns `true` if the clock is currently paused.
|
|
|
|
#[inline]
|
|
|
|
pub fn is_paused(&self) -> bool {
|
|
|
|
self.context().paused
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns `true` if the clock was paused at the start of this update.
|
|
|
|
#[inline]
|
|
|
|
pub fn was_paused(&self) -> bool {
|
|
|
|
self.context().effective_speed == 0.0
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Updates the elapsed duration of `self` by `raw_delta`, up to the `max_delta`.
|
|
|
|
fn advance_with_raw_delta(&mut self, raw_delta: Duration) {
|
|
|
|
let max_delta = self.context().max_delta;
|
|
|
|
let clamped_delta = if raw_delta > max_delta {
|
2023-10-16 19:18:39 +00:00
|
|
|
debug!(
|
Unify `FixedTime` and `Time` while fixing several problems (#8964)
# Objective
Current `FixedTime` and `Time` have several problems. This pull aims to
fix many of them at once.
- If there is a longer pause between app updates, time will jump forward
a lot at once and fixed time will iterate on `FixedUpdate` for a large
number of steps. If the pause is merely seconds, then this will just
mean jerkiness and possible unexpected behaviour in gameplay. If the
pause is hours/days as with OS suspend, the game will appear to freeze
until it has caught up with real time.
- If calculating a fixed step takes longer than specified fixed step
period, the game will enter a death spiral where rendering each frame
takes longer and longer due to more and more fixed step updates being
run per frame and the game appears to freeze.
- There is no way to see current fixed step elapsed time inside fixed
steps. In order to track this, the game designer needs to add a custom
system inside `FixedUpdate` that calculates elapsed or step count in a
resource.
- Access to delta time inside fixed step is `FixedStep::period` rather
than `Time::delta`. This, coupled with the issue that `Time::elapsed`
isn't available at all for fixed steps, makes it that time requiring
systems are either implemented to be run in `FixedUpdate` or `Update`,
but rarely work in both.
- Fixes #8800
- Fixes #8543
- Fixes #7439
- Fixes #5692
## Solution
- Create a generic `Time<T>` clock that has no processing logic but
which can be instantiated for multiple usages. This is also exposed for
users to add custom clocks.
- Create three standard clocks, `Time<Real>`, `Time<Virtual>` and
`Time<Fixed>`, all of which contain their individual logic.
- Create one "default" clock, which is just `Time` (or `Time<()>`),
which will be overwritten from `Time<Virtual>` on each update, and
`Time<Fixed>` inside `FixedUpdate` schedule. This way systems that do
not care specifically which time they track can work both in `Update`
and `FixedUpdate` without changes and the behaviour is intuitive.
- Add `max_delta` to virtual time update, which limits how much can be
added to virtual time by a single update. This fixes both the behaviour
after a long freeze, and also the death spiral by limiting how many
fixed timestep iterations there can be per update. Possible future work
could be adding `max_accumulator` to add a sort of "leaky bucket" time
processing to possibly smooth out jumps in time while keeping frame rate
stable.
- Many minor tweaks and clarifications to the time functions and their
documentation.
## Changelog
- `Time::raw_delta()`, `Time::raw_elapsed()` and related methods are
moved to `Time<Real>::delta()` and `Time<Real>::elapsed()` and now match
`Time` API
- `FixedTime` is now `Time<Fixed>` and matches `Time` API.
- `Time<Fixed>` default timestep is now 64 Hz, or 15625 microseconds.
- `Time` inside `FixedUpdate` now reflects fixed timestep time, making
systems portable between `Update ` and `FixedUpdate`.
- `Time::pause()`, `Time::set_relative_speed()` and related methods must
now be called as `Time<Virtual>::pause()` etc.
- There is a new `max_delta` setting in `Time<Virtual>` that limits how
much the clock can jump by a single update. The default value is 0.25
seconds.
- Removed `on_fixed_timer()` condition as `on_timer()` does the right
thing inside `FixedUpdate` now.
## Migration Guide
- Change all `Res<Time>` instances that access `raw_delta()`,
`raw_elapsed()` and related methods to `Res<Time<Real>>` and `delta()`,
`elapsed()`, etc.
- Change access to `period` from `Res<FixedTime>` to `Res<Time<Fixed>>`
and use `delta()`.
- The default timestep has been changed from 60 Hz to 64 Hz. If you wish
to restore the old behaviour, use
`app.insert_resource(Time::<Fixed>::from_hz(60.0))`.
- Change `app.insert_resource(FixedTime::new(duration))` to
`app.insert_resource(Time::<Fixed>::from_duration(duration))`
- Change `app.insert_resource(FixedTime::new_from_secs(secs))` to
`app.insert_resource(Time::<Fixed>::from_seconds(secs))`
- Change `system.on_fixed_timer(duration)` to
`system.on_timer(duration)`. Timers in systems placed in `FixedUpdate`
schedule automatically use the fixed time clock.
- Change `ResMut<Time>` calls to `pause()`, `is_paused()`,
`set_relative_speed()` and related methods to `ResMut<Time<Virtual>>`
calls. The API is the same, with the exception that `relative_speed()`
will return the actual last ste relative speed, while
`effective_relative_speed()` returns 0.0 if the time is paused and
corresponds to the speed that was set when the update for the current
frame started.
## Todo
- [x] Update pull name and description
- [x] Top level documentation on usage
- [x] Fix examples
- [x] Decide on default `max_delta` value
- [x] Decide naming of the three clocks: is `Real`, `Virtual`, `Fixed`
good?
- [x] Decide if the three clock inner structures should be in prelude
- [x] Decide on best way to configure values at startup: is manually
inserting a new clock instance okay, or should there be config struct
separately?
- [x] Fix links in docs
- [x] Decide what should be public and what not
- [x] Decide how `wrap_period` should be handled when it is changed
- [x] ~~Add toggles to disable setting the clock as default?~~ No,
separate pull if needed.
- [x] Add tests
- [x] Reformat, ensure adheres to conventions etc.
- [x] Build documentation and see that it looks correct
## Contributors
Huge thanks to @alice-i-cecile and @maniwani while building this pull.
It was a shared effort!
---------
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Cameron <51241057+maniwani@users.noreply.github.com>
Co-authored-by: Jerome Humbert <djeedai@gmail.com>
2023-10-16 01:57:55 +00:00
|
|
|
"delta time larger than maximum delta, clamping delta to {:?} and skipping {:?}",
|
|
|
|
max_delta,
|
|
|
|
raw_delta - max_delta
|
|
|
|
);
|
|
|
|
max_delta
|
|
|
|
} else {
|
|
|
|
raw_delta
|
|
|
|
};
|
|
|
|
let effective_speed = if self.context().paused {
|
|
|
|
0.0
|
|
|
|
} else {
|
|
|
|
self.context().relative_speed
|
|
|
|
};
|
|
|
|
let delta = if effective_speed != 1.0 {
|
|
|
|
clamped_delta.mul_f64(effective_speed)
|
|
|
|
} else {
|
|
|
|
// avoid rounding when at normal speed
|
|
|
|
clamped_delta
|
|
|
|
};
|
|
|
|
self.context_mut().effective_speed = effective_speed;
|
|
|
|
self.advance_by(delta);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Virtual {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
|
|
|
max_delta: Time::<Virtual>::DEFAULT_MAX_DELTA,
|
|
|
|
paused: false,
|
|
|
|
relative_speed: 1.0,
|
|
|
|
effective_speed: 1.0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Advances [`Time<Virtual>`] and [`Time`] based on the elapsed [`Time<Real>`].
|
|
|
|
///
|
|
|
|
/// The virtual time will be advanced up to the provided [`Time::max_delta`].
|
|
|
|
pub fn virtual_time_system(
|
|
|
|
mut current: ResMut<Time>,
|
|
|
|
mut virt: ResMut<Time<Virtual>>,
|
|
|
|
real: Res<Time<Real>>,
|
|
|
|
) {
|
|
|
|
let raw_delta = real.delta();
|
|
|
|
virt.advance_with_raw_delta(raw_delta);
|
|
|
|
*current = virt.as_generic();
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_default() {
|
|
|
|
let time = Time::<Virtual>::default();
|
|
|
|
|
|
|
|
assert!(!time.is_paused()); // false
|
|
|
|
assert_eq!(time.relative_speed(), 1.0);
|
|
|
|
assert_eq!(time.max_delta(), Time::<Virtual>::DEFAULT_MAX_DELTA);
|
|
|
|
assert_eq!(time.delta(), Duration::ZERO);
|
|
|
|
assert_eq!(time.elapsed(), Duration::ZERO);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_advance() {
|
|
|
|
let mut time = Time::<Virtual>::default();
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(125));
|
|
|
|
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(125));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(125));
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(125));
|
|
|
|
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(125));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(250));
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(125));
|
|
|
|
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(125));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(375));
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(125));
|
|
|
|
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(125));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(500));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_relative_speed() {
|
|
|
|
let mut time = Time::<Virtual>::default();
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(250));
|
|
|
|
|
|
|
|
assert_eq!(time.relative_speed(), 1.0);
|
|
|
|
assert_eq!(time.effective_speed(), 1.0);
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(250));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(250));
|
|
|
|
|
|
|
|
time.set_relative_speed_f64(2.0);
|
|
|
|
|
|
|
|
assert_eq!(time.relative_speed(), 2.0);
|
|
|
|
assert_eq!(time.effective_speed(), 1.0);
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(250));
|
|
|
|
|
|
|
|
assert_eq!(time.relative_speed(), 2.0);
|
|
|
|
assert_eq!(time.effective_speed(), 2.0);
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(500));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(750));
|
|
|
|
|
|
|
|
time.set_relative_speed_f64(0.5);
|
|
|
|
|
|
|
|
assert_eq!(time.relative_speed(), 0.5);
|
|
|
|
assert_eq!(time.effective_speed(), 2.0);
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(250));
|
|
|
|
|
|
|
|
assert_eq!(time.relative_speed(), 0.5);
|
|
|
|
assert_eq!(time.effective_speed(), 0.5);
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(125));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(875));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_pause() {
|
|
|
|
let mut time = Time::<Virtual>::default();
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(250));
|
|
|
|
|
|
|
|
assert!(!time.is_paused()); // false
|
|
|
|
assert!(!time.was_paused()); // false
|
|
|
|
assert_eq!(time.relative_speed(), 1.0);
|
|
|
|
assert_eq!(time.effective_speed(), 1.0);
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(250));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(250));
|
|
|
|
|
|
|
|
time.pause();
|
|
|
|
|
|
|
|
assert!(time.is_paused()); // true
|
|
|
|
assert!(!time.was_paused()); // false
|
|
|
|
assert_eq!(time.relative_speed(), 1.0);
|
|
|
|
assert_eq!(time.effective_speed(), 1.0);
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(250));
|
|
|
|
|
|
|
|
assert!(time.is_paused()); // true
|
|
|
|
assert!(time.was_paused()); // true
|
|
|
|
assert_eq!(time.relative_speed(), 1.0);
|
|
|
|
assert_eq!(time.effective_speed(), 0.0);
|
|
|
|
assert_eq!(time.delta(), Duration::ZERO);
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(250));
|
|
|
|
|
|
|
|
time.unpause();
|
|
|
|
|
|
|
|
assert!(!time.is_paused()); // false
|
|
|
|
assert!(time.was_paused()); // true
|
|
|
|
assert_eq!(time.relative_speed(), 1.0);
|
|
|
|
assert_eq!(time.effective_speed(), 0.0);
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(250));
|
|
|
|
|
|
|
|
assert!(!time.is_paused()); // false
|
|
|
|
assert!(!time.was_paused()); // false
|
|
|
|
assert_eq!(time.relative_speed(), 1.0);
|
|
|
|
assert_eq!(time.effective_speed(), 1.0);
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(250));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(500));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_max_delta() {
|
|
|
|
let mut time = Time::<Virtual>::default();
|
|
|
|
time.set_max_delta(Duration::from_millis(500));
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(250));
|
|
|
|
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(250));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(250));
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(500));
|
|
|
|
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(500));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(750));
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(750));
|
|
|
|
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(500));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(1250));
|
|
|
|
|
|
|
|
time.set_max_delta(Duration::from_secs(1));
|
|
|
|
|
|
|
|
assert_eq!(time.max_delta(), Duration::from_secs(1));
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(750));
|
|
|
|
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(750));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(2000));
|
|
|
|
|
|
|
|
time.advance_with_raw_delta(Duration::from_millis(1250));
|
|
|
|
|
|
|
|
assert_eq!(time.delta(), Duration::from_millis(1000));
|
|
|
|
assert_eq!(time.elapsed(), Duration::from_millis(3000));
|
|
|
|
}
|
|
|
|
}
|