use web_time::Duration;
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule};
use bevy_ecs::system::ScheduleSystem;
pub use bevy_ecs;
pub struct Sim {
pub world: World,
schedule: Schedule,
accumulator: Duration,
pub step: Duration,
pub max_steps: u32,
}
#[derive(Resource, Clone, Copy, Debug, Default)]
pub struct SimTime {
pub elapsed_secs: f64,
pub delta_secs: f32,
}
impl Sim {
pub fn new(step: Duration) -> Self {
let mut world = World::new();
world.init_resource::<SimTime>();
Self {
world,
schedule: Schedule::default(),
accumulator: Duration::ZERO,
step,
max_steps: 8,
}
}
pub fn with_default_step() -> Self {
Self::new(Duration::from_secs_f64(1.0 / 60.0))
}
pub fn add_system<M>(&mut self, system: impl IntoSystem<(), (), M>) -> &mut Self {
self.schedule.add_systems(system);
self
}
pub fn add_chained_systems<M>(
&mut self,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self {
self.schedule.add_systems(systems);
self
}
pub fn step(&mut self, dt: Duration) -> u32 {
if self.step.is_zero() {
return 0;
}
self.accumulator += dt;
let mut ran = 0;
while self.accumulator >= self.step && ran < self.max_steps.max(1) {
self.accumulator -= self.step;
self.tick();
ran += 1;
}
if self.accumulator >= self.step {
self.accumulator = Duration::ZERO;
}
ran
}
pub fn leftover(&self) -> Duration {
self.accumulator
}
pub fn alpha(&self) -> f32 {
let step = self.step.as_secs_f64();
if step <= 0.0 {
return 0.0;
}
(self.accumulator.as_secs_f64() / step).clamp(0.0, 1.0) as f32
}
pub fn tick(&mut self) {
{
let mut time = self.world.resource_mut::<SimTime>();
time.delta_secs = self.step.as_secs_f32();
time.elapsed_secs += self.step.as_secs_f64();
}
self.schedule.run(&mut self.world);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_steps_accumulate() {
let mut sim = Sim::with_default_step();
let ran = sim.step(Duration::from_nanos(50_000_001));
assert_eq!(ran, 3);
let time = sim.world.resource::<SimTime>();
assert!((time.elapsed_secs - 3.0 / 60.0).abs() < 1e-9);
}
#[test]
fn zero_step_never_ticks() {
let mut sim = Sim::new(Duration::ZERO);
let ran = sim.step(Duration::from_secs(10));
assert_eq!(ran, 0);
let time = sim.world.resource::<SimTime>();
assert_eq!(time.elapsed_secs, 0.0);
}
#[test]
fn spiral_of_death_is_capped() {
let mut sim = Sim::with_default_step();
sim.max_steps = 4;
let ran = sim.step(Duration::from_secs(10));
assert_eq!(ran, 4);
assert_eq!(sim.leftover(), Duration::ZERO);
}
#[test]
fn alpha_is_interpolation_fraction() {
let mut sim = Sim::new(Duration::from_millis(16));
sim.step(Duration::from_millis(8));
assert!((sim.alpha() - 0.5).abs() < 1e-6, "got {}", sim.alpha());
sim.step(Duration::from_millis(8));
assert!(sim.alpha() < 1e-6, "step boundary, got {}", sim.alpha());
}
}