1use web_time::Duration;
17
18use bevy_ecs::prelude::*;
19use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule};
20use bevy_ecs::system::ScheduleSystem;
21
22pub use bevy_ecs;
23
24pub struct Sim {
27 pub world: World,
29 schedule: Schedule,
30 accumulator: Duration,
31 pub step: Duration,
35 pub max_steps: u32,
40}
41
42#[derive(Resource, Clone, Copy, Debug, Default)]
44pub struct SimTime {
45 pub elapsed_secs: f64,
47 pub delta_secs: f32,
49}
50
51impl Sim {
52 pub fn new(step: Duration) -> Self {
53 let mut world = World::new();
54 world.init_resource::<SimTime>();
55 Self {
56 world,
57 schedule: Schedule::default(),
58 accumulator: Duration::ZERO,
59 step,
60 max_steps: 8,
61 }
62 }
63
64 pub fn with_default_step() -> Self {
66 Self::new(Duration::from_secs_f64(1.0 / 60.0))
67 }
68
69 pub fn add_system<M>(&mut self, system: impl IntoSystem<(), (), M>) -> &mut Self {
71 self.schedule.add_systems(system);
72 self
73 }
74
75 pub fn add_chained_systems<M>(
79 &mut self,
80 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
81 ) -> &mut Self {
82 self.schedule.add_systems(systems);
83 self
84 }
85
86 pub fn step(&mut self, dt: Duration) -> u32 {
95 if self.step.is_zero() {
96 return 0;
97 }
98 self.accumulator += dt;
99 let mut ran = 0;
100 while self.accumulator >= self.step && ran < self.max_steps.max(1) {
101 self.accumulator -= self.step;
102 self.tick();
103 ran += 1;
104 }
105 if self.accumulator >= self.step {
106 let step_ns = self.step.as_nanos();
108 if step_ns > 0 {
109 let acc_ns = self.accumulator.as_nanos() % step_ns;
110 self.accumulator = Duration::from_nanos(acc_ns as u64);
111 } else {
112 self.accumulator = Duration::ZERO;
113 }
114 }
115 ran
116 }
117
118 pub fn leftover(&self) -> Duration {
120 self.accumulator
121 }
122
123 pub fn alpha(&self) -> f32 {
129 let step = self.step.as_secs_f64();
130 if step <= 0.0 {
131 return 0.0;
132 }
133 (self.accumulator.as_secs_f64() / step).clamp(0.0, 1.0) as f32
134 }
135
136 pub fn tick(&mut self) {
139 {
140 let mut time = self.world.resource_mut::<SimTime>();
141 time.delta_secs = self.step.as_secs_f32();
142 time.elapsed_secs += self.step.as_secs_f64();
143 }
144 self.schedule.run(&mut self.world);
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn fixed_steps_accumulate() {
154 let mut sim = Sim::with_default_step();
155 let ran = sim.step(Duration::from_nanos(50_000_001));
156 assert_eq!(ran, 3);
157 let time = sim.world.resource::<SimTime>();
158 assert!((time.elapsed_secs - 3.0 / 60.0).abs() < 1e-9);
159 }
160
161 #[test]
162 fn zero_step_never_ticks() {
163 let mut sim = Sim::new(Duration::ZERO);
164 let ran = sim.step(Duration::from_secs(10));
165 assert_eq!(ran, 0);
166 let time = sim.world.resource::<SimTime>();
167 assert_eq!(time.elapsed_secs, 0.0);
168 }
169
170 #[test]
171 fn spiral_of_death_is_capped() {
172 let mut sim = Sim::with_default_step();
173 sim.max_steps = 4;
174 let ran = sim.step(Duration::from_secs(10));
175 assert_eq!(ran, 4);
176 assert!(
177 sim.leftover() < sim.step,
178 "keeps only fraction, got {:?}",
179 sim.leftover()
180 );
181 }
182
183 #[test]
184 fn alpha_is_interpolation_fraction() {
185 let mut sim = Sim::new(Duration::from_millis(16));
186 sim.step(Duration::from_millis(8));
187 assert!((sim.alpha() - 0.5).abs() < 1e-6, "got {}", sim.alpha());
188 sim.step(Duration::from_millis(8));
189 assert!(sim.alpha() < 1e-6, "step boundary, got {}", sim.alpha());
190 }
191
192 #[test]
193 fn chained_systems_run_in_order() {
194 use std::sync::{Arc, Mutex};
195 let log: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
196 let (a_log, b_log, c_log) = (log.clone(), log.clone(), log.clone());
197 let mut sim = Sim::with_default_step();
198 sim.add_chained_systems(
199 (
200 move || a_log.lock().unwrap().push("a"),
201 move || b_log.lock().unwrap().push("b"),
202 move || c_log.lock().unwrap().push("c"),
203 )
204 .chain(),
205 );
206 sim.tick();
207 assert_eq!(*log.lock().unwrap(), vec!["a", "b", "c"]);
208 }
209}