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>
This commit is contained in:
Nuutti Kotivuori 2023-10-16 04:57:55 +03:00 committed by GitHub
parent 02943737b2
commit 3d79dc4cdc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1602 additions and 871 deletions

View file

@ -1398,6 +1398,16 @@ description = "Illustrates creating custom system parameters with `SystemParam`"
category = "ECS (Entity Component System)"
wasm = false
[[example]]
name = "time"
path = "examples/ecs/time.rs"
[package.metadata.example.time]
name = "Time handling"
description = "Explains how Time is handled in ECS"
category = "ECS (Entity Component System)"
wasm = false
[[example]]
name = "timers"
path = "examples/ecs/timers.rs"

View file

@ -2,7 +2,7 @@ use crate::{Diagnostic, DiagnosticId, Diagnostics, RegisterDiagnostic};
use bevy_app::prelude::*;
use bevy_core::FrameCount;
use bevy_ecs::prelude::*;
use bevy_time::Time;
use bevy_time::{Real, Time};
/// Adds "frame time" diagnostic to an App, specifically "frame time", "fps" and "frame count"
#[derive(Default)]
@ -30,12 +30,12 @@ impl FrameTimeDiagnosticsPlugin {
pub fn diagnostic_system(
mut diagnostics: Diagnostics,
time: Res<Time>,
time: Res<Time<Real>>,
frame_count: Res<FrameCount>,
) {
diagnostics.add_measurement(Self::FRAME_COUNT, || frame_count.0 as f64);
let delta_seconds = time.raw_delta_seconds_f64();
let delta_seconds = time.delta_seconds_f64();
if delta_seconds == 0.0 {
return;
}

View file

@ -2,7 +2,7 @@ use super::{Diagnostic, DiagnosticId, DiagnosticsStore};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_log::{debug, info};
use bevy_time::{Time, Timer, TimerMode};
use bevy_time::{Real, Time, Timer, TimerMode};
use bevy_utils::Duration;
/// An App Plugin that logs diagnostics to the console
@ -82,10 +82,10 @@ impl LogDiagnosticsPlugin {
fn log_diagnostics_system(
mut state: ResMut<LogDiagnosticsState>,
time: Res<Time>,
time: Res<Time<Real>>,
diagnostics: Res<DiagnosticsStore>,
) {
if state.timer.tick(time.raw_delta()).finished() {
if state.timer.tick(time.delta()).finished() {
if let Some(ref filter) = state.filter {
for diagnostic in filter.iter().flat_map(|id| {
diagnostics
@ -107,10 +107,10 @@ impl LogDiagnosticsPlugin {
fn log_diagnostics_debug_system(
mut state: ResMut<LogDiagnosticsState>,
time: Res<Time>,
time: Res<Time<Real>>,
diagnostics: Res<DiagnosticsStore>,
) {
if state.timer.tick(time.raw_delta()).finished() {
if state.timer.tick(time.delta()).finished() {
if let Some(ref filter) = state.filter {
for diagnostic in filter.iter().flat_map(|id| {
diagnostics

View file

@ -5,7 +5,7 @@ use bevy_ecs::{
};
use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest};
use bevy_log::{debug, warn};
use bevy_time::Time;
use bevy_time::{Real, Time};
use bevy_utils::{Duration, HashMap};
use gilrs::{
ff::{self, BaseEffect, BaseEffectType, Repeat, Replay},
@ -120,12 +120,12 @@ fn handle_rumble_request(
Ok(())
}
pub(crate) fn play_gilrs_rumble(
time: Res<Time>,
time: Res<Time<Real>>,
mut gilrs: NonSendMut<Gilrs>,
mut requests: EventReader<GamepadRumbleRequest>,
mut running_rumbles: NonSendMut<RunningRumbleEffects>,
) {
let current_time = time.raw_elapsed();
let current_time = time.elapsed();
// Remove outdated rumble effects.
for rumbles in running_rumbles.rumbles.values_mut() {
// `ff::Effect` uses RAII, dropping = deactivating

View file

@ -39,7 +39,7 @@ fn extract_frame_count(mut commands: Commands, frame_count: Extract<Res<FrameCou
}
fn extract_time(mut commands: Commands, time: Extract<Res<Time>>) {
commands.insert_resource(time.clone());
commands.insert_resource(**time);
}
/// Contains global values useful when writing shaders.

View file

@ -1,12 +1,10 @@
use crate::{fixed_timestep::FixedTime, Time, Timer, TimerMode};
use crate::{Time, Timer, TimerMode};
use bevy_ecs::system::Res;
use bevy_utils::Duration;
/// Run condition that is active on a regular time interval, using [`Time`] to advance
/// the timer.
///
/// If used for a fixed timestep system, use [`on_fixed_timer`] instead.
///
/// ```rust,no_run
/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup, Update};
/// # use bevy_ecs::schedule::IntoSystemConfigs;
@ -40,40 +38,6 @@ pub fn on_timer(duration: Duration) -> impl FnMut(Res<Time>) -> bool + Clone {
}
}
/// Run condition that is active on a regular time interval, using [`FixedTime`] to
/// advance the timer.
///
/// If used for a non-fixed timestep system, use [`on_timer`] instead.
///
/// ```rust,no_run
/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup, FixedUpdate};
/// # use bevy_ecs::schedule::IntoSystemConfigs;
/// # use bevy_utils::Duration;
/// # use bevy_time::common_conditions::on_fixed_timer;
/// fn main() {
/// App::new()
/// .add_plugins(DefaultPlugins)
/// .add_systems(FixedUpdate,
/// tick.run_if(on_fixed_timer(Duration::from_secs(1))),
/// )
/// .run();
/// }
/// fn tick() {
/// // ran once a second
/// }
/// ```
///
/// Note that this run condition may not behave as expected if `duration` is smaller
/// than the fixed timestep period, since the timer may complete multiple times in
/// one fixed update.
pub fn on_fixed_timer(duration: Duration) -> impl FnMut(Res<FixedTime>) -> bool + Clone {
let mut timer = Timer::new(duration, TimerMode::Repeating);
move |time: Res<FixedTime>| {
timer.tick(time.period);
timer.just_finished()
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -85,9 +49,7 @@ mod tests {
#[test]
fn distributive_run_if_compiles() {
Schedule::default().add_systems(
(test_system, test_system)
.distributive_run_if(on_timer(Duration::new(1, 0)))
.distributive_run_if(on_fixed_timer(Duration::new(1, 0))),
(test_system, test_system).distributive_run_if(on_timer(Duration::new(1, 0))),
);
}
}

View file

@ -0,0 +1,343 @@
use bevy_ecs::world::World;
use bevy_reflect::Reflect;
use bevy_utils::Duration;
use crate::{time::Time, virt::Virtual, FixedUpdate};
/// The fixed timestep game clock following virtual time.
///
/// A specialization of the [`Time`] structure. **For method documentation, see
/// [`Time<Fixed>#impl-Time<Fixed>`].**
///
/// It is automatically inserted as a resource by
/// [`TimePlugin`](crate::TimePlugin) and updated based on
/// [`Time<Virtual>`](Virtual). The fixed clock is automatically set as the
/// generic [`Time`] resource during [`FixedUpdate`] schedule processing.
///
/// The fixed timestep clock advances in fixed-size increments, which is
/// extremely useful for writing logic (like physics) that should have
/// consistent behavior, regardless of framerate.
///
/// The default [`timestep()`](Time::timestep) is 64 hertz, or 15625
/// microseconds. This value was chosen because using 60 hertz has the potential
/// for a pathological interaction with the monitor refresh rate where the game
/// alternates between running two fixed timesteps and zero fixed timesteps per
/// frame (for example when running two fixed timesteps takes longer than a
/// frame). Additionally, the value is a power of two which losslessly converts
/// into [`f32`] and [`f64`].
///
/// To run a system on a fixed timestep, add it to the [`FixedUpdate`] schedule.
/// This schedule is run a number of times between
/// [`PreUpdate`](bevy_app::PreUpdate) and [`Update`](bevy_app::Update)
/// according to the accumulated [`overstep()`](Time::overstep) time divided by
/// the [`timestep()`](Time::timestep). This means the schedule may run 0, 1 or
/// more times during a single update (which typically corresponds to a rendered
/// frame).
///
/// `Time<Fixed>` and the generic [`Time`] resource will report a
/// [`delta()`](Time::delta) equal to [`timestep()`](Time::timestep) and always
/// grow [`elapsed()`](Time::elapsed) by one [`timestep()`](Time::timestep) per
/// iteration.
///
/// The fixed timestep clock follows the [`Time<Virtual>`](Virtual) clock, which
/// means it is affected by [`pause()`](Time::pause),
/// [`set_relative_speed()`](Time::set_relative_speed) and
/// [`set_max_delta()`](Time::set_max_delta) from virtual time. If the virtual
/// clock is paused, the [`FixedUpdate`] schedule will not run. It is guaranteed
/// that the [`elapsed()`](Time::elapsed) time in `Time<Fixed>` is always
/// between the previous `elapsed()` and the current `elapsed()` value in
/// `Time<Virtual>`, so the values are compatible.
///
/// Changing the timestep size while the game is running should not normally be
/// done, as having a regular interval is the point of this schedule, but it may
/// be necessary for effects like "bullet-time" if the normal granularity of the
/// fixed timestep is too big for the slowed down time. In this case,
/// [`set_timestep()`](Time::set_timestep) and be called to set a new value. The
/// new value will be used immediately for the next run of the [`FixedUpdate`]
/// schedule, meaning that it will affect the [`delta()`](Time::delta) value for
/// the very next [`FixedUpdate`], even if it is still during the same frame.
/// Any [`overstep()`](Time::overstep) present in the accumulator will be
/// processed according to the new [`timestep()`](Time::timestep) value.
#[derive(Debug, Copy, Clone, Reflect)]
pub struct Fixed {
timestep: Duration,
overstep: Duration,
}
impl Time<Fixed> {
/// Corresponds to 64 Hz.
const DEFAULT_TIMESTEP: Duration = Duration::from_micros(15625);
/// Return new fixed time clock with given timestep as [`Duration`]
///
/// # Panics
///
/// Panics if `timestep` is zero.
pub fn from_duration(timestep: Duration) -> Self {
let mut ret = Self::default();
ret.set_timestep(timestep);
ret
}
/// Return new fixed time clock with given timestep seconds as `f64`
///
/// # Panics
///
/// Panics if `seconds` is zero, negative or not finite.
pub fn from_seconds(seconds: f64) -> Self {
let mut ret = Self::default();
ret.set_timestep_seconds(seconds);
ret
}
/// Return new fixed time clock with given timestep frequency in Hertz (1/seconds)
///
/// # Panics
///
/// Panics if `hz` is zero, negative or not finite.
pub fn from_hz(hz: f64) -> Self {
let mut ret = Self::default();
ret.set_timestep_hz(hz);
ret
}
/// Returns the amount of virtual time that must pass before the fixed
/// timestep schedule is run again.
#[inline]
pub fn timestep(&self) -> Duration {
self.context().timestep
}
/// Sets the amount of virtual time that must pass before the fixed timestep
/// schedule is run again, as [`Duration`].
///
/// Takes effect immediately on the next run of the schedule, respecting
/// what is currently in [`Self::overstep`].
///
/// # Panics
///
/// Panics if `timestep` is zero.
#[inline]
pub fn set_timestep(&mut self, timestep: Duration) {
assert_ne!(
timestep,
Duration::ZERO,
"attempted to set fixed timestep to zero"
);
self.context_mut().timestep = timestep;
}
/// Sets the amount of virtual time that must pass before the fixed timestep
/// schedule is run again, as seconds.
///
/// Timestep is stored as a [`Duration`], which has fixed nanosecond
/// resolution and will be converted from the floating point number.
///
/// Takes effect immediately on the next run of the schedule, respecting
/// what is currently in [`Self::overstep`].
///
/// # Panics
///
/// Panics if `seconds` is zero, negative or not finite.
#[inline]
pub fn set_timestep_seconds(&mut self, seconds: f64) {
assert!(
seconds.is_sign_positive(),
"seconds less than or equal to zero"
);
assert!(seconds.is_finite(), "seconds is infinite");
self.set_timestep(Duration::from_secs_f64(seconds));
}
/// Sets the amount of virtual time that must pass before the fixed timestep
/// schedule is run again, as frequency.
///
/// The timestep value is set to `1 / hz`, converted to a [`Duration`] which
/// has fixed nanosecond resolution.
///
/// Takes effect immediately on the next run of the schedule, respecting
/// what is currently in [`Self::overstep`].
///
/// # Panics
///
/// Panics if `hz` is zero, negative or not finite.
#[inline]
pub fn set_timestep_hz(&mut self, hz: f64) {
assert!(hz.is_sign_positive(), "Hz less than or equal to zero");
assert!(hz.is_finite(), "Hz is infinite");
self.set_timestep_seconds(1.0 / hz);
}
/// Returns the amount of overstep time accumulated toward new steps, as
/// [`Duration`].
#[inline]
pub fn overstep(&self) -> Duration {
self.context().overstep
}
/// Returns the amount of overstep time accumulated toward new steps, as an
/// [`f32`] fraction of the timestep.
#[inline]
pub fn overstep_percentage(&self) -> f32 {
self.context().overstep.as_secs_f32() / self.context().timestep.as_secs_f32()
}
/// Returns the amount of overstep time accumulated toward new steps, as an
/// [`f64`] fraction of the timestep.
#[inline]
pub fn overstep_percentage_f64(&self) -> f64 {
self.context().overstep.as_secs_f64() / self.context().timestep.as_secs_f64()
}
fn accumulate(&mut self, delta: Duration) {
self.context_mut().overstep += delta;
}
fn expend(&mut self) -> bool {
let timestep = self.timestep();
if let Some(new_value) = self.context_mut().overstep.checked_sub(timestep) {
// reduce accumulated and increase elapsed by period
self.context_mut().overstep = new_value;
self.advance_by(timestep);
true
} else {
// no more periods left in accumulated
false
}
}
}
impl Default for Fixed {
fn default() -> Self {
Self {
timestep: Time::<Fixed>::DEFAULT_TIMESTEP,
overstep: Duration::ZERO,
}
}
}
/// Runs [`FixedUpdate`] zero or more times based on delta of
/// [`Time<Virtual>`](Virtual) and [`Time::overstep`]
pub fn run_fixed_update_schedule(world: &mut World) {
let delta = world.resource::<Time<Virtual>>().delta();
world.resource_mut::<Time<Fixed>>().accumulate(delta);
// Run the schedule until we run out of accumulated time
let _ = world.try_schedule_scope(FixedUpdate, |world, schedule| {
while world.resource_mut::<Time<Fixed>>().expend() {
*world.resource_mut::<Time>() = world.resource::<Time<Fixed>>().as_generic();
schedule.run(world);
}
});
*world.resource_mut::<Time>() = world.resource::<Time<Virtual>>().as_generic();
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_set_timestep() {
let mut time = Time::<Fixed>::default();
assert_eq!(time.timestep(), Time::<Fixed>::DEFAULT_TIMESTEP);
time.set_timestep(Duration::from_millis(500));
assert_eq!(time.timestep(), Duration::from_millis(500));
time.set_timestep_seconds(0.25);
assert_eq!(time.timestep(), Duration::from_millis(250));
time.set_timestep_hz(8.0);
assert_eq!(time.timestep(), Duration::from_millis(125));
}
#[test]
fn test_expend() {
let mut time = Time::<Fixed>::from_seconds(2.0);
assert_eq!(time.delta(), Duration::ZERO);
assert_eq!(time.elapsed(), Duration::ZERO);
time.accumulate(Duration::from_secs(1));
assert_eq!(time.delta(), Duration::ZERO);
assert_eq!(time.elapsed(), Duration::ZERO);
assert_eq!(time.overstep(), Duration::from_secs(1));
assert_eq!(time.overstep_percentage(), 0.5);
assert_eq!(time.overstep_percentage_f64(), 0.5);
assert!(!time.expend()); // false
assert_eq!(time.delta(), Duration::ZERO);
assert_eq!(time.elapsed(), Duration::ZERO);
assert_eq!(time.overstep(), Duration::from_secs(1));
assert_eq!(time.overstep_percentage(), 0.5);
assert_eq!(time.overstep_percentage_f64(), 0.5);
time.accumulate(Duration::from_secs(1));
assert_eq!(time.delta(), Duration::ZERO);
assert_eq!(time.elapsed(), Duration::ZERO);
assert_eq!(time.overstep(), Duration::from_secs(2));
assert_eq!(time.overstep_percentage(), 1.0);
assert_eq!(time.overstep_percentage_f64(), 1.0);
assert!(time.expend()); // true
assert_eq!(time.delta(), Duration::from_secs(2));
assert_eq!(time.elapsed(), Duration::from_secs(2));
assert_eq!(time.overstep(), Duration::ZERO);
assert_eq!(time.overstep_percentage(), 0.0);
assert_eq!(time.overstep_percentage_f64(), 0.0);
assert!(!time.expend()); // false
assert_eq!(time.delta(), Duration::from_secs(2));
assert_eq!(time.elapsed(), Duration::from_secs(2));
assert_eq!(time.overstep(), Duration::ZERO);
assert_eq!(time.overstep_percentage(), 0.0);
assert_eq!(time.overstep_percentage_f64(), 0.0);
time.accumulate(Duration::from_secs(1));
assert_eq!(time.delta(), Duration::from_secs(2));
assert_eq!(time.elapsed(), Duration::from_secs(2));
assert_eq!(time.overstep(), Duration::from_secs(1));
assert_eq!(time.overstep_percentage(), 0.5);
assert_eq!(time.overstep_percentage_f64(), 0.5);
assert!(!time.expend()); // false
assert_eq!(time.delta(), Duration::from_secs(2));
assert_eq!(time.elapsed(), Duration::from_secs(2));
assert_eq!(time.overstep(), Duration::from_secs(1));
assert_eq!(time.overstep_percentage(), 0.5);
assert_eq!(time.overstep_percentage_f64(), 0.5);
}
#[test]
fn test_expend_multiple() {
let mut time = Time::<Fixed>::from_seconds(2.0);
time.accumulate(Duration::from_secs(7));
assert_eq!(time.overstep(), Duration::from_secs(7));
assert!(time.expend()); // true
assert_eq!(time.elapsed(), Duration::from_secs(2));
assert_eq!(time.overstep(), Duration::from_secs(5));
assert!(time.expend()); // true
assert_eq!(time.elapsed(), Duration::from_secs(4));
assert_eq!(time.overstep(), Duration::from_secs(3));
assert!(time.expend()); // true
assert_eq!(time.elapsed(), Duration::from_secs(6));
assert_eq!(time.overstep(), Duration::from_secs(1));
assert!(!time.expend()); // false
assert_eq!(time.elapsed(), Duration::from_secs(6));
assert_eq!(time.overstep(), Duration::from_secs(1));
}
}

View file

@ -1,178 +0,0 @@
//! Tools to run systems at a regular interval.
//! This can be extremely useful for steady, frame-rate independent gameplay logic and physics.
//!
//! To run a system on a fixed timestep, add it to the [`FixedUpdate`] [`Schedule`](bevy_ecs::schedule::Schedule).
//! This schedule is run in [`RunFixedUpdateLoop`](bevy_app::RunFixedUpdateLoop) near the start of each frame,
//! via the [`run_fixed_update_schedule`] exclusive system.
//!
//! This schedule will be run a number of times each frame,
//! equal to the accumulated divided by the period resource, rounded down,
//! as tracked in the [`FixedTime`] resource.
//! Unused time will be carried over.
//!
//! This does not guarantee that the time elapsed between executions is exact,
//! and systems in this schedule can run 0, 1 or more times on any given frame.
//!
//! For example, a system with a fixed timestep run criteria of 120 times per second will run
//! two times during a ~16.667ms frame, once during a ~8.333ms frame, and once every two frames
//! with ~4.167ms frames. However, the same criteria may not result in exactly 8.333ms passing
//! between each execution.
//!
//! When using fixed time steps, it is advised not to rely on [`Time::delta`] or any of it's
//! variants for game simulation, but rather use the value of [`FixedTime`] instead.
use crate::Time;
use bevy_app::FixedUpdate;
use bevy_ecs::{system::Resource, world::World};
use bevy_utils::Duration;
use thiserror::Error;
/// The amount of time that must pass before the fixed timestep schedule is run again.
///
/// For more information, see the [module-level documentation](self).
///
/// When using bevy's default configuration, this will be updated using the [`Time`]
/// resource. To customize how `Time` is updated each frame, see [`TimeUpdateStrategy`].
///
/// [`TimeUpdateStrategy`]: crate::TimeUpdateStrategy
#[derive(Resource, Debug)]
pub struct FixedTime {
accumulated: Duration,
/// The amount of time spanned by each fixed update.
/// Defaults to 1/60th of a second.
///
/// To configure this value, simply mutate or overwrite this field.
pub period: Duration,
}
impl FixedTime {
/// Creates a new [`FixedTime`] struct with a specified period.
pub fn new(period: Duration) -> Self {
FixedTime {
accumulated: Duration::ZERO,
period,
}
}
/// Creates a new [`FixedTime`] struct with a period specified in seconds.
pub fn new_from_secs(period: f32) -> Self {
FixedTime {
accumulated: Duration::ZERO,
period: Duration::from_secs_f32(period),
}
}
/// Adds to this instance's accumulated time. `delta_time` should be the amount of in-game time
/// that has passed since `tick` was last called.
///
/// Note that if you are using the default configuration of bevy, this will be called for you.
pub fn tick(&mut self, delta_time: Duration) {
self.accumulated += delta_time;
}
/// Returns the current amount of accumulated time.
///
/// Approximately, this represents how far behind the fixed update schedule is from the main schedule.
pub fn accumulated(&self) -> Duration {
self.accumulated
}
/// Attempts to advance by a single period. This will return [`FixedUpdateError`] if there is not enough
/// accumulated time -- in other words, if advancing time would put the fixed update schedule
/// ahead of the main schedule.
///
/// Note that if you are using the default configuration of bevy, this will be called for you.
pub fn expend(&mut self) -> Result<(), FixedUpdateError> {
if let Some(new_value) = self.accumulated.checked_sub(self.period) {
self.accumulated = new_value;
Ok(())
} else {
Err(FixedUpdateError::NotEnoughTime {
accumulated: self.accumulated,
period: self.period,
})
}
}
}
impl Default for FixedTime {
fn default() -> Self {
FixedTime {
accumulated: Duration::ZERO,
period: Duration::from_secs_f32(1. / 60.),
}
}
}
/// An error returned when working with [`FixedTime`].
#[derive(Debug, Error)]
pub enum FixedUpdateError {
/// There is not enough accumulated time to advance the fixed update schedule.
#[error("At least one period worth of time must be accumulated.")]
NotEnoughTime {
/// The amount of time available to advance the fixed update schedule.
accumulated: Duration,
/// The length of one fixed update.
period: Duration,
},
}
/// Ticks the [`FixedTime`] resource then runs the [`FixedUpdate`].
///
/// For more information, see the [module-level documentation](self).
pub fn run_fixed_update_schedule(world: &mut World) {
// Tick the time
let delta_time = world.resource::<Time>().delta();
let mut fixed_time = world.resource_mut::<FixedTime>();
fixed_time.tick(delta_time);
// Run the schedule until we run out of accumulated time
let _ = world.try_schedule_scope(FixedUpdate, |world, schedule| {
while world.resource_mut::<FixedTime>().expend().is_ok() {
schedule.run(world);
}
});
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn fixed_time_starts_at_zero() {
let new_time = FixedTime::new_from_secs(42.);
assert_eq!(new_time.accumulated(), Duration::ZERO);
let default_time = FixedTime::default();
assert_eq!(default_time.accumulated(), Duration::ZERO);
}
#[test]
fn fixed_time_ticks_up() {
let mut fixed_time = FixedTime::default();
fixed_time.tick(Duration::from_secs(1));
assert_eq!(fixed_time.accumulated(), Duration::from_secs(1));
}
#[test]
fn enough_accumulated_time_is_required() {
let mut fixed_time = FixedTime::new(Duration::from_secs(2));
fixed_time.tick(Duration::from_secs(1));
assert!(fixed_time.expend().is_err());
assert_eq!(fixed_time.accumulated(), Duration::from_secs(1));
fixed_time.tick(Duration::from_secs(1));
assert!(fixed_time.expend().is_ok());
assert_eq!(fixed_time.accumulated(), Duration::ZERO);
}
#[test]
fn repeatedly_expending_time() {
let mut fixed_time = FixedTime::new(Duration::from_secs(1));
fixed_time.tick(Duration::from_secs_f32(3.2));
assert!(fixed_time.expend().is_ok());
assert!(fixed_time.expend().is_ok());
assert!(fixed_time.expend().is_ok());
assert!(fixed_time.expend().is_err());
}
}

View file

@ -4,16 +4,20 @@
/// Common run conditions
pub mod common_conditions;
pub mod fixed_timestep;
mod fixed;
mod real;
mod stopwatch;
#[allow(clippy::module_inception)]
mod time;
mod timer;
mod virt;
use fixed_timestep::FixedTime;
pub use fixed::*;
pub use real::*;
pub use stopwatch::*;
pub use time::*;
pub use timer::*;
pub use virt::*;
use bevy_ecs::system::{Res, ResMut};
use bevy_utils::{tracing::warn, Duration, Instant};
@ -23,14 +27,12 @@ use crossbeam_channel::{Receiver, Sender};
pub mod prelude {
//! The Bevy Time Prelude.
#[doc(hidden)]
pub use crate::{fixed_timestep::FixedTime, Time, Timer, TimerMode};
pub use crate::{Fixed, Real, Time, Timer, TimerMode, Virtual};
}
use bevy_app::{prelude::*, RunFixedUpdateLoop};
use bevy_ecs::prelude::*;
use crate::fixed_timestep::run_fixed_update_schedule;
/// Adds time functionality to Apps.
#[derive(Default)]
pub struct TimePlugin;
@ -43,12 +45,20 @@ pub struct TimeSystem;
impl Plugin for TimePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<Time>()
.init_resource::<Time<Real>>()
.init_resource::<Time<Virtual>>()
.init_resource::<Time<Fixed>>()
.init_resource::<TimeUpdateStrategy>()
.register_type::<Timer>()
.register_type::<Time>()
.register_type::<Time<Real>>()
.register_type::<Time<Virtual>>()
.register_type::<Time<Fixed>>()
.register_type::<Timer>()
.register_type::<Stopwatch>()
.init_resource::<FixedTime>()
.add_systems(First, time_system.in_set(TimeSystem))
.add_systems(
First,
(time_system, virtual_time_system.after(time_system)).in_set(TimeSystem),
)
.add_systems(RunFixedUpdateLoop, run_fixed_update_schedule);
#[cfg(feature = "bevy_ci_testing")]
@ -68,8 +78,8 @@ impl Plugin for TimePlugin {
/// Configuration resource used to determine how the time system should run.
///
/// For most cases, [`TimeUpdateStrategy::Automatic`] is fine. When writing tests, dealing with networking, or similar
/// you may prefer to set the next [`Time`] value manually.
/// For most cases, [`TimeUpdateStrategy::Automatic`] is fine. When writing tests, dealing with
/// networking or similar, you may prefer to set the next [`Time`] value manually.
#[derive(Resource, Default)]
pub enum TimeUpdateStrategy {
/// [`Time`] will be automatically updated each frame using an [`Instant`] sent from the render world via a [`TimeSender`].
@ -101,10 +111,10 @@ pub fn create_time_channels() -> (TimeSender, TimeReceiver) {
(TimeSender(s), TimeReceiver(r))
}
/// The system used to update the [`Time`] used by app logic. If there is a render world the time is sent from
/// there to this system through channels. Otherwise the time is updated in this system.
/// The system used to update the [`Time`] used by app logic. If there is a render world the time is
/// sent from there to this system through channels. Otherwise the time is updated in this system.
fn time_system(
mut time: ResMut<Time>,
mut time: ResMut<Time<Real>>,
update_strategy: Res<TimeUpdateStrategy>,
time_recv: Option<Res<TimeReceiver>>,
mut has_received_time: Local<bool>,
@ -127,9 +137,6 @@ fn time_system(
match update_strategy.as_ref() {
TimeUpdateStrategy::Automatic => time.update_with_instant(new_time),
TimeUpdateStrategy::ManualInstant(instant) => time.update_with_instant(*instant),
TimeUpdateStrategy::ManualDuration(duration) => {
let last_update = time.last_update().unwrap_or_else(|| time.startup());
time.update_with_instant(last_update + *duration);
}
TimeUpdateStrategy::ManualDuration(duration) => time.update_with_duration(*duration),
}
}

View file

@ -0,0 +1,220 @@
use bevy_reflect::Reflect;
use bevy_utils::{Duration, Instant};
use crate::time::Time;
/// Real time clock representing elapsed wall clock time.
///
/// A specialization of the [`Time`] structure. **For method documentation, see
/// [`Time<Real>#impl-Time<Real>`].**
///
/// It is automatically inserted as a resource by
/// [`TimePlugin`](crate::TimePlugin) and updated with time instants according
/// to [`TimeUpdateStrategy`](crate::TimeUpdateStrategy).
///
/// The [`delta()`](Time::delta) and [`elapsed()`](Time::elapsed) values of this
/// clock should be used for anything which deals specifically with real time
/// (wall clock time). It will not be affected by relative game speed
/// adjustments, pausing or other adjustments.
///
/// The clock does not count time from [`startup()`](Time::startup) to
/// [`first_update()`](Time::first_update()) into elapsed, but instead will
/// start counting time from the first update call. [`delta()`](Time::delta) and
/// [`elapsed()`](Time::elapsed) will report zero on the first update as there
/// is no previous update instant. This means that a [`delta()`](Time::delta) of
/// zero must be handled without errors in application logic, as it may
/// theoretically also happen at other times.
///
/// [`Instant`](std::time::Instant)s for [`startup()`](Time::startup),
/// [`first_update()`](Time::first_update) and
/// [`last_update()`](Time::last_update) are recorded and accessible.
#[derive(Debug, Copy, Clone, Reflect)]
pub struct Real {
startup: Instant,
first_update: Option<Instant>,
last_update: Option<Instant>,
}
impl Default for Real {
fn default() -> Self {
Self {
startup: Instant::now(),
first_update: None,
last_update: None,
}
}
}
impl Time<Real> {
/// Constructs a new `Time<Real>` instance with a specific startup
/// [`Instant`](std::time::Instant).
pub fn new(startup: Instant) -> Self {
Self::new_with(Real {
startup,
..Default::default()
})
}
/// Updates the internal time measurements.
///
/// Calling this method as part of your app will most likely result in
/// inaccurate timekeeping, as the [`Time`] resource is ordinarily managed
/// by the [`TimePlugin`](crate::TimePlugin).
pub fn update(&mut self) {
let instant = Instant::now();
self.update_with_instant(instant);
}
/// Updates time with a specified [`Duration`].
///
/// This method is provided for use in tests.
///
/// Calling this method as part of your app will most likely result in
/// inaccurate timekeeping, as the [`Time`] resource is ordinarily managed
/// by the [`TimePlugin`](crate::TimePlugin).
pub fn update_with_duration(&mut self, duration: Duration) {
let last_update = self.context().last_update.unwrap_or(self.context().startup);
self.update_with_instant(last_update + duration);
}
/// Updates time with a specified [`Instant`](std::time::Instant).
///
/// This method is provided for use in tests.
///
/// Calling this method as part of your app will most likely result in inaccurate timekeeping,
/// as the [`Time`] resource is ordinarily managed by the [`TimePlugin`](crate::TimePlugin).
pub fn update_with_instant(&mut self, instant: Instant) {
let Some(last_update) = self.context().last_update else {
let context = self.context_mut();
context.first_update = Some(instant);
context.last_update = Some(instant);
return;
};
let delta = instant - last_update;
self.advance_by(delta);
self.context_mut().last_update = Some(instant);
}
/// Returns the [`Instant`](std::time::Instant) the clock was created.
///
/// This usually represents when the app was started.
#[inline]
pub fn startup(&self) -> Instant {
self.context().startup
}
/// Returns the [`Instant`](std::time::Instant) when [`Self::update`] was first called, if it
/// exists.
///
/// This usually represents when the first app update started.
#[inline]
pub fn first_update(&self) -> Option<Instant> {
self.context().first_update
}
/// Returns the [`Instant`](std::time::Instant) when [`Self::update`] was last called, if it
/// exists.
///
/// This usually represents when the current app update started.
#[inline]
pub fn last_update(&self) -> Option<Instant> {
self.context().last_update
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_update() {
let startup = Instant::now();
let mut time = Time::<Real>::new(startup);
assert_eq!(time.startup(), startup);
assert_eq!(time.first_update(), None);
assert_eq!(time.last_update(), None);
assert_eq!(time.delta(), Duration::ZERO);
assert_eq!(time.elapsed(), Duration::ZERO);
time.update();
assert_ne!(time.first_update(), None);
assert_ne!(time.last_update(), None);
assert_eq!(time.delta(), Duration::ZERO);
assert_eq!(time.elapsed(), Duration::ZERO);
time.update();
assert_ne!(time.first_update(), None);
assert_ne!(time.last_update(), None);
assert_ne!(time.last_update(), time.first_update());
assert_ne!(time.delta(), Duration::ZERO);
assert_eq!(time.elapsed(), time.delta());
let prev_elapsed = time.elapsed();
time.update();
assert_ne!(time.delta(), Duration::ZERO);
assert_eq!(time.elapsed(), prev_elapsed + time.delta());
}
#[test]
fn test_update_with_instant() {
let startup = Instant::now();
let mut time = Time::<Real>::new(startup);
let first_update = Instant::now();
time.update_with_instant(first_update);
assert_eq!(time.startup(), startup);
assert_eq!(time.first_update(), Some(first_update));
assert_eq!(time.last_update(), Some(first_update));
assert_eq!(time.delta(), Duration::ZERO);
assert_eq!(time.elapsed(), Duration::ZERO);
let second_update = Instant::now();
time.update_with_instant(second_update);
assert_eq!(time.first_update(), Some(first_update));
assert_eq!(time.last_update(), Some(second_update));
assert_eq!(time.delta(), second_update - first_update);
assert_eq!(time.elapsed(), second_update - first_update);
let third_update = Instant::now();
time.update_with_instant(third_update);
assert_eq!(time.first_update(), Some(first_update));
assert_eq!(time.last_update(), Some(third_update));
assert_eq!(time.delta(), third_update - second_update);
assert_eq!(time.elapsed(), third_update - first_update);
}
#[test]
fn test_update_with_duration() {
let startup = Instant::now();
let mut time = Time::<Real>::new(startup);
time.update_with_duration(Duration::from_secs(1));
assert_eq!(time.startup(), startup);
assert_eq!(time.first_update(), Some(startup + Duration::from_secs(1)));
assert_eq!(time.last_update(), Some(startup + Duration::from_secs(1)));
assert_eq!(time.delta(), Duration::ZERO);
assert_eq!(time.elapsed(), Duration::ZERO);
time.update_with_duration(Duration::from_secs(1));
assert_eq!(time.first_update(), Some(startup + Duration::from_secs(1)));
assert_eq!(time.last_update(), Some(startup + Duration::from_secs(2)));
assert_eq!(time.delta(), Duration::from_secs(1));
assert_eq!(time.elapsed(), Duration::from_secs(1));
time.update_with_duration(Duration::from_secs(1));
assert_eq!(time.first_update(), Some(startup + Duration::from_secs(1)));
assert_eq!(time.last_update(), Some(startup + Duration::from_secs(3)));
assert_eq!(time.delta(), Duration::from_secs(1));
assert_eq!(time.elapsed(), Duration::from_secs(2));
}
}

View file

@ -1,5 +1,4 @@
use bevy_reflect::prelude::*;
use bevy_reflect::Reflect;
use bevy_reflect::{prelude::*, Reflect};
use bevy_utils::Duration;
/// A Stopwatch is a struct that track elapsed time when started.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,437 @@
use bevy_ecs::system::{Res, ResMut};
use bevy_reflect::Reflect;
use bevy_utils::{tracing::warn, Duration};
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 {
warn!(
"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));
}
}

View file

@ -2,13 +2,12 @@
use bevy::prelude::*;
const TIME_STEP: f32 = 1.0 / 60.0;
const BOUNDS: Vec2 = Vec2::new(1200.0, 640.0);
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(FixedTime::new_from_secs(TIME_STEP))
.insert_resource(Time::<Fixed>::from_hz(60.0))
.add_systems(Startup, setup)
.add_systems(
FixedUpdate,
@ -117,6 +116,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
/// Demonstrates applying rotation and movement based on keyboard input.
fn player_movement_system(
time: Res<Time>,
keyboard_input: Res<Input<KeyCode>>,
mut query: Query<(&Player, &mut Transform)>,
) {
@ -138,12 +138,14 @@ fn player_movement_system(
}
// update the ship rotation around the Z axis (perpendicular to the 2D plane of the screen)
transform.rotate_z(rotation_factor * ship.rotation_speed * TIME_STEP);
transform.rotate_z(rotation_factor * ship.rotation_speed * time.delta_seconds());
// get the ship's forward vector by applying the current rotation to the ships initial facing vector
// get the ship's forward vector by applying the current rotation to the ships initial facing
// vector
let movement_direction = transform.rotation * Vec3::Y;
// get the distance the ship will move based on direction, the ship's movement speed and delta time
let movement_distance = movement_factor * ship.movement_speed * TIME_STEP;
// get the distance the ship will move based on direction, the ship's movement speed and delta
// time
let movement_distance = movement_factor * ship.movement_speed * time.delta_seconds();
// create the change in translation using the new movement direction and distance
let translation_delta = movement_direction * movement_distance;
// update the ship translation with our new translation delta
@ -182,8 +184,8 @@ fn snap_to_player_system(
/// if not, which way to rotate to face the player. The dot product on two unit length vectors
/// will return a value between -1.0 and +1.0 which tells us the following about the two vectors:
///
/// * If the result is 1.0 the vectors are pointing in the same direction, the angle between them
/// is 0 degrees.
/// * If the result is 1.0 the vectors are pointing in the same direction, the angle between them is
/// 0 degrees.
/// * If the result is 0.0 the vectors are perpendicular, the angle between them is 90 degrees.
/// * If the result is -1.0 the vectors are parallel but pointing in opposite directions, the angle
/// between them is 180 degrees.
@ -198,6 +200,7 @@ fn snap_to_player_system(
/// floating point precision loss, so it pays to clamp your dot product value before calling
/// `acos`.
fn rotate_to_player_system(
time: Res<Time>,
mut query: Query<(&RotateToPlayer, &mut Transform), Without<Player>>,
player_query: Query<&Transform, With<Player>>,
) {
@ -242,7 +245,8 @@ fn rotate_to_player_system(
let max_angle = forward_dot_player.clamp(-1.0, 1.0).acos(); // clamp acos for safety
// calculate angle of rotation with limit
let rotation_angle = rotation_sign * (config.rotation_speed * TIME_STEP).min(max_angle);
let rotation_angle =
rotation_sign * (config.rotation_speed * time.delta_seconds()).min(max_angle);
// rotate the enemy to face the player
enemy_transform.rotate_z(rotation_angle);

View file

@ -233,6 +233,7 @@ Example | Description
[System Closure](../examples/ecs/system_closure.rs) | Show how to use closures as systems, and how to configure `Local` variables by capturing external state
[System Parameter](../examples/ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam`
[System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully
[Time handling](../examples/ecs/time.rs) | Explains how Time is handled in ECS
[Timers](../examples/ecs/timers.rs) | Illustrates ticking `Timer` resources inside systems and handling their state
## Games

View file

@ -2,7 +2,6 @@
use bevy::prelude::*;
const FIXED_TIMESTEP: f32 = 0.5;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
@ -11,28 +10,31 @@ fn main() {
// add our system to the fixed timestep schedule
.add_systems(FixedUpdate, fixed_update)
// configure our fixed timestep schedule to run twice a second
.insert_resource(FixedTime::new_from_secs(FIXED_TIMESTEP))
.insert_resource(Time::<Fixed>::from_seconds(0.5))
.run();
}
fn frame_update(mut last_time: Local<f32>, time: Res<Time>) {
// Default `Time` is `Time<Virtual>` here
info!(
"time since last frame_update: {}",
time.raw_elapsed_seconds() - *last_time
time.elapsed_seconds() - *last_time
);
*last_time = time.raw_elapsed_seconds();
*last_time = time.elapsed_seconds();
}
fn fixed_update(mut last_time: Local<f32>, time: Res<Time>, fixed_time: Res<FixedTime>) {
fn fixed_update(mut last_time: Local<f32>, time: Res<Time>, fixed_time: Res<Time<Fixed>>) {
// Default `Time`is `Time<Fixed>` here
info!(
"time since last fixed_update: {}\n",
time.raw_elapsed_seconds() - *last_time
time.elapsed_seconds() - *last_time
);
info!("fixed timestep: {}\n", FIXED_TIMESTEP);
info!("fixed timestep: {}\n", time.delta_seconds());
// If we want to see the overstep, we need to access `Time<Fixed>` specifically
info!(
"time accrued toward next fixed_update: {}\n",
fixed_time.accumulated().as_secs_f32()
fixed_time.overstep().as_secs_f32()
);
*last_time = time.raw_elapsed_seconds();
*last_time = time.elapsed_seconds();
}

View file

@ -3,8 +3,6 @@
use bevy::{pbr::AmbientLight, prelude::*};
use rand::{rngs::StdRng, Rng, SeedableRng};
const DELTA_TIME: f32 = 0.01;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
@ -13,7 +11,6 @@ fn main() {
..default()
})
.insert_resource(ClearColor(Color::BLACK))
.insert_resource(FixedTime::new_from_secs(DELTA_TIME))
.add_systems(Startup, generate_bodies)
.add_systems(FixedUpdate, (interact_bodies, integrate))
.add_systems(Update, look_at_star)
@ -41,6 +38,7 @@ struct BodyBundle {
}
fn generate_bodies(
time: Res<Time>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
@ -96,7 +94,7 @@ fn generate_bodies(
rng.gen_range(vel_range.clone()),
rng.gen_range(vel_range.clone()),
rng.gen_range(vel_range.clone()),
) * DELTA_TIME,
) * time.delta_seconds(),
),
});
}
@ -160,8 +158,8 @@ fn interact_bodies(mut query: Query<(&Mass, &GlobalTransform, &mut Acceleration)
}
}
fn integrate(mut query: Query<(&mut Acceleration, &mut Transform, &mut LastPos)>) {
let dt_sq = DELTA_TIME * DELTA_TIME;
fn integrate(time: Res<Time>, mut query: Query<(&mut Acceleration, &mut Transform, &mut LastPos)>) {
let dt_sq = time.delta_seconds() * time.delta_seconds();
for (mut acceleration, mut transform, mut last_pos) in &mut query {
// verlet integration
// x(t+dt) = 2x(t) - x(t-dt) + a(t)dt^2 + O(dt^4)

115
examples/ecs/time.rs Normal file
View file

@ -0,0 +1,115 @@
use bevy::prelude::*;
use std::io::{self, BufRead};
use std::time::Duration;
fn banner() {
println!("This example is meant to intuitively demonstrate how Time works in Bevy.");
println!();
println!("Time will be printed in three different schedules in the app:");
println!("- PreUpdate: real time is printed");
println!("- FixedUpdate: fixed time step time is printed, may be run zero or multiple times");
println!("- Update: virtual game time is printed");
println!();
println!("Max delta time is set to 5 seconds. Fixed timestep is set to 1 second.");
println!();
}
fn help() {
println!("The app reads commands line-by-line from standard input.");
println!();
println!("Commands:");
println!(" empty line: Run app.update() once on the Bevy App");
println!(" q: Quit the app.");
println!(" f: Set speed to fast, 2x");
println!(" n: Set speed to normal, 1x");
println!(" n: Set speed to slow, 0.5x");
println!(" p: Pause");
println!(" u: Unpause");
}
fn runner(mut app: App) {
banner();
help();
let stdin = io::stdin();
for line in stdin.lock().lines() {
if let Err(err) = line {
println!("read err: {:#}", err);
break;
}
match line.unwrap().as_str() {
"" => {
app.update();
}
"f" => {
println!("FAST: setting relative speed to 2x");
app.world
.resource_mut::<Time<Virtual>>()
.set_relative_speed(2.0);
}
"n" => {
println!("NORMAL: setting relative speed to 1x");
app.world
.resource_mut::<Time<Virtual>>()
.set_relative_speed(1.0);
}
"s" => {
println!("SLOW: setting relative speed to 0.5x");
app.world
.resource_mut::<Time<Virtual>>()
.set_relative_speed(0.5);
}
"p" => {
println!("PAUSE: pausing virtual clock");
app.world.resource_mut::<Time<Virtual>>().pause();
}
"u" => {
println!("UNPAUSE: resuming virtual clock");
app.world.resource_mut::<Time<Virtual>>().unpause();
}
"q" => {
println!("QUITTING!");
break;
}
_ => {
help();
}
}
}
}
fn print_real_time(time: Res<Time<Real>>) {
println!(
"PreUpdate: this is real time clock, delta is {:?} and elapsed is {:?}",
time.delta(),
time.elapsed()
);
}
fn print_fixed_time(time: Res<Time>) {
println!(
"FixedUpdate: this is generic time clock inside fixed, delta is {:?} and elapsed is {:?}",
time.delta(),
time.elapsed()
);
}
fn print_time(time: Res<Time>) {
println!(
"Update: this is generic time clock, delta is {:?} and elapsed is {:?}",
time.delta(),
time.elapsed()
);
}
fn main() {
App::new()
.add_plugins(MinimalPlugins)
.insert_resource(Time::<Virtual>::from_max_delta(Duration::from_secs(5)))
.insert_resource(Time::<Fixed>::from_duration(Duration::from_secs(1)))
.add_systems(PreUpdate, print_real_time)
.add_systems(FixedUpdate, print_fixed_time)
.add_systems(Update, print_time)
.set_runner(runner)
.run();
}

View file

@ -53,10 +53,9 @@ fn main() {
.insert_resource(Scoreboard { score: 0 })
.insert_resource(ClearColor(BACKGROUND_COLOR))
.add_event::<CollisionEvent>()
// Configure how frequently our gameplay systems are run
.insert_resource(FixedTime::new_from_secs(1.0 / 60.0))
.add_systems(Startup, setup)
// Add our gameplay simulation systems to the fixed timestep schedule
// which runs at 64 Hz by default
.add_systems(
FixedUpdate,
(
@ -306,7 +305,7 @@ fn setup(
fn move_paddle(
keyboard_input: Res<Input<KeyCode>>,
mut query: Query<&mut Transform, With<Paddle>>,
time_step: Res<FixedTime>,
time: Res<Time>,
) {
let mut paddle_transform = query.single_mut();
let mut direction = 0.0;
@ -321,7 +320,7 @@ fn move_paddle(
// Calculate the new horizontal paddle position based on player input
let new_paddle_position =
paddle_transform.translation.x + direction * PADDLE_SPEED * time_step.period.as_secs_f32();
paddle_transform.translation.x + direction * PADDLE_SPEED * time.delta_seconds();
// Update the paddle position,
// making sure it doesn't cause the paddle to leave the arena
@ -331,10 +330,10 @@ fn move_paddle(
paddle_transform.translation.x = new_paddle_position.clamp(left_bound, right_bound);
}
fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time_step: Res<FixedTime>) {
fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time: Res<Time>) {
for (mut transform, velocity) in &mut query {
transform.translation.x += velocity.x * time_step.period.as_secs_f32();
transform.translation.y += velocity.y * time_step.period.as_secs_f32();
transform.translation.x += velocity.x * time.delta_seconds();
transform.translation.y += velocity.y * time.delta_seconds();
}
}

View file

@ -10,6 +10,7 @@ use bevy::{
prelude::*,
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
sprite::{MaterialMesh2dBundle, Mesh2dHandle},
utils::Duration,
window::{PresentMode, WindowResolution},
};
use rand::{rngs::StdRng, seq::SliceRandom, Rng, SeedableRng};
@ -123,7 +124,9 @@ fn main() {
counter_system,
),
)
.insert_resource(FixedTime::new_from_secs(FIXED_TIMESTEP))
.insert_resource(Time::<Fixed>::from_duration(Duration::from_secs_f32(
FIXED_TIMESTEP,
)))
.run();
}