Skip to main content

game_gem/
time.rs

1//! Time management: delta time, FPS tracking, timers, and time utilities.
2//!
3//! game-gem provides a first-class [`Time`] resource that's automatically
4//! updated each frame, unlike macroquad which exposes raw `get_time()`.
5
6use std::time::Instant;
7
8/// Time resource, updated automatically every frame.
9///
10/// Access via `ctx.time` inside your [`GameState`] implementation.
11#[derive(Debug)]
12pub struct Time {
13    /// Time since the engine started (seconds).
14    elapsed: f64,
15    /// Wall-clock start time (kept for callers that want absolute time references).
16    #[allow(dead_code)]
17    start: Instant,
18    /// Time of the previous frame.
19    last_frame: Instant,
20    /// Duration of the last frame in seconds.
21    delta: f64,
22    /// Exponential moving average of delta (for stable FPS display).
23    smoothed_delta: f64,
24    /// Target fixed timestep (for fixed-update patterns).
25    fixed_timestep: f64,
26    /// Accumulator for fixed updates.
27    fixed_accumulator: f64,
28    /// Whether to use a fixed timestep.
29    use_fixed_timestep: bool,
30    /// Frame counter since start.
31    frame_count: u64,
32    /// Scale applied to delta time (for slow-motion, pause, etc.).
33    time_scale: f64,
34    /// FPS tracker.
35    fps_tracker: FpsTracker,
36}
37
38#[derive(Debug)]
39struct FpsTracker {
40    samples: [f64; 60],
41    index: usize,
42    filled: bool,
43}
44
45impl Default for FpsTracker {
46    fn default() -> Self {
47        Self {
48            samples: [0.0; 60],
49            index: 0,
50            filled: false,
51        }
52    }
53}
54
55impl FpsTracker {
56    fn record(&mut self, delta: f64) {
57        self.samples[self.index] = delta;
58        self.index = (self.index + 1) % self.samples.len();
59        if self.index == 0 {
60            self.filled = true;
61        }
62    }
63
64    fn average_fps(&self) -> f64 {
65        let count = if self.filled {
66            self.samples.len()
67        } else {
68            self.index
69        };
70        if count == 0 {
71            return 0.0;
72        }
73        let sum: f64 = self.samples[..count].iter().sum();
74        if sum < 1e-9 {
75            return f64::INFINITY;
76        }
77        count as f64 / sum
78    }
79}
80
81impl Time {
82    /// Create a new Time resource.
83    pub(crate) fn new() -> Self {
84        let now = Instant::now();
85        Self {
86            elapsed: 0.0,
87            start: now,
88            last_frame: now,
89            delta: 0.0,
90            smoothed_delta: 1.0 / 60.0,
91            fixed_timestep: 1.0 / 60.0,
92            fixed_accumulator: 0.0,
93            use_fixed_timestep: false,
94            frame_count: 0,
95            time_scale: 1.0,
96            fps_tracker: FpsTracker::default(),
97        }
98    }
99
100    /// Called at the start of each frame.
101    pub(crate) fn tick(&mut self) {
102        let now = Instant::now();
103        let raw_delta = now.duration_since(self.last_frame).as_secs_f64();
104        self.last_frame = now;
105
106        // Clamp delta to avoid spiral-of-death on lag spikes
107        let clamped = raw_delta.min(0.25);
108        self.delta = clamped * self.time_scale;
109        self.smoothed_delta = self.smoothed_delta * 0.9 + self.delta * 0.1;
110        self.elapsed += self.delta;
111        self.frame_count += 1;
112        self.fps_tracker.record(self.delta);
113    }
114
115    /// Check if a fixed update should run this frame.
116    /// Returns the number of fixed steps to perform.
117    pub(crate) fn fixed_step_count(&mut self) -> u32 {
118        if !self.use_fixed_timestep {
119            return 0;
120        }
121        self.fixed_accumulator += self.delta;
122        let max_steps = 5; // Prevent spiral of death
123        let mut steps = 0;
124        while self.fixed_accumulator >= self.fixed_timestep && steps < max_steps {
125            self.fixed_accumulator -= self.fixed_timestep;
126            steps += 1;
127        }
128        if steps >= max_steps {
129            self.fixed_accumulator = 0.0;
130        }
131        steps
132    }
133
134    // --- Public getters ---
135
136    /// Seconds since the engine started.
137    #[inline]
138    pub fn elapsed(&self) -> f64 {
139        self.elapsed
140    }
141
142    /// Duration of the last frame in seconds (affected by time_scale).
143    #[inline]
144    pub fn delta(&self) -> f64 {
145        self.delta
146    }
147
148    /// Smoothed delta time (exponential moving average).
149    #[inline]
150    pub fn smoothed_delta(&self) -> f64 {
151        self.smoothed_delta
152    }
153
154    /// Frames per second (averaged over last 60 frames).
155    #[inline]
156    pub fn fps(&self) -> f64 {
157        self.fps_tracker.average_fps()
158    }
159
160    /// Current frame number (starts at 0, increments each frame).
161    #[inline]
162    pub fn frame_count(&self) -> u64 {
163        self.frame_count
164    }
165
166    /// Current time scale factor (1.0 = normal, 0.0 = paused, 2.0 = double speed).
167    #[inline]
168    pub fn time_scale(&self) -> f64 {
169        self.time_scale
170    }
171
172    /// Set the time scale factor.
173    pub fn set_time_scale(&mut self, scale: f64) {
174        self.time_scale = scale.clamp(0.0, 10.0);
175    }
176
177    /// Enable fixed timestep updates at the given rate (in Hz).
178    pub fn set_fixed_timestep(&mut self, fps: u32) {
179        self.use_fixed_timestep = true;
180        self.fixed_timestep = 1.0 / fps as f64;
181        self.fixed_accumulator = 0.0;
182    }
183
184    /// Disable fixed timestep updates.
185    pub fn disable_fixed_timestep(&mut self) {
186        self.use_fixed_timestep = false;
187        self.fixed_accumulator = 0.0;
188    }
189
190    /// Whether fixed timestep is enabled.
191    #[inline]
192    pub fn is_fixed_timestep_enabled(&self) -> bool {
193        self.use_fixed_timestep
194    }
195
196    /// The fixed timestep duration in seconds.
197    #[inline]
198    pub fn fixed_delta(&self) -> f64 {
199        self.fixed_timestep
200    }
201}
202
203// --- Standalone timer utility ---
204
205/// A simple countdown timer.
206///
207/// Useful for cooldowns, delays, and timed events.
208///
209/// # Example
210/// ```
211/// let mut timer = Timer::from_seconds(2.0, false);
212/// // In update loop:
213/// if timer.tick(delta) {
214///     println!("2 seconds elapsed!");
215/// }
216/// ```
217#[derive(Debug, Clone)]
218pub struct Timer {
219    duration: f64,
220    elapsed: f64,
221    repeating: bool,
222    finished: bool,
223}
224
225impl Timer {
226    /// Create a timer with the given duration.
227    pub fn from_seconds(seconds: f64, repeating: bool) -> Self {
228        Self {
229            duration: seconds,
230            elapsed: 0.0,
231            repeating,
232            finished: false,
233        }
234    }
235
236    /// Advance the timer by `delta` seconds. Returns `true` when it fires.
237    pub fn tick(&mut self, delta: f64) -> bool {
238        if self.finished && !self.repeating {
239            return false;
240        }
241        self.elapsed += delta;
242        if self.elapsed >= self.duration {
243            if self.repeating {
244                self.elapsed -= self.duration;
245            } else {
246                self.finished = true;
247                self.elapsed = self.duration;
248            }
249            true
250        } else {
251            false
252        }
253    }
254
255    /// Reset the timer to zero.
256    pub fn reset(&mut self) {
257        self.elapsed = 0.0;
258        self.finished = false;
259    }
260
261    /// Set a new duration and reset.
262    pub fn set_duration(&mut self, seconds: f64) {
263        self.duration = seconds;
264        self.reset();
265    }
266
267    /// Fraction completed (0.0 to 1.0).
268    pub fn fraction(&self) -> f32 {
269        (self.elapsed / self.duration).clamp(0.0, 1.0) as f32
270    }
271
272    /// Whether the timer has finished (non-repeating only).
273    #[inline]
274    pub fn is_finished(&self) -> bool {
275        self.finished
276    }
277
278    /// Whether the timer is still running.
279    #[inline]
280    pub fn is_running(&self) -> bool {
281        !self.finished
282    }
283
284    /// Remaining time in seconds.
285    pub fn remaining(&self) -> f64 {
286        (self.duration - self.elapsed).max(0.0)
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_timer_basic() {
296        let mut t = Timer::from_seconds(1.0, false);
297        assert!(!t.tick(0.5));
298        assert!(!t.tick(0.4));
299        assert!(t.tick(0.2)); // total 1.1s
300        assert!(t.is_finished());
301        assert!(!t.tick(0.5)); // non-repeating, won't fire again
302    }
303
304    #[test]
305    fn test_timer_repeating() {
306        let mut t = Timer::from_seconds(1.0, true);
307        assert!(!t.tick(0.5));
308        assert!(t.tick(0.5));
309        assert!(!t.tick(0.3));
310        assert!(t.tick(0.7));
311    }
312}