use web_time::Duration;
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule};
use bevy_ecs::system::ScheduleSystem;
pub use bevy_ecs;
pub mod state;
pub mod task;
pub mod timer;
pub use state::{NextState, State, States, in_state};
pub use task::{Task, TaskPool};
pub use timer::{StepTimer, TimerMode};
pub struct Sim {
pub world: World,
schedule: Schedule,
state_appliers: Vec<Box<dyn FnMut(&mut World) + Send + Sync>>,
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(),
state_appliers: Vec::new(),
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 add_state<S: States>(&mut self, initial: S) -> &mut Self {
self.world.insert_resource(State::<S>::new(initial));
self.world.insert_resource(NextState::<S>::new());
state::init_schedules::<S>(&mut self.world);
self.state_appliers.push(Box::new(state::apply::<S>));
self
}
pub fn on_enter<S, M>(
&mut self,
state: S,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self
where
S: States,
{
state::add_enter(&mut self.world, state, systems);
self
}
pub fn on_exit<S, M>(
&mut self,
state: S,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self
where
S: States,
{
state::add_exit(&mut self.world, state, systems);
self
}
pub fn on_transition<S, M>(
&mut self,
exited: S,
entered: S,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self
where
S: States,
{
state::add_transition(&mut self.world, exited, entered, systems);
self
}
pub fn set_state<S: States>(&mut self, next: S) -> &mut Self {
self.world.resource_mut::<NextState<S>>().set(next);
self
}
pub fn state<S: States>(&self) -> &S {
self.world.resource::<State<S>>().get()
}
pub fn step(&mut self, dt: Duration) -> u32 {
self.step_with(dt, |_| {})
}
fn paused(&self) -> bool {
self.time_scale.is_finite() && self.time_scale <= 0.0
}
pub fn step_with(&mut self, dt: Duration, mut after_tick: impl FnMut(&mut World)) -> u32 {
if self.step.is_zero() {
return 0;
}
if self.paused() {
self.accumulator = Duration::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)) {
if self.paused() {
return;
}
let time_scale = if self.time_scale.is_finite() {
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;
}
let Sim {
state_appliers,
world,
schedule,
..
} = self;
for apply in state_appliers.iter_mut() {
apply(world);
}
schedule.run(world);
after_tick(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_time_scale_pauses_without_banking_wall_time() {
let per_frame = Sim::with_default_step().step(Duration::from_millis(50));
assert!(per_frame > 0);
let mut sim = Sim::with_default_step();
sim.step(Duration::from_millis(50));
let before = sim.world.resource::<SimTime>().elapsed_secs;
sim.time_scale = 0.0;
assert_eq!(
sim.step(Duration::from_secs(5)),
0,
"paused sim runs no ticks"
);
sim.tick();
assert_eq!(
sim.world.resource::<SimTime>().elapsed_secs,
before,
"paused sim does not advance its clock"
);
sim.time_scale = 1.0;
assert_eq!(
sim.step(Duration::from_millis(50)),
per_frame,
"resuming must not fire a catch-up burst"
);
}
#[test]
fn non_finite_time_scale_runs_at_normal_speed() {
let mut sim = Sim::with_default_step();
sim.time_scale = f32::NAN;
sim.step(Duration::from_millis(51));
let time = sim.world.resource::<SimTime>();
assert!(
(time.delta_secs - 1.0 / 60.0).abs() < 1e-6,
"a non-finite scale must not stall the delta, got {}",
time.delta_secs
);
assert!(time.elapsed_secs > 0.0, "the clock still advances");
}
#[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"]);
}
}