Skip to main content

optic_loop/
time.rs

1use std::time::Instant;
2
3/// Frame timing data — delta time, smoothed FPS, and elapsed wall-clock time.
4///
5/// `Time` is updated once per frame by the engine (via [`Time::update`])
6/// before user code runs. It provides three fundamental measurements:
7///
8/// | Field | Meaning | Use case |
9/// |---|---|---|
10/// | [`delta`](Time::delta) | Seconds since the last frame | Physics, movement (frame-rate independent) |
11/// | [`fps`](Time::fps) | Smoothed frames-per-second | Display in HUD, performance monitoring |
12/// | [`elapsed`](Time::elapsed) | Total seconds since start | Timers, countdowns, replays |
13///
14/// # FPS smoothing
15///
16/// The FPS value is a running average of the last **32** delta samples.
17/// This avoids jarring frame-to-frame fluctuations while still responding
18/// to sustained changes in frame rate. The smoothing window size is fixed.
19///
20/// # Which timing method should I use?
21///
22/// | You want… | Use |
23/// |---|---|
24/// | Frame-independent movement speed | [`delta`](Time::delta) |
25/// | "Time since game started" | [`elapsed`](Time::elapsed) |
26/// | Timestamp for logging / profiling | [`now_ms`](Time::now_ms) |
27/// | "5 seconds from now" | `let deadline = time.elapsed() + 5.0;` |
28///
29/// # Example
30///
31/// ```ignore
32/// use optic_loop::Time;
33///
34/// let mut time = Time::new();
35///
36/// // Simulate a 16 ms frame
37/// std::thread::sleep(std::time::Duration::from_millis(16));
38/// time.update();
39///
40/// println!("Delta: {:.4}s  FPS: {:.1}", time.delta(), time.fps());
41/// // → "Delta: 0.0160s  FPS: 62.5"
42/// ```
43pub struct Time {
44    pub fps: f64,
45    pub delta: f64,
46    pub tick_count: u64,
47    pub elapsed: f64,
48    pub start_time: Instant,
49    pub prev_time: Instant,
50    pub prev_sec: Instant,
51    pub local_tick: u32,
52    prev_deltas: Vec<f64>,
53    prev_deltas_size: usize,
54}
55
56impl Time {
57    /// Creates a new timer with all counters at zero.
58    ///
59    /// The internal `start_time` is set to the current wall-clock instant.
60    pub fn new() -> Self {
61        let now = Instant::now();
62        Self {
63            fps: 0.0,
64            delta: 0.0,
65            tick_count: 0,
66            elapsed: 0.0,
67            start_time: now,
68            prev_time: now,
69            prev_sec: now,
70            local_tick: 0,
71            prev_deltas: Vec::with_capacity(32),
72            prev_deltas_size: 32,
73        }
74    }
75
76    /// Advances the timer by one frame.
77    ///
78    /// Called automatically by the engine each frame. Increments
79    /// `tick_count`, computes `delta` and `elapsed` from wall-clock time,
80    /// and updates the smoothed FPS.
81    ///
82    /// You should not normally call this manually — [`Game`](crate::Game)
83    /// and [`GameLoop`](crate::GameLoop) call it before invoking user code.
84    pub fn update(&mut self) {
85        self.tick_count += 1;
86        self.local_tick += 1;
87        let now = Instant::now();
88
89        self.elapsed = now.duration_since(self.start_time).as_secs_f64();
90        self.delta = now.duration_since(self.prev_time).as_secs_f64();
91        self.prev_time = now;
92
93        self.prev_deltas.push(self.delta);
94        if self.prev_deltas.len() > self.prev_deltas_size {
95            self.prev_deltas.remove(0);
96        }
97
98        let avg = self.prev_deltas.iter().sum::<f64>() / self.prev_deltas.len() as f64;
99        self.fps = if avg > 0.0 { 1.0 / avg } else { 0.0 };
100
101        if now.duration_since(self.prev_sec).as_secs_f64() >= 1.0 {
102            self.local_tick = 0;
103            self.prev_sec = now;
104        }
105    }
106
107    /// Smoothed frames-per-second (averaged over the last 32 frames).
108    pub fn fps(&self) -> f64 { self.fps }
109
110    /// Delta time in seconds since the last frame.
111    ///
112    /// Multiply speeds by this value to get frame-rate-independent motion:
113    ///
114    /// ```ignore
115    /// let speed = 10.0; // units per second
116    /// entity.position.x += speed * time.delta();
117    /// ```
118    pub fn delta(&self) -> f64 { self.delta }
119
120    /// Total wall-clock seconds since [`Time::new`] was called.
121    pub fn elapsed(&self) -> f64 { self.elapsed }
122
123    /// Current elapsed time in seconds (re-queries `Instant::now`).
124    ///
125    /// Unlike [`elapsed`](Time::elapsed), this always returns the very latest
126    /// wall-clock time, even if `update` has not been called yet for this
127    /// frame.
128    pub fn now(&self) -> f64 {
129        Instant::now().duration_since(self.start_time).as_secs_f64()
130    }
131
132    /// Current elapsed time in milliseconds.
133    pub fn now_ms(&self) -> u64 {
134        Instant::now().duration_since(self.start_time).as_millis() as u64
135    }
136
137    /// Alias for [`now_ms`](Time::now_ms).
138    pub fn now_as_ms(&self) -> u64 {
139        self.now_ms()
140    }
141
142    /// Current elapsed time in nanoseconds.
143    pub fn now_as_ns(&self) -> u64 {
144        Instant::now().duration_since(self.start_time).as_nanos() as u64
145    }
146
147    /// Blocks the current thread for the given fractional seconds.
148    ///
149    /// Useful for frame-rate limiting in non-interactive contexts.
150    pub fn sleep(&self, secs: f64) {
151        std::thread::sleep(std::time::Duration::from_secs_f64(secs));
152    }
153
154    /// Blocks the current thread for the given milliseconds.
155    pub fn sleep_ms(&self, millis: u64) {
156        std::thread::sleep(std::time::Duration::from_millis(millis));
157    }
158
159    /// Blocks the current thread for the given nanoseconds.
160    pub fn sleep_ns(&self, nanos: u64) {
161        std::thread::sleep(std::time::Duration::from_nanos(nanos));
162    }
163}