bevy/crates/bevy_time/src/fixed_timestep.rs
Joseph f99dcadf8a
Add missing documentation to bevy_time (#9428)
# Objective

* Add documentation to undocumented items in `bevy_time`.
* Add a warning for undocumented items.

---------

Co-authored-by: Sélène Amanita <134181069+Selene-Amanita@users.noreply.github.com>
2023-08-15 21:48:37 +00:00

179 lines
6.5 KiB
Rust

//! 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());
}
}