use crate::schedule::Schedule;
use crate::virtual_time::VirtualTime;
use crate::world::World;
pub struct FixedTimestep {
pub step: f64,
accumulator: f64,
pub tick_count: u64,
}
impl FixedTimestep {
pub const MIN_HZ: f64 = 1.0;
pub const MAX_HZ: f64 = 240.0;
pub fn new(tick_rate: f64) -> Self {
assert!(
(Self::MIN_HZ..=Self::MAX_HZ).contains(&tick_rate),
"tick rate {tick_rate} Hz out of range [{}, {}]",
Self::MIN_HZ,
Self::MAX_HZ,
);
Self {
step: 1.0 / tick_rate,
accumulator: 0.0,
tick_count: 0,
}
}
pub fn default_rts() -> Self {
Self::new(10.0)
}
pub fn strategy() -> Self {
Self::new(20.0)
}
pub fn action() -> Self {
Self::new(30.0)
}
pub fn fast() -> Self {
Self::new(60.0)
}
pub fn tick_rate(&self) -> f64 {
1.0 / self.step
}
}
pub fn tick(world: &mut World, schedule: &mut Schedule, elapsed: f64) -> u32 {
let virtual_elapsed = if let Some(mut vt) = world.try_take_resource::<VirtualTime>() {
let ve = vt.effective_elapsed(elapsed);
vt.elapsed += ve;
world.insert_resource(vt);
ve
} else {
elapsed
};
let mut ts = world.take_resource::<FixedTimestep>();
ts.accumulator += virtual_elapsed;
let mut ticks = 0u32;
while ts.accumulator >= ts.step {
ts.accumulator -= ts.step;
ts.tick_count += 1;
ticks += 1;
world.advance_tick();
world.insert_resource(FixedTimestep {
step: ts.step,
accumulator: ts.accumulator,
tick_count: ts.tick_count,
});
schedule.run(world);
ts = world.take_resource::<FixedTimestep>();
}
world.insert_resource(ts);
ticks
}
#[cfg(test)]
mod tests {
use super::*;
use crate::component::Component;
use crate::system_param::{QueryMut, Res};
use crate::virtual_time::VirtualTime;
#[derive(Debug)]
struct TickCounter(u32);
impl Component for TickCounter {}
fn count_system(mut counters: QueryMut<'_, TickCounter>) {
for (_, counter) in counters.iter_mut() {
counter.0 += 1;
}
}
#[test]
fn genre_presets() {
let rts = FixedTimestep::default_rts();
assert!((rts.tick_rate() - 10.0).abs() < f64::EPSILON);
let strat = FixedTimestep::strategy();
assert!((strat.tick_rate() - 20.0).abs() < f64::EPSILON);
let act = FixedTimestep::action();
assert!((act.tick_rate() - 30.0).abs() < f64::EPSILON);
let fps = FixedTimestep::fast();
assert!((fps.tick_rate() - 60.0).abs() < f64::EPSILON);
}
#[test]
fn rejects_below_min_hz() {
let result = std::panic::catch_unwind(|| FixedTimestep::new(0.5));
assert!(result.is_err());
}
#[test]
fn rejects_above_max_hz() {
let result = std::panic::catch_unwind(|| FixedTimestep::new(500.0));
assert!(result.is_err());
}
#[test]
fn accepts_boundary_values() {
let low = FixedTimestep::new(FixedTimestep::MIN_HZ);
assert!((low.tick_rate() - 1.0).abs() < f64::EPSILON);
let high = FixedTimestep::new(FixedTimestep::MAX_HZ);
assert!((high.tick_rate() - 240.0).abs() < f64::EPSILON);
}
#[test]
fn fixed_timestep_creation() {
let ts = FixedTimestep::new(10.0);
assert!((ts.step - 0.1).abs() < f64::EPSILON);
assert_eq!(ts.tick_count, 0);
assert!((ts.tick_rate() - 10.0).abs() < f64::EPSILON);
}
#[test]
fn tick_runs_correct_number_of_times() {
let mut world = World::new();
world.insert_resource(FixedTimestep::new(10.0));
world.spawn((TickCounter(0),));
let mut schedule = Schedule::new();
schedule.add_system::<(QueryMut<'_, TickCounter>,)>("simulate", "count", count_system);
let ticks = tick(&mut world, &mut schedule, 0.25);
assert_eq!(ticks, 2);
let counts: Vec<u32> = world.query::<&TickCounter>().map(|(_, c)| c.0).collect();
assert_eq!(counts, vec![2]);
}
#[test]
fn accumulator_carries_remainder() {
let mut world = World::new();
world.insert_resource(FixedTimestep::new(10.0));
world.spawn((TickCounter(0),));
let mut schedule = Schedule::new();
schedule.add_system::<(QueryMut<'_, TickCounter>,)>("simulate", "count", count_system);
let ticks = tick(&mut world, &mut schedule, 0.05);
assert_eq!(ticks, 0);
let ticks = tick(&mut world, &mut schedule, 0.06);
assert_eq!(ticks, 1);
let counts: Vec<u32> = world.query::<&TickCounter>().map(|(_, c)| c.0).collect();
assert_eq!(counts, vec![1]);
}
#[test]
fn tick_count_increments() {
let mut world = World::new();
world.insert_resource(FixedTimestep::new(10.0));
let mut schedule = Schedule::new();
tick(&mut world, &mut schedule, 0.35); let ts = world.resource::<FixedTimestep>();
assert_eq!(ts.tick_count, 3);
}
#[test]
fn systems_can_read_timestep() {
fn read_step(ts: Res<'_, FixedTimestep>) {
assert!((ts.step - 0.1).abs() < f64::EPSILON);
}
let mut world = World::new();
world.insert_resource(FixedTimestep::new(10.0));
let mut schedule = Schedule::new();
schedule.add_system::<(Res<'_, FixedTimestep>,)>("simulate", "read_step", read_step);
tick(&mut world, &mut schedule, 0.1);
}
#[test]
fn no_virtual_time_unchanged_behavior() {
let mut world = World::new();
world.insert_resource(FixedTimestep::new(10.0));
world.spawn((TickCounter(0),));
let mut schedule = Schedule::new();
schedule.add_system::<(QueryMut<'_, TickCounter>,)>("simulate", "count", count_system);
let ticks = tick(&mut world, &mut schedule, 0.25);
assert_eq!(ticks, 2);
}
#[test]
fn virtual_time_paused_zero_ticks() {
let mut world = World::new();
world.insert_resource(FixedTimestep::new(10.0));
let mut vt = VirtualTime::new();
vt.paused = true;
world.insert_resource(vt);
world.spawn((TickCounter(0),));
let mut schedule = Schedule::new();
schedule.add_system::<(QueryMut<'_, TickCounter>,)>("simulate", "count", count_system);
let ticks = tick(&mut world, &mut schedule, 1.0);
assert_eq!(ticks, 0);
let counts: Vec<u32> = world.query::<&TickCounter>().map(|(_, c)| c.0).collect();
assert_eq!(counts, vec![0]);
}
#[test]
fn virtual_time_scale_doubles_ticks() {
let mut world = World::new();
world.insert_resource(FixedTimestep::new(10.0));
let mut vt = VirtualTime::new();
vt.scale = 2.0;
world.insert_resource(vt);
world.spawn((TickCounter(0),));
let mut schedule = Schedule::new();
schedule.add_system::<(QueryMut<'_, TickCounter>,)>("simulate", "count", count_system);
let ticks = tick(&mut world, &mut schedule, 0.1);
assert_eq!(ticks, 2);
}
#[test]
fn virtual_time_max_delta_prevents_death_spiral() {
let mut world = World::new();
world.insert_resource(FixedTimestep::new(10.0));
world.insert_resource(VirtualTime::new()); world.spawn((TickCounter(0),));
let mut schedule = Schedule::new();
schedule.add_system::<(QueryMut<'_, TickCounter>,)>("simulate", "count", count_system);
let ticks = tick(&mut world, &mut schedule, 5.0);
assert_eq!(ticks, 2);
}
#[test]
fn virtual_time_elapsed_accumulates() {
let mut world = World::new();
world.insert_resource(FixedTimestep::new(10.0));
world.insert_resource(VirtualTime::new());
let mut schedule = Schedule::new();
tick(&mut world, &mut schedule, 0.1);
tick(&mut world, &mut schedule, 0.15);
let vt = world.resource::<VirtualTime>();
assert!((vt.elapsed - 0.25).abs() < f64::EPSILON);
}
}