bones_lib/
time.rs

1//! Time functionality for the Bones framework.
2//!
3//! This is a slimmed down version of [`bevy_time`].
4//!
5//! [`bevy_time`] is licensed under MIT OR Apache-2.0.
6//!
7//! [`bevy_time`]: https://github.com/bevyengine/bevy/tree/aa4170d9a471c6f6a4f3bea4e41ed2c39de98e16/crates/bevy_time
8
9use crate::prelude::*;
10
11use instant::{Duration, Instant};
12
13/// A clock that tracks how much it has advanced (and how much real time has elapsed) since
14/// its previous update and since its creation.
15#[derive(Clone, Copy, Debug, HasSchema)]
16#[repr(C)]
17pub struct Time {
18    #[schema(opaque)]
19    startup: Instant,
20    #[schema(opaque)]
21    last_update: Option<Instant>,
22    #[schema(opaque)]
23    first_update: Option<Instant>,
24
25    // pausing
26    paused: bool,
27
28    delta: Duration,
29    delta_seconds: f32,
30    delta_seconds_f64: f64,
31
32    elapsed: Duration,
33    elapsed_seconds: f32,
34    elapsed_seconds_f64: f64,
35}
36
37impl Default for Time {
38    fn default() -> Self {
39        Self {
40            paused: false,
41            first_update: None,
42            last_update: None,
43            delta_seconds: 0.0,
44            delta: Duration::ZERO,
45            elapsed_seconds: 0.0,
46            delta_seconds_f64: 0.0,
47            startup: Instant::now(),
48            elapsed: Duration::ZERO,
49            elapsed_seconds_f64: 0.0,
50        }
51    }
52}
53
54impl Time {
55    /// Constructs a new `Time` instance with a specific startup `Instant`.
56    pub fn new(startup: Instant) -> Self {
57        Self {
58            startup,
59            ..Default::default()
60        }
61    }
62
63    /// Updates the internal time measurements.
64    ///
65    /// Calling this method as part of your app will most likely result in inaccurate timekeeping,
66    /// as the `Time` resource is ordinarily managed by the bones rendering backend.
67    pub fn update(&mut self) {
68        let now = Instant::now();
69        self.update_with_instant(now);
70    }
71
72    /// Updates time with a specified [`Instant`].
73    ///
74    /// This method is provided for use in tests. Calling this method as part of your app will most
75    /// likely result in inaccurate timekeeping, as the `Time` resource is ordinarily managed by
76    /// whatever bones renderer you are using.
77    ///
78    /// # Examples
79    ///
80    /// ```ignore
81    /// # use bones_input::prelude::*;
82    /// # use bones_ecs::prelude::*;
83    /// # use std::time::Duration;
84    /// # fn main () {
85    /// #     test_health_system();
86    /// # }
87    /// #[derive(Resource)]
88    /// struct Health {
89    ///     // Health value between 0.0 and 1.0
90    ///     health_value: f32,
91    /// }
92    ///
93    /// fn health_system(time: Res<Time>, mut health: ResMut<Health>) {
94    ///     // Increase health value by 0.1 per second, independent of frame rate,
95    ///     // but not beyond 1.0
96    ///     health.health_value = (health.health_value + 0.1 * time.delta_seconds()).min(1.0);
97    /// }
98    ///
99    /// // Mock time in tests
100    /// fn test_health_system() {
101    ///     let mut world = World::default();
102    ///     let mut time = Time::default();
103    ///     time.update();
104    ///     world.insert_resource(time);
105    ///     world.insert_resource(Health { health_value: 0.2 });
106    ///
107    ///     let mut schedule = Schedule::new();
108    ///     schedule.add_system(health_system);
109    ///
110    ///     // Simulate that 30 ms have passed
111    ///     let mut time = world.resource_mut::<Time>();
112    ///     let last_update = time.last_update().unwrap();
113    ///     time.update_with_instant(last_update + Duration::from_millis(30));
114    ///
115    ///     // Run system
116    ///     schedule.run(&mut world);
117    ///
118    ///     // Check that 0.003 has been added to the health value
119    ///     let expected_health_value = 0.2 + 0.1 * 0.03;
120    ///     let actual_health_value = world.resource::<Health>().health_value;
121    ///     assert_eq!(expected_health_value, actual_health_value);
122    /// }
123    /// ```
124    pub fn update_with_instant(&mut self, instant: Instant) {
125        let raw_delta = instant - self.last_update.unwrap_or(self.startup);
126        let delta = if self.paused {
127            Duration::ZERO
128        } else {
129            // avoid rounding when at normal speed
130            raw_delta
131        };
132
133        if self.last_update.is_some() {
134            self.delta = delta;
135            self.delta_seconds = self.delta.as_secs_f32();
136            self.delta_seconds_f64 = self.delta.as_secs_f64();
137        } else {
138            self.first_update = Some(instant);
139        }
140
141        self.elapsed += delta;
142        self.elapsed_seconds = self.elapsed.as_secs_f32();
143        self.elapsed_seconds_f64 = self.elapsed.as_secs_f64();
144
145        self.last_update = Some(instant);
146    }
147
148    /// Advance the time exactly by the given duration.
149    ///
150    /// This is useful when ticking the time exactly by a fixed timestep.
151    pub fn advance_exact(&mut self, duration: Duration) {
152        let next_instant = self.last_update.unwrap_or_else(Instant::now) + duration;
153        self.update_with_instant(next_instant);
154    }
155
156    /// Returns how much time has advanced since the last [`update`](#method.update), as a [`Duration`].
157    #[inline]
158    pub fn delta(&self) -> Duration {
159        self.delta
160    }
161
162    /// Returns how much time has advanced since the last [`update`](#method.update), as [`prim@f32`] seconds.
163    #[inline]
164    pub fn delta_seconds(&self) -> f32 {
165        self.delta_seconds
166    }
167
168    /// Returns how much time has advanced since the last [`update`](#method.update), as [`prim@f64`] seconds.
169    #[inline]
170    pub fn delta_seconds_f64(&self) -> f64 {
171        self.delta_seconds_f64
172    }
173
174    /// Returns how much time has advanced since [`startup`](#method.startup), as [`Duration`].
175    #[inline]
176    pub fn elapsed(&self) -> Duration {
177        self.elapsed
178    }
179
180    /// Returns how much time has advanced since [`startup`](#method.startup), as [`prim@f32`] seconds.
181    ///
182    /// **Note:** This is a monotonically increasing value. It's precision will degrade over time.
183    /// If you need an `f32` but that precision loss is unacceptable,
184    /// use [`elapsed_seconds_wrapped`](#method.elapsed_seconds_wrapped).
185    #[inline]
186    pub fn elapsed_seconds(&self) -> f32 {
187        self.elapsed_seconds
188    }
189
190    /// Returns how much time has advanced since [`startup`](#method.startup), as [`prim@f64`] seconds.
191    #[inline]
192    pub fn elapsed_seconds_f64(&self) -> f64 {
193        self.elapsed_seconds_f64
194    }
195
196    /// Stops the clock, preventing it from advancing until resumed.
197    ///
198    /// **Note:** This does affect the `raw_*` measurements.
199    #[inline]
200    pub fn pause(&mut self) {
201        self.paused = true;
202    }
203
204    /// Resumes the clock if paused.
205    #[inline]
206    pub fn unpause(&mut self) {
207        self.paused = false;
208    }
209
210    /// Returns `true` if the clock is currently paused.
211    #[inline]
212    pub fn is_paused(&self) -> bool {
213        self.paused
214    }
215}
216
217#[cfg(test)]
218#[allow(clippy::float_cmp)]
219mod tests {
220    use super::Time;
221
222    use std::time::{Duration, Instant};
223
224    #[test]
225    fn update_test() {
226        let start_instant = Instant::now();
227        let mut time = Time::new(start_instant);
228
229        // Ensure `time` was constructed correctly.
230        assert_eq!(time.delta(), Duration::ZERO);
231        assert_eq!(time.delta_seconds(), 0.0);
232        assert_eq!(time.delta_seconds_f64(), 0.0);
233        assert_eq!(time.elapsed(), Duration::ZERO);
234        assert_eq!(time.elapsed_seconds(), 0.0);
235        assert_eq!(time.elapsed_seconds_f64(), 0.0);
236
237        // Update `time` and check results.
238        // The first update to `time` normally happens before other systems have run,
239        // so the first delta doesn't appear until the second update.
240        let first_update_instant = Instant::now();
241        time.update_with_instant(first_update_instant);
242
243        assert_eq!(time.delta(), Duration::ZERO);
244        assert_eq!(time.delta_seconds(), 0.0);
245        assert_eq!(time.delta_seconds_f64(), 0.0);
246        assert_eq!(time.elapsed(), first_update_instant - start_instant,);
247        assert_eq!(
248            time.elapsed_seconds(),
249            (first_update_instant - start_instant).as_secs_f32(),
250        );
251        assert_eq!(
252            time.elapsed_seconds_f64(),
253            (first_update_instant - start_instant).as_secs_f64(),
254        );
255
256        // Update `time` again and check results.
257        // At this point its safe to use time.delta().
258        let second_update_instant = Instant::now();
259        time.update_with_instant(second_update_instant);
260        assert_eq!(time.delta(), second_update_instant - first_update_instant);
261        assert_eq!(
262            time.delta_seconds(),
263            (second_update_instant - first_update_instant).as_secs_f32(),
264        );
265        assert_eq!(
266            time.delta_seconds_f64(),
267            (second_update_instant - first_update_instant).as_secs_f64(),
268        );
269        assert_eq!(time.elapsed(), second_update_instant - start_instant,);
270        assert_eq!(
271            time.elapsed_seconds(),
272            (second_update_instant - start_instant).as_secs_f32(),
273        );
274        assert_eq!(
275            time.elapsed_seconds_f64(),
276            (second_update_instant - start_instant).as_secs_f64(),
277        );
278    }
279
280    #[test]
281    fn pause_test() {
282        let start_instant = Instant::now();
283        let mut time = Time::new(start_instant);
284
285        let first_update_instant = Instant::now();
286        time.update_with_instant(first_update_instant);
287
288        assert!(!time.is_paused());
289
290        time.pause();
291
292        assert!(time.is_paused());
293
294        let second_update_instant = Instant::now();
295        time.update_with_instant(second_update_instant);
296        assert_eq!(time.delta(), Duration::ZERO);
297        assert_eq!(time.elapsed(), first_update_instant - start_instant);
298
299        time.unpause();
300
301        assert!(!time.is_paused());
302
303        let third_update_instant = Instant::now();
304        time.update_with_instant(third_update_instant);
305        assert_eq!(time.delta(), third_update_instant - second_update_instant);
306        assert_eq!(
307            time.elapsed(),
308            (third_update_instant - second_update_instant) + (first_update_instant - start_instant),
309        );
310    }
311}