use bevy_app::FixedMain; use bevy_ecs::world::World; use bevy_reflect::Reflect; use bevy_utils::Duration; use crate::{time::Time, virt::Virtual}; /// The fixed timestep game clock following virtual time. /// /// A specialization of the [`Time`] structure. **For method documentation, see /// [`Time#impl-Time`].** /// /// It is automatically inserted as a resource by /// [`TimePlugin`](crate::TimePlugin) and updated based on /// [`Time`](Virtual). The fixed clock is automatically set as the /// generic [`Time`] resource during [`FixedUpdate`](bevy_app::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 one of the [`FixedMain`] /// schedules, most commonly [`FixedUpdate`](bevy_app::FixedUpdate). /// /// 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` 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) 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`](bevy_app::FixedUpdate) schedule will /// not run. It is guaranteed that the [`elapsed()`](Time::elapsed) time in /// `Time` is always between the previous `elapsed()` and the current /// `elapsed()` value in `Time`, 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`](bevy_app::FixedUpdate) schedule, meaning that it will affect /// the [`delta()`](Time::delta) value for the very next /// [`FixedUpdate`](bevy_app::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 { /// 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 } /// Discard a part of the overstep amount. /// /// If `discard` is higher than overstep, the overstep becomes zero. #[inline] pub fn discard_overstep(&mut self, discard: Duration) { let context = self.context_mut(); context.overstep = context.overstep.saturating_sub(discard); } /// Returns the amount of overstep time accumulated toward new steps, as an /// [`f32`] fraction of the timestep. #[inline] pub fn overstep_fraction(&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_fraction_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::::DEFAULT_TIMESTEP, overstep: Duration::ZERO, } } } /// Runs [`FixedMain`] zero or more times based on delta of /// [`Time`](Virtual) and [`Time::overstep`] pub fn run_fixed_main_schedule(world: &mut World) { let delta = world.resource::>().delta(); world.resource_mut::>().accumulate(delta); // Run the schedule until we run out of accumulated time let _ = world.try_schedule_scope(FixedMain, |world, schedule| { while world.resource_mut::>().expend() { *world.resource_mut::