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 mod state;
25pub mod task;
26pub mod timer;
27
28pub use state::{NextState, State, States, in_state};
29pub use task::{Task, TaskPool};
30pub use timer::{StepTimer, TimerMode};
31
32pub struct Sim {
35 pub world: World,
37 schedule: Schedule,
38 state_appliers: Vec<Box<dyn FnMut(&mut World) + Send + Sync>>,
40 accumulator: Duration,
41 pub step: Duration,
45 pub time_scale: f32,
48 pub max_steps: u32,
53}
54
55#[derive(Resource, Clone, Copy, Debug, Default)]
57pub struct SimTime {
58 pub elapsed_secs: f64,
60 pub delta_secs: f32,
62}
63
64impl Sim {
65 pub fn new(step: Duration) -> Self {
66 let mut world = World::new();
67 world.init_resource::<SimTime>();
68 Self {
69 world,
70 schedule: Schedule::default(),
71 state_appliers: Vec::new(),
72 accumulator: Duration::ZERO,
73 step,
74 time_scale: 1.0,
75 max_steps: 8,
76 }
77 }
78
79 pub fn with_default_step() -> Self {
81 Self::new(Duration::from_secs_f64(1.0 / 60.0))
82 }
83
84 pub fn add_system<M>(&mut self, system: impl IntoSystem<(), (), M>) -> &mut Self {
86 self.schedule.add_systems(system);
87 self
88 }
89
90 pub fn add_chained_systems<M>(
94 &mut self,
95 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
96 ) -> &mut Self {
97 self.schedule.add_systems(systems);
98 self
99 }
100
101 pub fn add_state<S: States>(&mut self, initial: S) -> &mut Self {
107 self.world.insert_resource(State::<S>::new(initial));
108 self.world.insert_resource(NextState::<S>::new());
109 state::init_schedules::<S>(&mut self.world);
110 self.state_appliers.push(Box::new(state::apply::<S>));
111 self
112 }
113
114 pub fn on_enter<S, M>(
118 &mut self,
119 state: S,
120 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
121 ) -> &mut Self
122 where
123 S: States,
124 {
125 state::add_enter(&mut self.world, state, systems);
126 self
127 }
128
129 pub fn on_exit<S, M>(
131 &mut self,
132 state: S,
133 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
134 ) -> &mut Self
135 where
136 S: States,
137 {
138 state::add_exit(&mut self.world, state, systems);
139 self
140 }
141
142 pub fn on_transition<S, M>(
144 &mut self,
145 exited: S,
146 entered: S,
147 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
148 ) -> &mut Self
149 where
150 S: States,
151 {
152 state::add_transition(&mut self.world, exited, entered, systems);
153 self
154 }
155
156 pub fn set_state<S: States>(&mut self, next: S) -> &mut Self {
159 self.world.resource_mut::<NextState<S>>().set(next);
160 self
161 }
162
163 pub fn state<S: States>(&self) -> &S {
165 self.world.resource::<State<S>>().get()
166 }
167
168 pub fn step(&mut self, dt: Duration) -> u32 {
177 self.step_with(dt, |_| {})
178 }
179
180 fn paused(&self) -> bool {
183 self.time_scale.is_finite() && self.time_scale <= 0.0
184 }
185
186 pub fn step_with(&mut self, dt: Duration, mut after_tick: impl FnMut(&mut World)) -> u32 {
187 if self.step.is_zero() {
188 return 0;
189 }
190 if self.paused() {
191 self.accumulator = Duration::ZERO;
194 return 0;
195 }
196 self.accumulator += dt;
197 let mut ran = 0;
198 while self.accumulator >= self.step && ran < self.max_steps.max(1) {
199 self.accumulator -= self.step;
200 self.tick_with(&mut after_tick);
201 ran += 1;
202 }
203 if self.accumulator >= self.step {
204 let step_ns = self.step.as_nanos();
206 if step_ns > 0 {
207 let acc_ns = self.accumulator.as_nanos() % step_ns;
208 self.accumulator = Duration::from_nanos(acc_ns as u64);
209 } else {
210 self.accumulator = Duration::ZERO;
211 }
212 }
213 ran
214 }
215
216 pub fn leftover(&self) -> Duration {
218 self.accumulator
219 }
220
221 pub fn alpha(&self) -> f32 {
227 let step = self.step.as_secs_f64();
228 if step <= 0.0 {
229 return 0.0;
230 }
231 (self.accumulator.as_secs_f64() / step).clamp(0.0, 1.0) as f32
232 }
233
234 pub fn tick(&mut self) {
237 self.tick_with(|_| {});
238 }
239
240 fn tick_with(&mut self, after_tick: impl FnOnce(&mut World)) {
241 if self.paused() {
242 return;
243 }
244 let time_scale = if self.time_scale.is_finite() {
245 self.time_scale
246 } else {
247 1.0
248 };
249 {
250 let mut time = self.world.resource_mut::<SimTime>();
251 time.delta_secs = self.step.as_secs_f32() * time_scale;
252 time.elapsed_secs += self.step.as_secs_f64() * time_scale as f64;
253 }
254 let Sim {
255 state_appliers,
256 world,
257 schedule,
258 ..
259 } = self;
260 for apply in state_appliers.iter_mut() {
261 apply(world);
262 }
263 schedule.run(world);
264 after_tick(world);
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn fixed_steps_accumulate() {
274 let mut sim = Sim::with_default_step();
275 let ran = sim.step(Duration::from_nanos(50_000_001));
276 assert_eq!(ran, 3);
277 let time = sim.world.resource::<SimTime>();
278 assert!((time.elapsed_secs - 3.0 / 60.0).abs() < 1e-9);
279 }
280
281 #[test]
282 fn zero_time_scale_pauses_without_banking_wall_time() {
283 let per_frame = Sim::with_default_step().step(Duration::from_millis(50));
284 assert!(per_frame > 0);
285
286 let mut sim = Sim::with_default_step();
287 sim.step(Duration::from_millis(50));
288 let before = sim.world.resource::<SimTime>().elapsed_secs;
289
290 sim.time_scale = 0.0;
291 assert_eq!(
292 sim.step(Duration::from_secs(5)),
293 0,
294 "paused sim runs no ticks"
295 );
296 sim.tick();
297 assert_eq!(
298 sim.world.resource::<SimTime>().elapsed_secs,
299 before,
300 "paused sim does not advance its clock"
301 );
302
303 sim.time_scale = 1.0;
304 assert_eq!(
305 sim.step(Duration::from_millis(50)),
306 per_frame,
307 "resuming must not fire a catch-up burst"
308 );
309 }
310
311 #[test]
312 fn non_finite_time_scale_runs_at_normal_speed() {
313 let mut sim = Sim::with_default_step();
314 sim.time_scale = f32::NAN;
315 sim.step(Duration::from_millis(51));
316 let time = sim.world.resource::<SimTime>();
317 assert!(
318 (time.delta_secs - 1.0 / 60.0).abs() < 1e-6,
319 "a non-finite scale must not stall the delta, got {}",
320 time.delta_secs
321 );
322 assert!(time.elapsed_secs > 0.0, "the clock still advances");
323 }
324
325 #[test]
326 fn post_tick_runs_after_each_fixed_step() {
327 use std::sync::{Arc, Mutex};
328
329 let log = Arc::new(Mutex::new(Vec::new()));
330 let sim_log = log.clone();
331 let mut sim = Sim::with_default_step();
332 sim.add_system(move || sim_log.lock().unwrap().push("sim"));
333 let post_log = log.clone();
334
335 let ran = sim.step_with(Duration::from_millis(51), move |_| {
336 post_log.lock().unwrap().push("post");
337 });
338
339 assert_eq!(ran, 3);
340 assert_eq!(
341 *log.lock().unwrap(),
342 ["sim", "post", "sim", "post", "sim", "post"]
343 );
344 }
345
346 #[test]
347 fn zero_step_never_ticks() {
348 let mut sim = Sim::new(Duration::ZERO);
349 let ran = sim.step(Duration::from_secs(10));
350 assert_eq!(ran, 0);
351 let time = sim.world.resource::<SimTime>();
352 assert_eq!(time.elapsed_secs, 0.0);
353 }
354
355 #[test]
356 fn spiral_of_death_is_capped() {
357 let mut sim = Sim::with_default_step();
358 sim.max_steps = 4;
359 let ran = sim.step(Duration::from_secs(10));
360 assert_eq!(ran, 4);
361 assert!(
362 sim.leftover() < sim.step,
363 "keeps only fraction, got {:?}",
364 sim.leftover()
365 );
366 }
367
368 #[test]
369 fn alpha_is_interpolation_fraction() {
370 let mut sim = Sim::new(Duration::from_millis(16));
371 sim.step(Duration::from_millis(8));
372 assert!((sim.alpha() - 0.5).abs() < 1e-6, "got {}", sim.alpha());
373 sim.step(Duration::from_millis(8));
374 assert!(sim.alpha() < 1e-6, "step boundary, got {}", sim.alpha());
375 }
376
377 #[test]
378 fn time_scale_changes_simulated_delta_without_changing_wall_accumulation() {
379 let mut sim = Sim::with_default_step();
380 sim.time_scale = 0.25;
381 let ran = sim.step(Duration::from_nanos(16_666_667));
382 let time = sim.world.resource::<SimTime>();
383 assert_eq!(ran, 1);
384 assert!((time.delta_secs - 1.0 / 240.0).abs() < 1e-6);
385 assert!((time.elapsed_secs - 1.0 / 240.0).abs() < 1e-9);
386 }
387
388 #[test]
389 fn chained_systems_run_in_order() {
390 use std::sync::{Arc, Mutex};
391 let log: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
392 let (a_log, b_log, c_log) = (log.clone(), log.clone(), log.clone());
393 let mut sim = Sim::with_default_step();
394 sim.add_chained_systems(
395 (
396 move || a_log.lock().unwrap().push("a"),
397 move || b_log.lock().unwrap().push("b"),
398 move || c_log.lock().unwrap().push("c"),
399 )
400 .chain(),
401 );
402 sim.tick();
403 assert_eq!(*log.lock().unwrap(), vec!["a", "b", "c"]);
404 }
405}