1use crate::schedule::Schedule;
4use crate::virtual_time::VirtualTime;
5use crate::world::World;
6
7pub struct FixedTimestep {
13 pub step: f64,
15 accumulator: f64,
17 pub tick_count: u64,
19}
20
21impl FixedTimestep {
22 pub const MIN_HZ: f64 = 1.0;
24 pub const MAX_HZ: f64 = 240.0;
27
28 pub fn new(tick_rate: f64) -> Self {
30 assert!(
31 (Self::MIN_HZ..=Self::MAX_HZ).contains(&tick_rate),
32 "tick rate {tick_rate} Hz out of range [{}, {}]",
33 Self::MIN_HZ,
34 Self::MAX_HZ,
35 );
36 Self {
37 step: 1.0 / tick_rate,
38 accumulator: 0.0,
39 tick_count: 0,
40 }
41 }
42
43 pub fn default_rts() -> Self {
45 Self::new(10.0)
46 }
47
48 pub fn strategy() -> Self {
50 Self::new(20.0)
51 }
52
53 pub fn action() -> Self {
55 Self::new(30.0)
56 }
57
58 pub fn fast() -> Self {
60 Self::new(60.0)
61 }
62
63 pub fn tick_rate(&self) -> f64 {
65 1.0 / self.step
66 }
67}
68
69pub fn tick(world: &mut World, schedule: &mut Schedule, elapsed: f64) -> u32 {
77 let virtual_elapsed = if let Some(mut vt) = world.try_take_resource::<VirtualTime>() {
79 let ve = vt.effective_elapsed(elapsed);
80 vt.elapsed += ve;
81 world.insert_resource(vt);
82 ve
83 } else {
84 elapsed
85 };
86
87 let mut ts = world.take_resource::<FixedTimestep>();
89 ts.accumulator += virtual_elapsed;
90
91 let mut ticks = 0u32;
92 while ts.accumulator >= ts.step {
93 ts.accumulator -= ts.step;
94 ts.tick_count += 1;
95 ticks += 1;
96
97 world.advance_tick();
100
101 world.insert_resource(FixedTimestep {
103 step: ts.step,
104 accumulator: ts.accumulator,
105 tick_count: ts.tick_count,
106 });
107 schedule.run(world);
108 ts = world.take_resource::<FixedTimestep>();
110 }
111
112 world.insert_resource(ts);
114 ticks
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::component::Component;
121 use crate::system_param::{QueryMut, Res};
122 use crate::virtual_time::VirtualTime;
123
124 #[derive(Debug)]
125 struct TickCounter(u32);
126 impl Component for TickCounter {}
127
128 fn count_system(mut counters: QueryMut<'_, TickCounter>) {
129 for (_, counter) in counters.iter_mut() {
130 counter.0 += 1;
131 }
132 }
133
134 #[test]
135 fn genre_presets() {
136 let rts = FixedTimestep::default_rts();
137 assert!((rts.tick_rate() - 10.0).abs() < f64::EPSILON);
138
139 let strat = FixedTimestep::strategy();
140 assert!((strat.tick_rate() - 20.0).abs() < f64::EPSILON);
141
142 let act = FixedTimestep::action();
143 assert!((act.tick_rate() - 30.0).abs() < f64::EPSILON);
144
145 let fps = FixedTimestep::fast();
146 assert!((fps.tick_rate() - 60.0).abs() < f64::EPSILON);
147 }
148
149 #[test]
150 fn rejects_below_min_hz() {
151 let result = std::panic::catch_unwind(|| FixedTimestep::new(0.5));
152 assert!(result.is_err());
153 }
154
155 #[test]
156 fn rejects_above_max_hz() {
157 let result = std::panic::catch_unwind(|| FixedTimestep::new(500.0));
158 assert!(result.is_err());
159 }
160
161 #[test]
162 fn accepts_boundary_values() {
163 let low = FixedTimestep::new(FixedTimestep::MIN_HZ);
164 assert!((low.tick_rate() - 1.0).abs() < f64::EPSILON);
165
166 let high = FixedTimestep::new(FixedTimestep::MAX_HZ);
167 assert!((high.tick_rate() - 240.0).abs() < f64::EPSILON);
168 }
169
170 #[test]
171 fn fixed_timestep_creation() {
172 let ts = FixedTimestep::new(10.0);
173 assert!((ts.step - 0.1).abs() < f64::EPSILON);
174 assert_eq!(ts.tick_count, 0);
175 assert!((ts.tick_rate() - 10.0).abs() < f64::EPSILON);
176 }
177
178 #[test]
179 fn tick_runs_correct_number_of_times() {
180 let mut world = World::new();
181 world.insert_resource(FixedTimestep::new(10.0));
182 world.spawn((TickCounter(0),));
183
184 let mut schedule = Schedule::new();
185 schedule.add_system::<(QueryMut<'_, TickCounter>,)>("simulate", "count", count_system);
186
187 let ticks = tick(&mut world, &mut schedule, 0.25);
189 assert_eq!(ticks, 2);
190
191 let counts: Vec<u32> = world.query::<&TickCounter>().map(|(_, c)| c.0).collect();
192 assert_eq!(counts, vec![2]);
193 }
194
195 #[test]
196 fn accumulator_carries_remainder() {
197 let mut world = World::new();
198 world.insert_resource(FixedTimestep::new(10.0));
199 world.spawn((TickCounter(0),));
200
201 let mut schedule = Schedule::new();
202 schedule.add_system::<(QueryMut<'_, TickCounter>,)>("simulate", "count", count_system);
203
204 let ticks = tick(&mut world, &mut schedule, 0.05);
206 assert_eq!(ticks, 0);
207
208 let ticks = tick(&mut world, &mut schedule, 0.06);
210 assert_eq!(ticks, 1);
211
212 let counts: Vec<u32> = world.query::<&TickCounter>().map(|(_, c)| c.0).collect();
213 assert_eq!(counts, vec![1]);
214 }
215
216 #[test]
217 fn tick_count_increments() {
218 let mut world = World::new();
219 world.insert_resource(FixedTimestep::new(10.0));
220
221 let mut schedule = Schedule::new();
222
223 tick(&mut world, &mut schedule, 0.35); let ts = world.resource::<FixedTimestep>();
225 assert_eq!(ts.tick_count, 3);
226 }
227
228 #[test]
229 fn systems_can_read_timestep() {
230 fn read_step(ts: Res<'_, FixedTimestep>) {
231 assert!((ts.step - 0.1).abs() < f64::EPSILON);
232 }
233
234 let mut world = World::new();
235 world.insert_resource(FixedTimestep::new(10.0));
236
237 let mut schedule = Schedule::new();
238 schedule.add_system::<(Res<'_, FixedTimestep>,)>("simulate", "read_step", read_step);
239
240 tick(&mut world, &mut schedule, 0.1);
241 }
242
243 #[test]
244 fn no_virtual_time_unchanged_behavior() {
245 let mut world = World::new();
247 world.insert_resource(FixedTimestep::new(10.0));
248 world.spawn((TickCounter(0),));
249
250 let mut schedule = Schedule::new();
251 schedule.add_system::<(QueryMut<'_, TickCounter>,)>("simulate", "count", count_system);
252
253 let ticks = tick(&mut world, &mut schedule, 0.25);
254 assert_eq!(ticks, 2);
255 }
256
257 #[test]
258 fn virtual_time_paused_zero_ticks() {
259 let mut world = World::new();
260 world.insert_resource(FixedTimestep::new(10.0));
261 let mut vt = VirtualTime::new();
262 vt.paused = true;
263 world.insert_resource(vt);
264 world.spawn((TickCounter(0),));
265
266 let mut schedule = Schedule::new();
267 schedule.add_system::<(QueryMut<'_, TickCounter>,)>("simulate", "count", count_system);
268
269 let ticks = tick(&mut world, &mut schedule, 1.0);
270 assert_eq!(ticks, 0);
271
272 let counts: Vec<u32> = world.query::<&TickCounter>().map(|(_, c)| c.0).collect();
273 assert_eq!(counts, vec![0]);
274 }
275
276 #[test]
277 fn virtual_time_scale_doubles_ticks() {
278 let mut world = World::new();
279 world.insert_resource(FixedTimestep::new(10.0));
280 let mut vt = VirtualTime::new();
281 vt.scale = 2.0;
282 world.insert_resource(vt);
283 world.spawn((TickCounter(0),));
284
285 let mut schedule = Schedule::new();
286 schedule.add_system::<(QueryMut<'_, TickCounter>,)>("simulate", "count", count_system);
287
288 let ticks = tick(&mut world, &mut schedule, 0.1);
290 assert_eq!(ticks, 2);
291 }
292
293 #[test]
294 fn virtual_time_max_delta_prevents_death_spiral() {
295 let mut world = World::new();
296 world.insert_resource(FixedTimestep::new(10.0));
297 world.insert_resource(VirtualTime::new()); world.spawn((TickCounter(0),));
299
300 let mut schedule = Schedule::new();
301 schedule.add_system::<(QueryMut<'_, TickCounter>,)>("simulate", "count", count_system);
302
303 let ticks = tick(&mut world, &mut schedule, 5.0);
305 assert_eq!(ticks, 2);
306 }
307
308 #[test]
309 fn virtual_time_elapsed_accumulates() {
310 let mut world = World::new();
311 world.insert_resource(FixedTimestep::new(10.0));
312 world.insert_resource(VirtualTime::new());
313
314 let mut schedule = Schedule::new();
315
316 tick(&mut world, &mut schedule, 0.1);
317 tick(&mut world, &mut schedule, 0.15);
318
319 let vt = world.resource::<VirtualTime>();
320 assert!((vt.elapsed - 0.25).abs() < f64::EPSILON);
321 }
322}