mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
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:
parent
02943737b2
commit
3d79dc4cdc
20 changed files with 1602 additions and 871 deletions
10
Cargo.toml
10
Cargo.toml
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
343
crates/bevy_time/src/fixed.rs
Normal file
343
crates/bevy_time/src/fixed.rs
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
220
crates/bevy_time/src/real.rs
Normal file
220
crates/bevy_time/src/real.rs
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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
437
crates/bevy_time/src/virt.rs
Normal file
437
crates/bevy_time/src/virt.rs
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
115
examples/ecs/time.rs
Normal 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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue