rig_harness/clock.rs
1//! Waking the loop that sleeps while nothing happens. Bevy's clock, which
2//! timers and delayed commands run on, stops while the machine sleeps; a
3//! [`WakeAt`] is a wall-clock instant instead. Once it passes, the entity
4//! gets [`Due`] in `First`, and the headless loop sleeps no longer than
5//! until the earliest one. A plugin reacts to `Add<Due>`, and arms the next
6//! time by inserting a new `WakeAt`, which takes `Due` away. A
7//! [`KeepAwake`] keeps the loop running at its pace while it lives.
8//!
9//! ```
10//! use std::time::SystemTime;
11//!
12//! use rig_harness::prelude::*;
13//!
14//! let mut app = App::new();
15//! app.add_plugins(rig_harness::clock::ClockPlugin);
16//! let alarm = app.world_mut().spawn(WakeAt(SystemTime::UNIX_EPOCH)).id();
17//! app.update();
18//! assert!(app.world().entity(alarm).contains::<Due>());
19//! ```
20
21use std::time::{Duration, SystemTime};
22
23use bevy_app::prelude::*;
24use bevy_ecs::lifecycle::HookContext;
25use bevy_ecs::prelude::*;
26use bevy_ecs::world::DeferredWorld;
27use bevy_reflect::prelude::*;
28
29/// When this entity is due, on the wall clock.
30#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
31#[component(on_insert = rearm)]
32pub struct WakeAt(pub SystemTime);
33
34impl WakeAt {
35 /// A wake `after` from now.
36 pub fn after(after: Duration) -> Self {
37 Self(SystemTime::now() + after)
38 }
39
40 /// How long until it is due; zero once it is.
41 pub fn left(&self, now: SystemTime) -> Duration {
42 self.0.duration_since(now).unwrap_or_default()
43 }
44}
45
46/// On an entity whose [`WakeAt`] passed, from the `First` of the frame
47/// after it did.
48#[derive(Component, Clone, Copy, Debug, Default)]
49#[component(storage = "SparseSet")]
50pub struct Due;
51
52/// Keeps the loop that sleeps between frames running a frame at least
53/// this often while the entity exists: a spinner, or a panel that polls.
54/// Time itself is Bevy's: a system paced with
55/// `.run_if(on_real_timer(interval))` runs on time while a `KeepAwake` of
56/// that interval lives, and a one-off wait is a delayed command
57/// (`commands.delayed().duration(wait)`), whose deadline the loop wakes for
58/// on its own.
59///
60/// ```
61/// use rig_harness::prelude::*;
62///
63/// const STEP: Duration = Duration::from_millis(200);
64///
65/// fn tick() {}
66///
67/// let mut app = App::new();
68/// // `tick` runs every step while this entity lives; once it is despawned
69/// // the loop sleeps until there is work.
70/// app.world_mut().spawn((Name::new("spinner"), KeepAwake(STEP)));
71/// app.add_systems(Update, tick.run_if(on_real_timer(STEP)));
72/// ```
73#[derive(Component, Reflect, Clone, Copy, Debug)]
74#[reflect(Component, Clone, Debug)]
75pub struct KeepAwake(pub Duration);
76
77/// Marks what is [`Due`].
78pub struct ClockPlugin;
79
80impl Plugin for ClockPlugin {
81 fn build(&self, app: &mut App) {
82 app.add_systems(First, mark_due);
83 }
84}
85
86/// A new [`WakeAt`] is not due yet.
87fn rearm(mut world: DeferredWorld, context: HookContext) {
88 world.commands().entity(context.entity).try_remove::<Due>();
89}
90
91fn mark_due(waits: Query<(Entity, &WakeAt), Without<Due>>, mut commands: Commands) {
92 let now = SystemTime::now();
93 for (entity, wake) in &waits {
94 if wake.0 <= now {
95 commands.entity(entity).try_insert(Due);
96 }
97 }
98}
99
100#[cfg(test)]
101mod tests;