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 time_scale: f32,
38 pub max_steps: u32,
43}
44
45#[derive(Resource, Clone, Copy, Debug, Default)]
47pub struct SimTime {
48 pub elapsed_secs: f64,
50 pub delta_secs: f32,
52}
53
54impl Sim {
55 pub fn new(step: Duration) -> Self {
56 let mut world = World::new();
57 world.init_resource::<SimTime>();
58 Self {
59 world,
60 schedule: Schedule::default(),
61 accumulator: Duration::ZERO,
62 step,
63 time_scale: 1.0,
64 max_steps: 8,
65 }
66 }
67
68 pub fn with_default_step() -> Self {
70 Self::new(Duration::from_secs_f64(1.0 / 60.0))
71 }
72
73 pub fn add_system<M>(&mut self, system: impl IntoSystem<(), (), M>) -> &mut Self {
75 self.schedule.add_systems(system);
76 self
77 }
78
79 pub fn add_chained_systems<M>(
83 &mut self,
84 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
85 ) -> &mut Self {
86 self.schedule.add_systems(systems);
87 self
88 }
89
90 pub fn step(&mut self, dt: Duration) -> u32 {
99 self.step_with(dt, |_| {})
100 }
101
102 fn paused(&self) -> bool {
105 self.time_scale.is_finite() && self.time_scale <= 0.0
106 }
107
108 pub fn step_with(&mut self, dt: Duration, mut after_tick: impl FnMut(&mut World)) -> u32 {
109 if self.step.is_zero() {
110 return 0;
111 }
112 if self.paused() {
113 self.accumulator = Duration::ZERO;
116 return 0;
117 }
118 self.accumulator += dt;
119 let mut ran = 0;
120 while self.accumulator >= self.step && ran < self.max_steps.max(1) {
121 self.accumulator -= self.step;
122 self.tick_with(&mut after_tick);
123 ran += 1;
124 }
125 if self.accumulator >= self.step {
126 let step_ns = self.step.as_nanos();
128 if step_ns > 0 {
129 let acc_ns = self.accumulator.as_nanos() % step_ns;
130 self.accumulator = Duration::from_nanos(acc_ns as u64);
131 } else {
132 self.accumulator = Duration::ZERO;
133 }
134 }
135 ran
136 }
137
138 pub fn leftover(&self) -> Duration {
140 self.accumulator
141 }
142
143 pub fn alpha(&self) -> f32 {
149 let step = self.step.as_secs_f64();
150 if step <= 0.0 {
151 return 0.0;
152 }
153 (self.accumulator.as_secs_f64() / step).clamp(0.0, 1.0) as f32
154 }
155
156 pub fn tick(&mut self) {
159 self.tick_with(|_| {});
160 }
161
162 fn tick_with(&mut self, after_tick: impl FnOnce(&mut World)) {
163 if self.paused() {
164 return;
165 }
166 let time_scale = if self.time_scale.is_finite() {
167 self.time_scale
168 } else {
169 1.0
170 };
171 {
172 let mut time = self.world.resource_mut::<SimTime>();
173 time.delta_secs = self.step.as_secs_f32() * time_scale;
174 time.elapsed_secs += self.step.as_secs_f64() * time_scale as f64;
175 }
176 self.schedule.run(&mut self.world);
177 after_tick(&mut self.world);
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn fixed_steps_accumulate() {
187 let mut sim = Sim::with_default_step();
188 let ran = sim.step(Duration::from_nanos(50_000_001));
189 assert_eq!(ran, 3);
190 let time = sim.world.resource::<SimTime>();
191 assert!((time.elapsed_secs - 3.0 / 60.0).abs() < 1e-9);
192 }
193
194 #[test]
195 fn zero_time_scale_pauses_without_banking_wall_time() {
196 let per_frame = Sim::with_default_step().step(Duration::from_millis(50));
197 assert!(per_frame > 0);
198
199 let mut sim = Sim::with_default_step();
200 sim.step(Duration::from_millis(50));
201 let before = sim.world.resource::<SimTime>().elapsed_secs;
202
203 sim.time_scale = 0.0;
204 assert_eq!(
205 sim.step(Duration::from_secs(5)),
206 0,
207 "paused sim runs no ticks"
208 );
209 sim.tick();
210 assert_eq!(
211 sim.world.resource::<SimTime>().elapsed_secs,
212 before,
213 "paused sim does not advance its clock"
214 );
215
216 sim.time_scale = 1.0;
217 assert_eq!(
218 sim.step(Duration::from_millis(50)),
219 per_frame,
220 "resuming must not fire a catch-up burst"
221 );
222 }
223
224 #[test]
225 fn non_finite_time_scale_runs_at_normal_speed() {
226 let mut sim = Sim::with_default_step();
227 sim.time_scale = f32::NAN;
228 sim.step(Duration::from_millis(51));
229 let time = sim.world.resource::<SimTime>();
230 assert!(
231 (time.delta_secs - 1.0 / 60.0).abs() < 1e-6,
232 "a non-finite scale must not stall the delta, got {}",
233 time.delta_secs
234 );
235 assert!(time.elapsed_secs > 0.0, "the clock still advances");
236 }
237
238 #[test]
239 fn post_tick_runs_after_each_fixed_step() {
240 use std::sync::{Arc, Mutex};
241
242 let log = Arc::new(Mutex::new(Vec::new()));
243 let sim_log = log.clone();
244 let mut sim = Sim::with_default_step();
245 sim.add_system(move || sim_log.lock().unwrap().push("sim"));
246 let post_log = log.clone();
247
248 let ran = sim.step_with(Duration::from_millis(51), move |_| {
249 post_log.lock().unwrap().push("post");
250 });
251
252 assert_eq!(ran, 3);
253 assert_eq!(
254 *log.lock().unwrap(),
255 ["sim", "post", "sim", "post", "sim", "post"]
256 );
257 }
258
259 #[test]
260 fn zero_step_never_ticks() {
261 let mut sim = Sim::new(Duration::ZERO);
262 let ran = sim.step(Duration::from_secs(10));
263 assert_eq!(ran, 0);
264 let time = sim.world.resource::<SimTime>();
265 assert_eq!(time.elapsed_secs, 0.0);
266 }
267
268 #[test]
269 fn spiral_of_death_is_capped() {
270 let mut sim = Sim::with_default_step();
271 sim.max_steps = 4;
272 let ran = sim.step(Duration::from_secs(10));
273 assert_eq!(ran, 4);
274 assert!(
275 sim.leftover() < sim.step,
276 "keeps only fraction, got {:?}",
277 sim.leftover()
278 );
279 }
280
281 #[test]
282 fn alpha_is_interpolation_fraction() {
283 let mut sim = Sim::new(Duration::from_millis(16));
284 sim.step(Duration::from_millis(8));
285 assert!((sim.alpha() - 0.5).abs() < 1e-6, "got {}", sim.alpha());
286 sim.step(Duration::from_millis(8));
287 assert!(sim.alpha() < 1e-6, "step boundary, got {}", sim.alpha());
288 }
289
290 #[test]
291 fn time_scale_changes_simulated_delta_without_changing_wall_accumulation() {
292 let mut sim = Sim::with_default_step();
293 sim.time_scale = 0.25;
294 let ran = sim.step(Duration::from_nanos(16_666_667));
295 let time = sim.world.resource::<SimTime>();
296 assert_eq!(ran, 1);
297 assert!((time.delta_secs - 1.0 / 240.0).abs() < 1e-6);
298 assert!((time.elapsed_secs - 1.0 / 240.0).abs() < 1e-9);
299 }
300
301 #[test]
302 fn chained_systems_run_in_order() {
303 use std::sync::{Arc, Mutex};
304 let log: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
305 let (a_log, b_log, c_log) = (log.clone(), log.clone(), log.clone());
306 let mut sim = Sim::with_default_step();
307 sim.add_chained_systems(
308 (
309 move || a_log.lock().unwrap().push("a"),
310 move || b_log.lock().unwrap().push("b"),
311 move || c_log.lock().unwrap().push("c"),
312 )
313 .chain(),
314 );
315 sim.tick();
316 assert_eq!(*log.lock().unwrap(), vec!["a", "b", "c"]);
317 }
318}