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}