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,
36 pub max_steps: u32,
41}
42
43#[derive(Resource, Clone, Copy, Debug, Default)]
45pub struct SimTime {
46 pub elapsed_secs: f64,
48 pub delta_secs: f32,
50}
51
52impl Sim {
53 pub fn new(step: Duration) -> Self {
54 let mut world = World::new();
55 world.init_resource::<SimTime>();
56 Self {
57 world,
58 schedule: Schedule::default(),
59 accumulator: Duration::ZERO,
60 step,
61 time_scale: 1.0,
62 max_steps: 8,
63 }
64 }
65
66 pub fn with_default_step() -> Self {
68 Self::new(Duration::from_secs_f64(1.0 / 60.0))
69 }
70
71 pub fn add_system<M>(&mut self, system: impl IntoSystem<(), (), M>) -> &mut Self {
73 self.schedule.add_systems(system);
74 self
75 }
76
77 pub fn add_chained_systems<M>(
81 &mut self,
82 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
83 ) -> &mut Self {
84 self.schedule.add_systems(systems);
85 self
86 }
87
88 pub fn step(&mut self, dt: Duration) -> u32 {
97 self.step_with(dt, |_| {})
98 }
99
100 pub fn step_with(&mut self, dt: Duration, mut after_tick: impl FnMut(&mut World)) -> u32 {
101 if self.step.is_zero() {
102 return 0;
103 }
104 self.accumulator += dt;
105 let mut ran = 0;
106 while self.accumulator >= self.step && ran < self.max_steps.max(1) {
107 self.accumulator -= self.step;
108 self.tick_with(&mut after_tick);
109 ran += 1;
110 }
111 if self.accumulator >= self.step {
112 let step_ns = self.step.as_nanos();
114 if step_ns > 0 {
115 let acc_ns = self.accumulator.as_nanos() % step_ns;
116 self.accumulator = Duration::from_nanos(acc_ns as u64);
117 } else {
118 self.accumulator = Duration::ZERO;
119 }
120 }
121 ran
122 }
123
124 pub fn leftover(&self) -> Duration {
126 self.accumulator
127 }
128
129 pub fn alpha(&self) -> f32 {
135 let step = self.step.as_secs_f64();
136 if step <= 0.0 {
137 return 0.0;
138 }
139 (self.accumulator.as_secs_f64() / step).clamp(0.0, 1.0) as f32
140 }
141
142 pub fn tick(&mut self) {
145 self.tick_with(|_| {});
146 }
147
148 fn tick_with(&mut self, after_tick: impl FnOnce(&mut World)) {
149 let time_scale = if self.time_scale.is_finite() && self.time_scale > 0.0 {
150 self.time_scale
151 } else {
152 1.0
153 };
154 {
155 let mut time = self.world.resource_mut::<SimTime>();
156 time.delta_secs = self.step.as_secs_f32() * time_scale;
157 time.elapsed_secs += self.step.as_secs_f64() * time_scale as f64;
158 }
159 self.schedule.run(&mut self.world);
160 after_tick(&mut self.world);
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn fixed_steps_accumulate() {
170 let mut sim = Sim::with_default_step();
171 let ran = sim.step(Duration::from_nanos(50_000_001));
172 assert_eq!(ran, 3);
173 let time = sim.world.resource::<SimTime>();
174 assert!((time.elapsed_secs - 3.0 / 60.0).abs() < 1e-9);
175 }
176
177 #[test]
178 fn post_tick_runs_after_each_fixed_step() {
179 use std::sync::{Arc, Mutex};
180
181 let log = Arc::new(Mutex::new(Vec::new()));
182 let sim_log = log.clone();
183 let mut sim = Sim::with_default_step();
184 sim.add_system(move || sim_log.lock().unwrap().push("sim"));
185 let post_log = log.clone();
186
187 let ran = sim.step_with(Duration::from_millis(51), move |_| {
188 post_log.lock().unwrap().push("post");
189 });
190
191 assert_eq!(ran, 3);
192 assert_eq!(
193 *log.lock().unwrap(),
194 ["sim", "post", "sim", "post", "sim", "post"]
195 );
196 }
197
198 #[test]
199 fn zero_step_never_ticks() {
200 let mut sim = Sim::new(Duration::ZERO);
201 let ran = sim.step(Duration::from_secs(10));
202 assert_eq!(ran, 0);
203 let time = sim.world.resource::<SimTime>();
204 assert_eq!(time.elapsed_secs, 0.0);
205 }
206
207 #[test]
208 fn spiral_of_death_is_capped() {
209 let mut sim = Sim::with_default_step();
210 sim.max_steps = 4;
211 let ran = sim.step(Duration::from_secs(10));
212 assert_eq!(ran, 4);
213 assert!(
214 sim.leftover() < sim.step,
215 "keeps only fraction, got {:?}",
216 sim.leftover()
217 );
218 }
219
220 #[test]
221 fn alpha_is_interpolation_fraction() {
222 let mut sim = Sim::new(Duration::from_millis(16));
223 sim.step(Duration::from_millis(8));
224 assert!((sim.alpha() - 0.5).abs() < 1e-6, "got {}", sim.alpha());
225 sim.step(Duration::from_millis(8));
226 assert!(sim.alpha() < 1e-6, "step boundary, got {}", sim.alpha());
227 }
228
229 #[test]
230 fn time_scale_changes_simulated_delta_without_changing_wall_accumulation() {
231 let mut sim = Sim::with_default_step();
232 sim.time_scale = 0.25;
233 let ran = sim.step(Duration::from_nanos(16_666_667));
234 let time = sim.world.resource::<SimTime>();
235 assert_eq!(ran, 1);
236 assert!((time.delta_secs - 1.0 / 240.0).abs() < 1e-6);
237 assert!((time.elapsed_secs - 1.0 / 240.0).abs() < 1e-9);
238 }
239
240 #[test]
241 fn chained_systems_run_in_order() {
242 use std::sync::{Arc, Mutex};
243 let log: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
244 let (a_log, b_log, c_log) = (log.clone(), log.clone(), log.clone());
245 let mut sim = Sim::with_default_step();
246 sim.add_chained_systems(
247 (
248 move || a_log.lock().unwrap().push("a"),
249 move || b_log.lock().unwrap().push("b"),
250 move || c_log.lock().unwrap().push("c"),
251 )
252 .chain(),
253 );
254 sim.tick();
255 assert_eq!(*log.lock().unwrap(), vec!["a", "b", "c"]);
256 }
257}