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}