pebble/time.rs
1//! Frame timing — `TimePlugin` inserts [`Time`] as a resource and ticks it
2//! once per frame in `PreUpdate`, before any gameplay system runs.
3
4use std::time::Duration;
5// `web_time::Instant`, not `std::time::Instant`: the latter's `wasm32`
6// support depends on how the final binary is linked (some setups panic
7// on `Instant::now()`), whereas `web_time` always calls
8// `performance.now()` directly. Identical API, backed by
9// `std::time::Instant` itself on every other target.
10use web_time::Instant;
11
12use crate::{
13 app::SystemStage,
14 ecs::{plugin::Plugin, system::ResMut},
15};
16
17/// This tick's frame timing. Cheap to read every system that needs it —
18/// `delta_seconds`/`elapsed_seconds` are plain `f32`, no locking.
19pub struct Time {
20 start: Instant,
21 last_tick: Instant,
22 delta: Duration,
23 elapsed: Duration,
24}
25
26impl Time {
27 fn new() -> Self {
28 let now = Instant::now();
29 Self { start: now, last_tick: now, delta: Duration::ZERO, elapsed: Duration::ZERO }
30 }
31
32 fn tick(&mut self) {
33 let now = Instant::now();
34 self.delta = now.duration_since(self.last_tick);
35 self.last_tick = now;
36 self.elapsed = now.duration_since(self.start);
37 }
38
39 /// Time since the previous tick.
40 pub fn delta(&self) -> Duration {
41 self.delta
42 }
43
44 /// [`Time::delta`] as seconds — the number to multiply a per-second
45 /// rate by (`transform.x += speed * time.delta_seconds()`).
46 pub fn delta_seconds(&self) -> f32 {
47 self.delta.as_secs_f32()
48 }
49
50 /// Time since `TimePlugin` was built (app startup), as of this tick.
51 pub fn elapsed(&self) -> Duration {
52 self.elapsed
53 }
54
55 /// [`Time::elapsed`] as seconds.
56 pub fn elapsed_seconds(&self) -> f32 {
57 self.elapsed.as_secs_f32()
58 }
59
60 /// `1.0 / delta_seconds()` — this tick's instantaneous frame rate.
61 /// `0.0` on the very first tick (`delta` is still zero) rather than
62 /// dividing by zero. Jitters frame to frame same as `delta` itself;
63 /// average it yourself over a window if you want a smoothed display
64 /// value.
65 pub fn fps(&self) -> f32 {
66 let seconds = self.delta_seconds();
67 if seconds > 0.0 { 1.0 / seconds } else { 0.0 }
68 }
69}
70
71fn tick_time(mut time: ResMut<Time>) {
72 time.tick();
73}
74
75/// Registers [`Time`] as a resource and advances it once per frame.
76///
77/// `App::new()` already builds this in, so `Res<Time>` works out of the
78/// box — you don't need to `add_plugin(TimePlugin)` yourself. It's still a
79/// public plugin (rather than baking the resource/system straight into
80/// `App::new()`) for the rare case of assembling an `App` by some other
81/// path that skips `App::new()`. Idempotent either way: it only inserts
82/// [`Time`]/registers its tick system the first time it actually runs, so
83/// an explicit `add_plugin(TimePlugin)` alongside the automatic one is
84/// harmless rather than double-ticking.
85///
86/// Backend-agnostic — works the same with `pebble::wgpu` or a hand-rolled
87/// `Backend`, and even with no graphics backend at all (see `ecs_basics`).
88pub struct TimePlugin;
89
90impl Plugin for TimePlugin {
91 fn build(&self, app: &mut crate::prelude::App) {
92 if app.try_insert_resource(Time::new()) {
93 app.add_system(SystemStage::PreUpdate, tick_time);
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn a_fresh_time_has_zero_delta_and_elapsed() {
104 let time = Time::new();
105 assert_eq!(time.delta(), Duration::ZERO);
106 assert_eq!(time.elapsed(), Duration::ZERO);
107 assert_eq!(time.fps(), 0.0);
108 }
109
110 #[test]
111 fn ticking_advances_delta_and_accumulates_elapsed() {
112 let mut time = Time::new();
113 std::thread::sleep(Duration::from_millis(5));
114 time.tick();
115
116 assert!(time.delta_seconds() > 0.0);
117 assert!(time.elapsed_seconds() >= time.delta_seconds());
118 assert!(time.fps() > 0.0);
119
120 let first_elapsed = time.elapsed();
121 std::thread::sleep(Duration::from_millis(5));
122 time.tick();
123 assert!(time.elapsed() > first_elapsed);
124 }
125}