Skip to main content

galeon_engine/
engine.rs

1// SPDX-License-Identifier: AGPL-3.0-only OR Commercial
2
3use crate::function_system::IntoSystem;
4use crate::game_loop::{self, FixedTimestep};
5use crate::schedule::Schedule;
6use crate::virtual_time::VirtualTime;
7use crate::world::World;
8
9/// The central game engine object.
10///
11/// `Engine` owns the [`World`] and [`Schedule`] and exposes a builder API for
12/// wiring up systems and plugins before (or during) the game loop.
13///
14/// # Example
15///
16/// ```rust
17/// use galeon_engine::{Engine, Plugin, QueryMut, Component};
18///
19/// #[derive(Component)]
20/// struct Score(u32);
21///
22/// fn add_score(mut scores: QueryMut<'_, Score>) {
23///     for (_, s) in scores.iter_mut() { s.0 += 1; }
24/// }
25///
26/// struct MyPlugin;
27/// impl Plugin for MyPlugin {
28///     fn build(&self, engine: &mut Engine) {
29///         engine.add_system::<(QueryMut<'_, Score>,)>("update", "add_score", add_score);
30///     }
31/// }
32///
33/// let mut engine = Engine::new();
34/// engine.add_plugin(MyPlugin);
35/// engine.run_once();
36/// ```
37pub struct Engine {
38    world: World,
39    schedule: Schedule,
40}
41
42impl Engine {
43    /// Create a new engine with an empty [`World`] and [`Schedule`].
44    pub fn new() -> Self {
45        Self {
46            world: World::new(),
47            schedule: Schedule::new(),
48        }
49    }
50
51    // -------------------------------------------------------------------------
52    // Accessors
53    // -------------------------------------------------------------------------
54
55    /// Immutable reference to the world.
56    pub fn world(&self) -> &World {
57        &self.world
58    }
59
60    /// Mutable reference to the world.
61    pub fn world_mut(&mut self) -> &mut World {
62        &mut self.world
63    }
64
65    /// Immutable reference to the schedule.
66    pub fn schedule(&self) -> &Schedule {
67        &self.schedule
68    }
69
70    // -------------------------------------------------------------------------
71    // Builder API
72    // -------------------------------------------------------------------------
73
74    /// Add a system to the schedule.
75    ///
76    /// Accepts any parameterized function like `fn(Res<T>, QueryMut<U>)`.
77    ///
78    /// Delegates to [`Schedule::add_system`]. Returns `&mut Self` for chaining.
79    pub fn add_system<P>(
80        &mut self,
81        stage: &'static str,
82        name: &'static str,
83        func: impl IntoSystem<P>,
84    ) -> &mut Self {
85        self.schedule.add_system(stage, name, func);
86        self
87    }
88
89    /// Apply a plugin to this engine.
90    ///
91    /// Calls [`Plugin::build`] with `self`. Returns `&mut Self` for chaining.
92    pub fn add_plugin(&mut self, plugin: impl Plugin) -> &mut Self {
93        plugin.build(self);
94        self
95    }
96
97    /// Set the fixed-timestep tick rate in Hz.
98    ///
99    /// Common values: 10 Hz (RTS/tycoon), 20 Hz (strategy), 30 Hz (action),
100    /// 60 Hz (platformer/FPS). If not called, defaults to 10 Hz on first tick.
101    pub fn set_tick_rate(&mut self, hz: f64) -> &mut Self {
102        self.world.insert_resource(FixedTimestep::new(hz));
103        self
104    }
105
106    /// Insert a resource into the world.
107    ///
108    /// Delegates to [`World::insert_resource`]. Returns `&mut Self` for
109    /// chaining.
110    pub fn insert_resource<T: Send + 'static>(&mut self, value: T) -> &mut Self {
111        self.world.insert_resource(value);
112        self
113    }
114
115    /// Register a named render data channel.
116    ///
117    /// The component type must implement [`ExtractToFloats`]. During frame
118    /// extraction, this channel will produce a flat `Vec<f32>` with
119    /// `T::STRIDE` floats per entity.
120    ///
121    /// Lazily inserts a [`RenderChannelRegistry`] resource if not already
122    /// present. Returns `&mut Self` for chaining.
123    ///
124    /// [`ExtractToFloats`]: crate::render_channel::ExtractToFloats
125    /// [`RenderChannelRegistry`]: crate::render_channel::RenderChannelRegistry
126    pub fn register_render_channel<T: crate::render_channel::ExtractToFloats>(
127        &mut self,
128        name: &str,
129    ) -> &mut Self {
130        if self
131            .world
132            .try_resource::<crate::render_channel::RenderChannelRegistry>()
133            .is_none()
134        {
135            self.world
136                .insert_resource(crate::render_channel::RenderChannelRegistry::new());
137        }
138        self.world
139            .resource_mut::<crate::render_channel::RenderChannelRegistry>()
140            .register::<T>(name);
141        self
142    }
143
144    // -------------------------------------------------------------------------
145    // Execution
146    // -------------------------------------------------------------------------
147
148    /// Advance the simulation by `elapsed` seconds using a fixed timestep.
149    ///
150    /// If a [`FixedTimestep`] resource has not been inserted yet this method
151    /// inserts the default RTS timestep (10 Hz) automatically. Returns the
152    /// number of ticks executed.
153    pub fn tick(&mut self, elapsed: f64) -> u32 {
154        // Lazily insert the default timestep so callers don't have to.
155        if !self.has_timestep() {
156            self.world.insert_resource(FixedTimestep::default_rts());
157        }
158        game_loop::tick(&mut self.world, &mut self.schedule, elapsed)
159    }
160
161    /// Run the schedule exactly once without any fixed-timestep logic.
162    ///
163    /// Useful for integration tests or non-game-loop scenarios.
164    pub fn run_once(&mut self) {
165        self.schedule.run(&mut self.world);
166    }
167
168    // -------------------------------------------------------------------------
169    // Virtual time controls
170    // -------------------------------------------------------------------------
171
172    /// Pause the simulation. Ticks will produce zero simulation steps.
173    ///
174    /// Lazily inserts a default `VirtualTime` if not already present.
175    pub fn pause(&mut self) {
176        self.ensure_virtual_time();
177        self.world.resource_mut::<VirtualTime>().paused = true;
178    }
179
180    /// Resume the simulation after a pause.
181    ///
182    /// Lazily inserts a default `VirtualTime` if not already present.
183    pub fn resume(&mut self) {
184        self.ensure_virtual_time();
185        self.world.resource_mut::<VirtualTime>().paused = false;
186    }
187
188    /// Set the simulation speed multiplier (clamped to `[0.0, 8.0]` at tick time).
189    ///
190    /// - 1.0 = normal speed
191    /// - 2.0 = double speed (RTS fast-forward)
192    /// - 0.5 = half speed (slow-mo)
193    ///
194    /// Lazily inserts a default `VirtualTime` if not already present.
195    pub fn set_speed(&mut self, scale: f64) {
196        self.ensure_virtual_time();
197        self.world.resource_mut::<VirtualTime>().scale = scale;
198    }
199
200    /// Returns `true` if the simulation is paused.
201    pub fn is_paused(&self) -> bool {
202        self.world
203            .try_resource::<VirtualTime>()
204            .is_some_and(|vt| vt.paused)
205    }
206
207    // -------------------------------------------------------------------------
208    // Private helpers
209    // -------------------------------------------------------------------------
210
211    /// Returns `true` if a [`FixedTimestep`] resource is already present.
212    fn has_timestep(&self) -> bool {
213        // We use a try-pattern by checking via a raw resource probe.
214        // `World::resource` panics, so we rely on the resource module's
215        // internal try_get once it is available. For now we track it via a
216        // small sentinel resource.
217        self.world.try_resource::<FixedTimestep>().is_some()
218    }
219
220    /// Ensures a `VirtualTime` resource exists, inserting a default if absent.
221    fn ensure_virtual_time(&mut self) {
222        if self.world.try_resource::<VirtualTime>().is_none() {
223            self.world.insert_resource(VirtualTime::new());
224        }
225    }
226}
227
228impl Default for Engine {
229    fn default() -> Self {
230        Self::new()
231    }
232}
233
234// =============================================================================
235// Plugin trait
236// =============================================================================
237
238/// A plugin encapsulates a cohesive set of systems and resources.
239///
240/// Implement this trait to bundle engine configuration into a reusable unit.
241///
242/// # Example
243///
244/// ```rust
245/// use galeon_engine::{Engine, Plugin, QueryMut, Component};
246///
247/// #[derive(Component)]
248/// struct Velocity { x: f32 }
249///
250/// fn physics_system(mut vels: QueryMut<'_, Velocity>) {
251///     for (_, v) in vels.iter_mut() { v.x *= 0.98; }
252/// }
253///
254/// pub struct PhysicsPlugin;
255///
256/// impl Plugin for PhysicsPlugin {
257///     fn build(&self, engine: &mut Engine) {
258///         engine.add_system::<(QueryMut<'_, Velocity>,)>("simulate", "physics", physics_system);
259///     }
260/// }
261/// ```
262pub trait Plugin {
263    /// Configure `engine` with this plugin's systems and resources.
264    fn build(&self, engine: &mut Engine);
265}
266
267// =============================================================================
268// Tests
269// =============================================================================
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use crate::component::Component;
275    use crate::system_param::QueryMut;
276
277    #[derive(Debug)]
278    struct Counter(u32);
279    impl Component for Counter {}
280
281    fn increment(mut counters: QueryMut<'_, Counter>) {
282        for (_, c) in counters.iter_mut() {
283            c.0 += 1;
284        }
285    }
286
287    // -------------------------------------------------------------------------
288    // Engine::new / accessors
289    // -------------------------------------------------------------------------
290
291    #[test]
292    fn new_engine_has_empty_world_and_schedule() {
293        let engine = Engine::new();
294        assert_eq!(engine.world().entity_count(), 0);
295        assert_eq!(engine.schedule().system_count(), 0);
296    }
297
298    #[test]
299    fn world_mut_allows_mutation() {
300        let mut engine = Engine::new();
301        engine.world_mut().spawn((Counter(0),));
302        assert_eq!(engine.world().entity_count(), 1);
303    }
304
305    // -------------------------------------------------------------------------
306    // Builder API
307    // -------------------------------------------------------------------------
308
309    #[test]
310    fn add_system_registers_system() {
311        let mut engine = Engine::new();
312        engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
313        assert_eq!(engine.schedule().system_count(), 1);
314    }
315
316    #[test]
317    fn add_system_is_chainable() {
318        let mut engine = Engine::new();
319        engine
320            .add_system::<(QueryMut<'_, Counter>,)>("pre", "increment", increment)
321            .add_system::<(QueryMut<'_, Counter>,)>("post", "increment", increment);
322        assert_eq!(engine.schedule().system_count(), 2);
323    }
324
325    #[test]
326    fn insert_resource_is_chainable() {
327        struct Gravity(f32);
328
329        let mut engine = Engine::new();
330        engine.insert_resource(Gravity(9.8));
331        assert!((engine.world().resource::<Gravity>().0 - 9.8).abs() < f32::EPSILON);
332    }
333
334    // -------------------------------------------------------------------------
335    // Plugin
336    // -------------------------------------------------------------------------
337
338    struct IncrementPlugin;
339    impl Plugin for IncrementPlugin {
340        fn build(&self, engine: &mut Engine) {
341            engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
342        }
343    }
344
345    #[test]
346    fn add_plugin_calls_build() {
347        let mut engine = Engine::new();
348        engine.add_plugin(IncrementPlugin);
349        assert_eq!(engine.schedule().system_count(), 1);
350    }
351
352    #[test]
353    fn add_plugin_is_chainable() {
354        let mut engine = Engine::new();
355        engine
356            .add_plugin(IncrementPlugin)
357            .add_plugin(IncrementPlugin);
358        assert_eq!(engine.schedule().system_count(), 2);
359    }
360
361    // -------------------------------------------------------------------------
362    // run_once
363    // -------------------------------------------------------------------------
364
365    #[test]
366    fn run_once_executes_schedule() {
367        let mut engine = Engine::new();
368        engine.world_mut().spawn((Counter(0),));
369        engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
370        engine.run_once();
371
372        let counts: Vec<u32> = engine
373            .world()
374            .query::<&Counter>()
375            .map(|(_, c)| c.0)
376            .collect();
377        assert_eq!(counts, vec![1]);
378    }
379
380    // -------------------------------------------------------------------------
381    // tick
382    // -------------------------------------------------------------------------
383
384    #[test]
385    fn tick_inserts_default_timestep_when_absent() {
386        let mut engine = Engine::new();
387        // No FixedTimestep inserted — tick should not panic.
388        let ticks = engine.tick(0.05); // 0.05 s < 0.1 s step → 0 ticks
389        assert_eq!(ticks, 0);
390    }
391
392    #[test]
393    fn tick_respects_existing_timestep() {
394        let mut engine = Engine::new();
395        // Use 10 Hz (0.1 s/tick) to avoid floating-point accumulation issues.
396        engine.world_mut().insert_resource(FixedTimestep::new(10.0));
397        engine.world_mut().spawn((Counter(0),));
398        engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
399
400        // 0.35 s at 10 Hz → 3 ticks (same as game_loop test)
401        let ticks = engine.tick(0.35);
402        assert_eq!(ticks, 3);
403
404        let counts: Vec<u32> = engine
405            .world()
406            .query::<&Counter>()
407            .map(|(_, c)| c.0)
408            .collect();
409        assert_eq!(counts, vec![3]);
410    }
411
412    #[test]
413    fn set_tick_rate_overrides_default() {
414        let mut engine = Engine::new();
415        engine.set_tick_rate(30.0);
416        engine.world_mut().spawn((Counter(0),));
417        engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
418
419        // 0.1 s at 30 Hz → 3 ticks (step = 1/30 ≈ 0.0333)
420        let ticks = engine.tick(0.1);
421        assert_eq!(ticks, 3);
422    }
423
424    #[test]
425    fn tick_returns_correct_tick_count() {
426        let mut engine = Engine::new();
427        // Default 10 Hz → 0.25 s yields 2 ticks
428        let ticks = engine.tick(0.25);
429        assert_eq!(ticks, 2);
430    }
431
432    // -------------------------------------------------------------------------
433    // Virtual time convenience API
434    // -------------------------------------------------------------------------
435
436    #[test]
437    fn pause_and_resume() {
438        let mut engine = Engine::new();
439        assert!(!engine.is_paused());
440
441        engine.pause();
442        assert!(engine.is_paused());
443
444        engine.resume();
445        assert!(!engine.is_paused());
446    }
447
448    #[test]
449    fn pause_stops_ticks() {
450        let mut engine = Engine::new();
451        engine.world_mut().spawn((Counter(0),));
452        engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
453
454        engine.pause();
455        engine.tick(1.0);
456
457        let counts: Vec<u32> = engine
458            .world()
459            .query::<&Counter>()
460            .map(|(_, c)| c.0)
461            .collect();
462        assert_eq!(counts, vec![0]);
463    }
464
465    #[test]
466    fn set_speed_doubles_ticks() {
467        let mut engine = Engine::new();
468        engine.world_mut().spawn((Counter(0),));
469        engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
470
471        engine.set_speed(2.0);
472        // 0.1s real at 2x = 0.2s virtual, default 10 Hz = 2 ticks
473        let ticks = engine.tick(0.1);
474        assert_eq!(ticks, 2);
475    }
476
477    #[test]
478    fn set_speed_persists() {
479        let mut engine = Engine::new();
480        engine.set_speed(4.0);
481        let vt = engine.world().resource::<VirtualTime>();
482        assert!((vt.scale - 4.0).abs() < f64::EPSILON);
483    }
484
485    #[test]
486    fn lazy_insert_virtual_time() {
487        let mut engine = Engine::new();
488        assert!(engine.world().try_resource::<VirtualTime>().is_none());
489
490        engine.pause();
491        assert!(engine.world().try_resource::<VirtualTime>().is_some());
492    }
493
494    // -------------------------------------------------------------------------
495    // Render channels
496    // -------------------------------------------------------------------------
497
498    #[derive(Debug)]
499    struct ShaderParams {
500        intensity: f32,
501    }
502    impl Component for ShaderParams {}
503    impl crate::render_channel::ExtractToFloats for ShaderParams {
504        const STRIDE: usize = 1;
505        fn extract(&self, buf: &mut [f32]) {
506            buf[0] = self.intensity;
507        }
508    }
509
510    #[test]
511    fn register_render_channel_creates_registry() {
512        let mut engine = Engine::new();
513        engine.register_render_channel::<ShaderParams>("shader");
514        let reg = engine
515            .world()
516            .try_resource::<crate::render_channel::RenderChannelRegistry>()
517            .expect("registry should exist");
518        assert_eq!(reg.len(), 1);
519    }
520
521    #[test]
522    fn register_render_channel_is_chainable() {
523        let mut engine = Engine::new();
524        engine
525            .register_render_channel::<ShaderParams>("shader")
526            .insert_resource(Counter(0));
527        assert_eq!(engine.world().resource::<Counter>().0, 0);
528    }
529}