good_web_game/timer.rs
1//! Timing and measurement functions.
2//!
3//! good-web-game does not try to do any framerate limitation by default (because it can't).
4//!
5//! For a detailed tutorial in how to handle frame timings in games,
6//! see <http://gafferongames.com/game-physics/fix-your-timestep/>
7
8use crate::context::Context;
9
10use std::cmp;
11use std::f64;
12use std::time;
13use std::time::Duration;
14
15type Instant = f64;
16
17/// Returns the system time.
18pub fn time() -> f64 {
19 miniquad::date::now()
20}
21
22/// A simple buffer that fills
23/// up to a limit and then holds the last
24/// N items that have been inserted into it,
25/// overwriting old ones in a round-robin fashion.
26///
27/// It's not quite a ring buffer 'cause you can't
28/// remove items from it, it just holds the last N
29/// things.
30#[derive(Debug, Clone)]
31struct LogBuffer<T>
32where
33 T: Clone,
34{
35 head: usize,
36 size: usize,
37 /// The number of actual samples inserted, used for
38 /// smarter averaging.
39 samples: usize,
40 contents: Vec<T>,
41}
42
43impl<T> LogBuffer<T>
44where
45 T: Clone + Copy,
46{
47 fn new(size: usize, init_val: T) -> LogBuffer<T> {
48 LogBuffer {
49 head: 0,
50 size,
51 contents: vec![init_val; size],
52 // Never divide by 0
53 samples: 1,
54 }
55 }
56
57 /// Pushes a new item into the `LogBuffer`, overwriting
58 /// the oldest item in it.
59 fn push(&mut self, item: T) {
60 self.head = (self.head + 1) % self.contents.len();
61 self.contents[self.head] = item;
62 self.size = cmp::min(self.size + 1, self.contents.len());
63 self.samples += 1;
64 }
65
66 /// Returns a slice pointing at the contents of the buffer.
67 /// They are in *no particular order*, and if not all the
68 /// slots are filled, the empty slots will be present but
69 /// contain the initial value given to [`new()`](#method.new).
70 ///
71 /// We're only using this to log FPS for a short time,
72 /// so we don't care for the second or so when it's inaccurate.
73 fn contents(&self) -> &[T] {
74 if self.samples > self.size {
75 &self.contents
76 } else {
77 &self.contents[..self.samples]
78 }
79 }
80
81 /// Returns the most recent value in the buffer.
82 fn latest(&self) -> T {
83 self.contents[self.head]
84 }
85}
86
87/// A structure that contains our time-tracking state.
88#[derive(Debug)]
89pub struct TimeContext {
90 init_instant: Instant,
91 last_instant: Instant,
92 frame_durations: LogBuffer<Duration>,
93 residual_update_dt: Duration,
94 frame_count: usize,
95}
96
97// How many frames we log update times for.
98const TIME_LOG_FRAMES: usize = 200;
99
100impl TimeContext {
101 /// Creates a new `TimeContext` and initializes the start to this instant.
102 pub fn new() -> TimeContext {
103 let initial_dt = time::Duration::from_millis(16);
104 TimeContext {
105 init_instant: time(),
106 last_instant: time(),
107 frame_durations: LogBuffer::new(TIME_LOG_FRAMES, initial_dt),
108 residual_update_dt: time::Duration::from_secs(0),
109 frame_count: 0,
110 }
111 }
112
113 /// Update the state of the `TimeContext` to record that
114 /// another frame has taken place. Necessary for the FPS
115 /// tracking and [`check_update_time()`](fn.check_update_time.html)
116 /// functions to work.
117 ///
118 /// It's usually not necessary to call this function yourself,
119 /// [`event::run()`](../event/fn.run.html) will do it for you.
120 pub fn tick(&mut self) {
121 let now = time();
122 let time_since_last = now - self.last_instant;
123 self.frame_durations.push(f64_to_duration(time_since_last));
124 self.last_instant = now;
125 self.frame_count += 1;
126
127 self.residual_update_dt += f64_to_duration(time_since_last);
128 }
129}
130
131impl Default for TimeContext {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137/// Get the time between the start of the last frame and the current one;
138/// in other words, the length of the last frame.
139pub fn delta(ctx: &Context) -> Duration {
140 let tc = &ctx.timer_context;
141 tc.frame_durations.latest()
142}
143
144/// Gets the average time of a frame, averaged
145/// over the last 200 frames.
146pub fn average_delta(ctx: &Context) -> Duration {
147 let tc = &ctx.timer_context;
148 let sum: Duration = tc.frame_durations.contents().iter().sum();
149 // If our buffer is actually full, divide by its size.
150 // Otherwise divide by the number of samples we've added
151 if tc.frame_durations.samples > tc.frame_durations.size {
152 sum / (tc.frame_durations.size as u32)
153 } else {
154 sum / (tc.frame_durations.samples as u32)
155 }
156}
157
158/// A convenience function to convert a Rust `Duration` type
159/// to a (less precise but more useful) `f64`.
160///
161/// Does not make sure that the `Duration` is within the bounds
162/// of the `f64`.
163pub fn duration_to_f64(d: Duration) -> f64 {
164 d.as_secs() as f64 + d.subsec_nanos() as f64 * 1e-9
165}
166
167/// A convenience function to create a Rust `Duration` type
168/// from a (less precise but more useful) `f64`.
169///
170/// Only handles positive numbers correctly.
171pub fn f64_to_duration(t: f64) -> Duration {
172 let seconds = t.trunc();
173 let nanos = t.fract() * 1.0e9;
174
175 Duration::new(seconds as u64, nanos as u32)
176}
177
178/// Returns a `Duration` representing how long each
179/// frame should be to match the given fps.
180///
181/// Approximately.
182fn fps_as_duration(fps: u32) -> Duration {
183 let target_dt_seconds = 1.0 / f64::from(fps);
184 f64_to_duration(target_dt_seconds)
185}
186
187/// Gets the FPS of the game, averaged over the last
188/// 200 frames.
189pub fn fps(ctx: &Context) -> f64 {
190 let duration_per_frame = average_delta(ctx);
191 let seconds_per_frame = duration_to_f64(duration_per_frame);
192 1.0 / seconds_per_frame
193}
194
195/// Returns the time since the game was initialized,
196/// as reported by the system clock.
197pub fn time_since_start(ctx: &Context) -> Duration {
198 let tc = &ctx.timer_context;
199 f64_to_duration(time() - tc.init_instant)
200}
201
202/// same as time_since_start, but without f64_to_duration inside
203pub fn time_since_start_f64(ctx: &Context) -> f64 {
204 let tc = &ctx.timer_context;
205 time() - tc.init_instant
206}
207
208/// This function will return true if the time since the
209/// last [`update()`](../event/trait.EventHandler.html#tymethod.update)
210/// call has been equal to or greater to
211/// the update FPS indicated by the `target_fps`.
212/// It keeps track of fractional frames, so if you want
213/// 60 fps (16.67 ms/frame) and the game stutters so that
214/// there is 40 ms between `update()` calls, this will return
215/// `true` twice, and take the remaining 6.67 ms into account
216/// in the next frame.
217///
218/// The intention is to for it to be called in a while loop
219/// in your `update()` callback:
220///
221/// ```rust
222/// # use ggez::*;
223/// # fn update_game_physics() -> GameResult { Ok(()) }
224/// # struct State;
225/// # impl ya2d::event::EventHandler for State {
226/// fn update(&mut self, ctx: &mut Context) -> GameResult {
227/// while(timer::check_update_time(ctx, 60)) {
228/// update_game_physics()?;
229/// }
230/// Ok(())
231/// }
232/// # fn draw(&mut self, _ctx: &mut Context) -> GameResult { Ok(()) }
233/// # }
234/// ```
235pub fn check_update_time(ctx: &mut Context, target_fps: u32) -> bool {
236 let timedata = &mut ctx.timer_context;
237
238 let target_dt = fps_as_duration(target_fps);
239 if timedata.residual_update_dt > target_dt {
240 timedata.residual_update_dt -= target_dt;
241 true
242 } else {
243 false
244 }
245}
246
247/// Returns the fractional amount of a frame not consumed
248/// by [`check_update_time()`](fn.check_update_time.html).
249/// For example, if the desired
250/// update frame time is 40 ms (25 fps), and 45 ms have
251/// passed since the last frame, [`check_update_time()`](fn.check_update_time.html)
252/// will return `true` and `remaining_update_time()` will
253/// return 5 ms -- the amount of time "overflowing" from one
254/// frame to the next.
255///
256/// The intention is for it to be called in your
257/// [`draw()`](../event/trait.EventHandler.html#tymethod.draw) callback
258/// to interpolate physics states for smooth rendering.
259/// (see <http://gafferongames.com/game-physics/fix-your-timestep/>)
260pub fn remaining_update_time(ctx: &mut Context) -> Duration {
261 ctx.timer_context.residual_update_dt
262}
263
264/// Gets the number of times the game has gone through its event loop.
265///
266/// Specifically, the number of times that [`TimeContext::tick()`](struct.TimeContext.html#method.tick)
267/// has been called by it.
268pub fn ticks(ctx: &Context) -> usize {
269 ctx.timer_context.frame_count
270}