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}