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}