1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
//! Plain-data time accumulators a game owns and ticks itself.
//!
//! A [`Timer`] counts toward a fixed duration and reports when it elapses, once
//! or on a repeating cycle. A [`Stopwatch`] counts up without end. Neither holds
//! engine state: store one in a component or a resource, advance it with the
//! frame's delta, and read its progress. A repeating timer carries the overshoot
//! into the next cycle and reports how many cycles a single tick completed, so a
//! long frame neither drifts the schedule nor silently drops events. Both derive
//! serde, so they round-trip through a save file mid-countdown.
use serde::{Deserialize, Serialize};
/// Whether a [`Timer`] stops after it elapses or restarts for another cycle.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub enum TimerMode {
/// Elapses once and then stays finished until [`Timer::reset`].
#[default]
Once,
/// Restarts each time it elapses, carrying the remainder into the next cycle.
Repeating,
}
/// A countdown toward a duration. Tick it with the frame delta and read
/// [`Timer::just_finished`] for the cycle it completes, [`Timer::fraction`] for
/// progress to drive a bar or a fade, or [`Timer::remaining`] for a clock.
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Timer {
duration: f32,
elapsed: f32,
mode: TimerMode,
paused: bool,
finished: bool,
times_finished_this_tick: u32,
}
impl Timer {
/// A timer of `duration` seconds with the given mode.
pub fn new(duration: f32, mode: TimerMode) -> Self {
Self {
duration: duration.max(0.0),
mode,
..Default::default()
}
}
/// A one-shot timer of `duration` seconds.
pub fn once(duration: f32) -> Self {
Self::new(duration, TimerMode::Once)
}
/// A repeating timer that elapses every `duration` seconds.
pub fn repeating(duration: f32) -> Self {
Self::new(duration, TimerMode::Repeating)
}
/// Advances the timer by `delta` seconds, returning it for chaining. A
/// repeating timer wraps and counts every cycle the delta crossed; a
/// finished one-shot timer ignores further ticks until reset; a paused timer
/// ignores the delta entirely.
pub fn tick(&mut self, delta: f32) -> &mut Self {
if self.paused {
self.times_finished_this_tick = 0;
return self;
}
if self.mode == TimerMode::Once && self.finished {
self.times_finished_this_tick = 0;
return self;
}
self.elapsed += delta;
if self.duration <= 0.0 {
self.finished = true;
self.times_finished_this_tick = 1;
self.elapsed = 0.0;
return self;
}
if self.elapsed >= self.duration {
self.finished = true;
match self.mode {
TimerMode::Repeating => {
self.times_finished_this_tick = (self.elapsed / self.duration) as u32;
self.elapsed %= self.duration;
}
TimerMode::Once => {
self.times_finished_this_tick = 1;
self.elapsed = self.duration;
}
}
} else {
self.finished = false;
self.times_finished_this_tick = 0;
}
self
}
/// Whether the timer reached its duration. For a repeating timer this is true
/// only on a tick that completed a cycle, matching [`Timer::just_finished`].
pub fn finished(&self) -> bool {
match self.mode {
TimerMode::Once => self.finished,
TimerMode::Repeating => self.times_finished_this_tick > 0,
}
}
/// Whether the most recent tick completed at least one cycle.
pub fn just_finished(&self) -> bool {
self.times_finished_this_tick > 0
}
/// How many full cycles the most recent tick completed. Always 0 or 1 for a
/// one-shot timer; for a repeating timer a long frame can complete several.
pub fn times_finished_this_tick(&self) -> u32 {
self.times_finished_this_tick
}
/// Seconds elapsed in the current cycle.
pub fn elapsed(&self) -> f32 {
self.elapsed
}
/// Seconds left in the current cycle.
pub fn remaining(&self) -> f32 {
(self.duration - self.elapsed).max(0.0)
}
/// The configured duration in seconds.
pub fn duration(&self) -> f32 {
self.duration
}
/// Progress through the current cycle from 0 at the start to 1 at the end.
pub fn fraction(&self) -> f32 {
if self.duration <= 0.0 {
1.0
} else {
(self.elapsed / self.duration).clamp(0.0, 1.0)
}
}
/// Progress remaining in the current cycle, from 1 at the start to 0 at the
/// end.
pub fn fraction_remaining(&self) -> f32 {
1.0 - self.fraction()
}
/// This timer's mode.
pub fn mode(&self) -> TimerMode {
self.mode
}
/// Whether the timer is paused.
pub fn paused(&self) -> bool {
self.paused
}
/// Sets the duration, keeping elapsed time. Negative values clamp to zero.
pub fn set_duration(&mut self, duration: f32) {
self.duration = duration.max(0.0);
}
/// Sets the elapsed time into the current cycle.
pub fn set_elapsed(&mut self, elapsed: f32) {
self.elapsed = elapsed.max(0.0);
}
/// Switches between one-shot and repeating.
pub fn set_mode(&mut self, mode: TimerMode) {
self.mode = mode;
}
/// Pauses or resumes ticking.
pub fn set_paused(&mut self, paused: bool) {
self.paused = paused;
}
/// Pauses ticking.
pub fn pause(&mut self) {
self.paused = true;
}
/// Resumes ticking.
pub fn unpause(&mut self) {
self.paused = false;
}
/// Clears elapsed time and the finished state, leaving duration and mode.
pub fn reset(&mut self) {
self.elapsed = 0.0;
self.finished = false;
self.times_finished_this_tick = 0;
}
}
/// An open-ended count-up. Tick it with the frame delta and read
/// [`Stopwatch::elapsed`]. Use it where there is no fixed deadline, such as time
/// since the player last moved or how long an input has been held.
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Stopwatch {
elapsed: f32,
paused: bool,
}
impl Stopwatch {
/// A stopwatch at zero.
pub fn new() -> Self {
Self::default()
}
/// Advances the stopwatch by `delta` seconds, returning it for chaining. A
/// paused stopwatch ignores the delta.
pub fn tick(&mut self, delta: f32) -> &mut Self {
if !self.paused {
self.elapsed += delta;
}
self
}
/// Seconds accumulated since the last reset.
pub fn elapsed(&self) -> f32 {
self.elapsed
}
/// Whether the stopwatch is paused.
pub fn paused(&self) -> bool {
self.paused
}
/// Sets the accumulated time.
pub fn set_elapsed(&mut self, elapsed: f32) {
self.elapsed = elapsed.max(0.0);
}
/// Pauses or resumes ticking.
pub fn set_paused(&mut self, paused: bool) {
self.paused = paused;
}
/// Pauses ticking.
pub fn pause(&mut self) {
self.paused = true;
}
/// Resumes ticking.
pub fn unpause(&mut self) {
self.paused = false;
}
/// Clears the accumulated time.
pub fn reset(&mut self) {
self.elapsed = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn once_latches_finished_and_clamps_elapsed() {
let mut timer = Timer::once(1.0);
timer.tick(0.6);
assert!(!timer.finished());
timer.tick(0.6);
assert!(timer.finished());
assert!(timer.just_finished());
assert_eq!(timer.elapsed(), 1.0);
timer.tick(5.0);
assert!(timer.finished());
assert!(!timer.just_finished());
assert_eq!(timer.elapsed(), 1.0);
}
#[test]
fn repeating_carries_remainder() {
let mut timer = Timer::repeating(1.0);
timer.tick(1.5);
assert!(timer.just_finished());
assert_eq!(timer.times_finished_this_tick(), 1);
assert!((timer.elapsed() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn repeating_counts_multiple_cycles_in_one_tick() {
let mut timer = Timer::repeating(0.5);
timer.tick(1.6);
assert_eq!(timer.times_finished_this_tick(), 3);
assert!((timer.elapsed() - 0.1).abs() < 1e-6);
}
#[test]
fn paused_timer_ignores_ticks() {
let mut timer = Timer::once(1.0);
timer.pause();
timer.tick(2.0);
assert!(!timer.finished());
assert_eq!(timer.elapsed(), 0.0);
timer.unpause();
timer.tick(2.0);
assert!(timer.finished());
}
#[test]
fn fraction_reports_progress() {
let mut timer = Timer::once(4.0);
timer.tick(1.0);
assert!((timer.fraction() - 0.25).abs() < f32::EPSILON);
assert!((timer.fraction_remaining() - 0.75).abs() < f32::EPSILON);
assert!((timer.remaining() - 3.0).abs() < f32::EPSILON);
}
#[test]
fn reset_clears_progress() {
let mut timer = Timer::once(1.0);
timer.tick(1.0);
timer.reset();
assert!(!timer.finished());
assert_eq!(timer.elapsed(), 0.0);
}
#[test]
fn stopwatch_accumulates_and_pauses() {
let mut stopwatch = Stopwatch::new();
stopwatch.tick(0.5);
stopwatch.tick(0.5);
assert!((stopwatch.elapsed() - 1.0).abs() < f32::EPSILON);
stopwatch.pause();
stopwatch.tick(1.0);
assert!((stopwatch.elapsed() - 1.0).abs() < f32::EPSILON);
stopwatch.reset();
assert_eq!(stopwatch.elapsed(), 0.0);
}
}