Skip to main content

repame_sim/
lib.rs

1//! Headless sim: a `bevy_ecs` world stepped without a renderer.
2//!
3//! The game owns a [`Sim`], advances it with fixed-timestep [`Sim::step`],
4//! and pulls a snapshot out each frame for the viewport (see `repame-sprite`).
5//! Systems are plain functions registered on the schedule; no `bevy_app`,
6//! no window, no renderer. The sim stays portable to full Bevy later.
7
8use std::time::Duration;
9
10use bevy_ecs::prelude::*;
11use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule};
12use bevy_ecs::system::ScheduleSystem;
13
14pub use bevy_ecs;
15
16/// Fixed-timestep simulation state.
17pub struct Sim {
18    /// Entity/component store. Game crates add their own components here.
19    pub world: World,
20    schedule: Schedule,
21    accumulator: Duration,
22    /// Fixed step size. Defaults to 60 Hz.
23    pub step: Duration,
24}
25
26/// Sim time resource, advanced once per fixed step.
27#[derive(Resource, Clone, Copy, Debug, Default)]
28pub struct SimTime {
29    /// Total simulated seconds.
30    pub elapsed_secs: f64,
31    /// Fixed delta of the current step.
32    pub delta_secs: f32,
33}
34
35impl Sim {
36    pub fn new(step: Duration) -> Self {
37        let mut world = World::new();
38        world.init_resource::<SimTime>();
39        Self {
40            world,
41            schedule: Schedule::default(),
42            accumulator: Duration::ZERO,
43            step,
44        }
45    }
46
47    /// 60 Hz fixed-step sim.
48    pub fn with_default_step() -> Self {
49        Self::new(Duration::from_secs_f64(1.0 / 60.0))
50    }
51
52    /// Register a system into the per-step schedule.
53    pub fn add_system<M>(&mut self, system: impl IntoSystem<(), (), M>) -> &mut Self {
54        self.schedule.add_systems(system);
55        self
56    }
57
58    /// Register systems with a guaranteed execution order.
59    ///
60    /// A bare `Schedule` does not preserve `add_system` insertion order for
61    /// systems with conflicting accesses, so multi-step pipelines (economy,
62    /// combat) must register as one `(a, b, c).chain()` tuple through here
63    /// instead of separate `add_system` calls.
64    pub fn add_chained_systems<M>(
65        &mut self,
66        systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
67    ) -> &mut Self {
68        self.schedule.add_systems(systems);
69        self
70    }
71
72    /// Advance the sim by `dt` wall time, running zero or more fixed steps.
73    /// Returns the number of steps run.
74    pub fn step(&mut self, dt: Duration) -> u32 {
75        self.accumulator += dt;
76        let mut ran = 0;
77        while self.accumulator >= self.step {
78            self.accumulator -= self.step;
79            self.tick();
80            ran += 1;
81        }
82        ran
83    }
84
85    /// Run the schedule exactly once, advancing sim time by one step.
86    /// Tick-model games (integer logic steps) drive this directly instead
87    /// of the wall-clock [`Sim::step`] accumulator.
88    pub fn tick(&mut self) {
89        {
90            let mut time = self.world.resource_mut::<SimTime>();
91            time.delta_secs = self.step.as_secs_f32();
92            time.elapsed_secs += self.step.as_secs_f64();
93        }
94        self.schedule.run(&mut self.world);
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn fixed_steps_accumulate() {
104        let mut sim = Sim::with_default_step();
105        let ran = sim.step(Duration::from_nanos(50_000_001));
106        assert_eq!(ran, 3);
107        let time = sim.world.resource::<SimTime>();
108        assert!((time.elapsed_secs - 3.0 / 60.0).abs() < 1e-9);
109    }
110}