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 time_scale: f32,
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,
time_scale: 1.0,
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 {
self.step_with(dt, |_| {})
}
pub fn step_with(&mut self, dt: Duration, mut after_tick: impl FnMut(&mut World)) -> 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_with(&mut after_tick);
ran += 1;
}
if self.accumulator >= self.step {
let step_ns = self.step.as_nanos();
if step_ns > 0 {
let acc_ns = self.accumulator.as_nanos() % step_ns;
self.accumulator = Duration::from_nanos(acc_ns as u64);
} else {
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) {
self.tick_with(|_| {});
}
fn tick_with(&mut self, after_tick: impl FnOnce(&mut World)) {
let time_scale = if self.time_scale.is_finite() && self.time_scale > 0.0 {
self.time_scale
} else {
1.0
};
{
let mut time = self.world.resource_mut::<SimTime>();
time.delta_secs = self.step.as_secs_f32() * time_scale;
time.elapsed_secs += self.step.as_secs_f64() * time_scale as f64;
}
self.schedule.run(&mut self.world);
after_tick(&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 post_tick_runs_after_each_fixed_step() {
use std::sync::{Arc, Mutex};
let log = Arc::new(Mutex::new(Vec::new()));
let sim_log = log.clone();
let mut sim = Sim::with_default_step();
sim.add_system(move || sim_log.lock().unwrap().push("sim"));
let post_log = log.clone();
let ran = sim.step_with(Duration::from_millis(51), move |_| {
post_log.lock().unwrap().push("post");
});
assert_eq!(ran, 3);
assert_eq!(
*log.lock().unwrap(),
["sim", "post", "sim", "post", "sim", "post"]
);
}
#[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!(
sim.leftover() < sim.step,
"keeps only fraction, got {:?}",
sim.leftover()
);
}
#[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());
}
#[test]
fn time_scale_changes_simulated_delta_without_changing_wall_accumulation() {
let mut sim = Sim::with_default_step();
sim.time_scale = 0.25;
let ran = sim.step(Duration::from_nanos(16_666_667));
let time = sim.world.resource::<SimTime>();
assert_eq!(ran, 1);
assert!((time.delta_secs - 1.0 / 240.0).abs() < 1e-6);
assert!((time.elapsed_secs - 1.0 / 240.0).abs() < 1e-9);
}
#[test]
fn chained_systems_run_in_order() {
use std::sync::{Arc, Mutex};
let log: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
let (a_log, b_log, c_log) = (log.clone(), log.clone(), log.clone());
let mut sim = Sim::with_default_step();
sim.add_chained_systems(
(
move || a_log.lock().unwrap().push("a"),
move || b_log.lock().unwrap().push("b"),
move || c_log.lock().unwrap().push("c"),
)
.chain(),
);
sim.tick();
assert_eq!(*log.lock().unwrap(), vec!["a", "b", "c"]);
}
}