1pub const DEFAULT_FIXED_HZ: u32 = 60;
7pub const MAX_STEPS: u32 = 5; #[derive(Debug, Clone)]
11pub struct FixedTime {
12 pub tick: u64,
14 pub fixed_dt: f32,
15 tick_rate: u32,
16 accumulator: f32,
18 freeze_frames: u16,
21}
22
23impl FixedTime {
24 pub fn new(hz: u32) -> Self {
25 Self {
26 tick: 0,
27 fixed_dt: 1.0 / hz as f32,
28 tick_rate: hz,
29 accumulator: 0.0,
30 freeze_frames: 0,
31 }
32 }
33
34 pub fn accumulate(&mut self, dt: f32) -> u32 {
37 if self.freeze_frames > 0 {
38 self.freeze_frames -= 1;
39 return 0;
40 }
41 self.accumulator += dt;
42 (self.accumulator / self.fixed_dt).min(MAX_STEPS as f32) as u32
43 }
44
45 pub fn advance(&mut self) {
47 self.accumulator -= self.fixed_dt;
48 self.tick += 1;
49 }
50
51 pub fn interpolation_alpha(&self) -> f32 {
54 self.accumulator / self.fixed_dt
55 }
56
57 pub fn tick_rate(&self) -> u32 {
58 self.tick_rate
59 }
60
61 pub fn set_tick_rate(&mut self, hz: u32) {
62 self.tick_rate = hz;
63 self.fixed_dt = 1.0 / hz as f32;
64 }
65
66 pub fn freeze(&mut self, frames: u16) {
68 self.freeze_frames = self.freeze_frames.max(frames);
69 }
70
71 pub fn is_frozen(&self) -> bool {
72 self.freeze_frames > 0
73 }
74
75 pub fn freeze_remaining(&self) -> u16 {
76 self.freeze_frames
77 }
78}
79
80impl Default for FixedTime {
81 fn default() -> Self {
82 Self::new(DEFAULT_FIXED_HZ)
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn single_step_at_exact_dt() {
92 let mut ft = FixedTime::new(60);
93 let steps = ft.accumulate(1.0 / 60.0);
94 assert_eq!(steps, 1);
95 ft.advance();
96 assert_eq!(ft.tick, 1);
97 }
98
99 #[test]
100 fn no_step_below_threshold() {
101 let mut ft = FixedTime::new(60);
102 let steps = ft.accumulate(0.005); assert_eq!(steps, 0);
104 assert_eq!(ft.tick, 0);
105 }
106
107 #[test]
108 fn multiple_steps_on_slow_frame() {
109 let mut ft = FixedTime::new(60);
110 let steps = ft.accumulate(1.0 / 30.0); assert_eq!(steps, 2);
112 ft.advance();
113 ft.advance();
114 assert_eq!(ft.tick, 2);
115 }
116
117 #[test]
118 fn accumulator_carries_remainder() {
119 let mut ft = FixedTime::new(60);
120 let dt = 1.0 / 60.0;
121 ft.accumulate(dt * 1.5);
123 ft.advance(); assert_eq!(ft.tick, 1);
125 let alpha = ft.interpolation_alpha();
127 assert!((alpha - 0.5).abs() < 0.01, "alpha was {alpha}");
128 }
129
130 #[test]
131 fn set_tick_rate_changes_dt() {
132 let mut ft = FixedTime::new(60);
133 ft.set_tick_rate(30);
134 assert_eq!(ft.tick_rate(), 30);
135 assert!((ft.fixed_dt - 1.0 / 30.0).abs() < f32::EPSILON);
136 }
137
138 #[test]
139 fn tick_monotonically_increases() {
140 let mut ft = FixedTime::new(60);
141 let dt = 1.0 / 60.0;
142 for expected in 1..=100u64 {
143 ft.accumulate(dt);
144 ft.advance();
145 assert_eq!(ft.tick, expected);
146 }
147 }
148
149 #[test]
150 fn freeze_blocks_accumulation() {
151 let mut ft = FixedTime::new(60);
152 ft.freeze(3);
153 assert!(ft.is_frozen());
154 assert_eq!(ft.accumulate(1.0 / 60.0), 0);
156 assert_eq!(ft.freeze_remaining(), 2);
157 assert_eq!(ft.accumulate(1.0 / 60.0), 0);
158 assert_eq!(ft.freeze_remaining(), 1);
159 assert_eq!(ft.accumulate(1.0 / 60.0), 0);
160 assert_eq!(ft.freeze_remaining(), 0);
161 assert!(!ft.is_frozen());
162 assert_eq!(ft.accumulate(1.0 / 60.0), 1);
164 }
165
166 #[test]
167 fn freeze_stacks_by_max() {
168 let mut ft = FixedTime::new(60);
169 ft.freeze(3);
170 ft.freeze(5);
171 assert_eq!(ft.freeze_remaining(), 5);
172 ft.freeze(2); assert_eq!(ft.freeze_remaining(), 5);
174 }
175}