Skip to main content

mittens_engine/engine/ecs/system/
clock_system.rs

1use std::sync::Arc;
2use std::time::Instant;
3
4use crate::engine::ecs::ComponentId;
5use crate::engine::ecs::World;
6use crate::engine::ecs::component::ClockComponent;
7use crate::engine::ecs::system::System;
8use crate::engine::graphics::VisualWorld;
9use crate::engine::user_input::InputState;
10
11pub trait ClockDriver: Send + Sync {
12    fn name(&self) -> &'static str;
13    fn time_now_sec(&self) -> f64;
14}
15
16#[derive(Debug)]
17pub struct SystemClockDriver {
18    start: Instant,
19}
20
21impl SystemClockDriver {
22    pub fn new() -> Self {
23        Self {
24            start: Instant::now(),
25        }
26    }
27}
28
29impl ClockDriver for SystemClockDriver {
30    fn name(&self) -> &'static str {
31        "system"
32    }
33
34    fn time_now_sec(&self) -> f64 {
35        self.start.elapsed().as_secs_f64()
36    }
37}
38
39/// Global clock system.
40///
41/// Exposes a beat-based timeline (beats) driven by a pluggable `ClockDriver`.
42pub struct ClockSystem {
43    driver: Arc<dyn ClockDriver>,
44    bpm: f64,
45
46    tempo_component: Option<ComponentId>,
47
48    // Cached for quick access + for other systems to read.
49    time_base_sec: f64,
50    beat_base: f64,
51    last_time_sec: f64,
52    last_beat: f64,
53}
54
55impl std::fmt::Debug for ClockSystem {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.debug_struct("ClockSystem")
58            .field("driver", &self.driver.name())
59            .field("bpm", &self.bpm)
60            .field("last_beat", &self.last_beat)
61            .finish()
62    }
63}
64
65impl Default for ClockSystem {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl ClockSystem {
72    pub fn new() -> Self {
73        let driver: Arc<dyn ClockDriver> = Arc::new(SystemClockDriver::new());
74        let t0 = driver.time_now_sec();
75        Self {
76            driver,
77            bpm: 120.0,
78            tempo_component: None,
79            time_base_sec: t0,
80            beat_base: 0.0,
81            last_time_sec: t0,
82            last_beat: 0.0,
83        }
84    }
85
86    pub fn driver_name(&self) -> &'static str {
87        self.driver.name()
88    }
89
90    pub fn bpm(&self) -> f64 {
91        self.bpm
92    }
93
94    pub fn beat_now(&self) -> f64 {
95        self.last_beat
96    }
97
98    pub fn register_clock_component(&mut self, component: ComponentId) {
99        self.tempo_component = Some(component);
100    }
101
102    pub fn set_bpm(&mut self, bpm: f64) {
103        if !bpm.is_finite() || bpm <= 0.0 {
104            return;
105        }
106
107        if (self.bpm - bpm).abs() < 1e-6 {
108            return;
109        }
110
111        // Keep beat continuous across tempo changes.
112        let now = self.driver.time_now_sec();
113        self.beat_base = self.beat_at_time(now);
114        self.time_base_sec = now;
115        self.bpm = bpm;
116    }
117
118    pub fn set_driver(&mut self, driver: Arc<dyn ClockDriver>) {
119        if self.driver_name() != driver.name() {
120            println!(
121                "[ClockSystem] driver: {} -> {}",
122                self.driver_name(),
123                driver.name()
124            );
125        }
126
127        // Keep beat continuous across driver changes.
128        let last_beat = self.last_beat;
129        self.driver = driver;
130
131        let now = self.driver.time_now_sec();
132        self.time_base_sec = now;
133        self.beat_base = last_beat;
134        self.last_time_sec = now;
135        self.last_beat = last_beat;
136    }
137
138    fn beat_at_time(&self, time_now_sec: f64) -> f64 {
139        let dt = (time_now_sec - self.time_base_sec).max(0.0);
140        self.beat_base + dt * (self.bpm / 60.0)
141    }
142
143    pub fn sample(&mut self) {
144        let now = self.driver.time_now_sec();
145        self.last_time_sec = now;
146        self.last_beat = self.beat_at_time(now);
147    }
148}
149
150impl System for ClockSystem {
151    fn tick(
152        &mut self,
153        world: &mut World,
154        _visuals: &mut VisualWorld,
155        _input: &InputState,
156        _dt_sec: f32,
157    ) {
158        if let Some(cid) = self.tempo_component {
159            if let Some(clock) = world.get_component_by_id_as::<ClockComponent>(cid) {
160                self.set_bpm(clock.bpm);
161            }
162        }
163
164        // Driver owns its own notion of time; we sample and convert to beats.
165        self.sample();
166    }
167}