use crate::function_system::IntoSystem;
use crate::game_loop::{self, FixedTimestep};
use crate::schedule::Schedule;
use crate::virtual_time::VirtualTime;
use crate::world::World;
pub struct Engine {
world: World,
schedule: Schedule,
}
impl Engine {
pub fn new() -> Self {
Self {
world: World::new(),
schedule: Schedule::new(),
}
}
pub fn world(&self) -> &World {
&self.world
}
pub fn world_mut(&mut self) -> &mut World {
&mut self.world
}
pub fn schedule(&self) -> &Schedule {
&self.schedule
}
pub fn add_system<P>(
&mut self,
stage: &'static str,
name: &'static str,
func: impl IntoSystem<P>,
) -> &mut Self {
self.schedule.add_system(stage, name, func);
self
}
pub fn add_plugin(&mut self, plugin: impl Plugin) -> &mut Self {
plugin.build(self);
self
}
pub fn set_tick_rate(&mut self, hz: f64) -> &mut Self {
self.world.insert_resource(FixedTimestep::new(hz));
self
}
pub fn insert_resource<T: 'static>(&mut self, value: T) -> &mut Self {
self.world.insert_resource(value);
self
}
pub fn register_render_channel<T: crate::render_channel::ExtractToFloats>(
&mut self,
name: &str,
) -> &mut Self {
if self
.world
.try_resource::<crate::render_channel::RenderChannelRegistry>()
.is_none()
{
self.world
.insert_resource(crate::render_channel::RenderChannelRegistry::new());
}
self.world
.resource_mut::<crate::render_channel::RenderChannelRegistry>()
.register::<T>(name);
self
}
pub fn tick(&mut self, elapsed: f64) -> u32 {
if !self.has_timestep() {
self.world.insert_resource(FixedTimestep::default_rts());
}
game_loop::tick(&mut self.world, &mut self.schedule, elapsed)
}
pub fn run_once(&mut self) {
self.schedule.run(&mut self.world);
}
pub fn pause(&mut self) {
self.ensure_virtual_time();
self.world.resource_mut::<VirtualTime>().paused = true;
}
pub fn resume(&mut self) {
self.ensure_virtual_time();
self.world.resource_mut::<VirtualTime>().paused = false;
}
pub fn set_speed(&mut self, scale: f64) {
self.ensure_virtual_time();
self.world.resource_mut::<VirtualTime>().scale = scale;
}
pub fn is_paused(&self) -> bool {
self.world
.try_resource::<VirtualTime>()
.is_some_and(|vt| vt.paused)
}
fn has_timestep(&self) -> bool {
self.world.try_resource::<FixedTimestep>().is_some()
}
fn ensure_virtual_time(&mut self) {
if self.world.try_resource::<VirtualTime>().is_none() {
self.world.insert_resource(VirtualTime::new());
}
}
}
impl Default for Engine {
fn default() -> Self {
Self::new()
}
}
pub trait Plugin {
fn build(&self, engine: &mut Engine);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::component::Component;
use crate::system_param::QueryMut;
#[derive(Debug)]
struct Counter(u32);
impl Component for Counter {}
fn increment(mut counters: QueryMut<'_, Counter>) {
for (_, c) in counters.iter_mut() {
c.0 += 1;
}
}
#[test]
fn new_engine_has_empty_world_and_schedule() {
let engine = Engine::new();
assert_eq!(engine.world().entity_count(), 0);
assert_eq!(engine.schedule().system_count(), 0);
}
#[test]
fn world_mut_allows_mutation() {
let mut engine = Engine::new();
engine.world_mut().spawn((Counter(0),));
assert_eq!(engine.world().entity_count(), 1);
}
#[test]
fn add_system_registers_system() {
let mut engine = Engine::new();
engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
assert_eq!(engine.schedule().system_count(), 1);
}
#[test]
fn add_system_is_chainable() {
let mut engine = Engine::new();
engine
.add_system::<(QueryMut<'_, Counter>,)>("pre", "increment", increment)
.add_system::<(QueryMut<'_, Counter>,)>("post", "increment", increment);
assert_eq!(engine.schedule().system_count(), 2);
}
#[test]
fn insert_resource_is_chainable() {
struct Gravity(f32);
let mut engine = Engine::new();
engine.insert_resource(Gravity(9.8));
assert!((engine.world().resource::<Gravity>().0 - 9.8).abs() < f32::EPSILON);
}
struct IncrementPlugin;
impl Plugin for IncrementPlugin {
fn build(&self, engine: &mut Engine) {
engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
}
}
#[test]
fn add_plugin_calls_build() {
let mut engine = Engine::new();
engine.add_plugin(IncrementPlugin);
assert_eq!(engine.schedule().system_count(), 1);
}
#[test]
fn add_plugin_is_chainable() {
let mut engine = Engine::new();
engine
.add_plugin(IncrementPlugin)
.add_plugin(IncrementPlugin);
assert_eq!(engine.schedule().system_count(), 2);
}
#[test]
fn run_once_executes_schedule() {
let mut engine = Engine::new();
engine.world_mut().spawn((Counter(0),));
engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
engine.run_once();
let counts: Vec<u32> = engine
.world()
.query::<&Counter>()
.map(|(_, c)| c.0)
.collect();
assert_eq!(counts, vec![1]);
}
#[test]
fn tick_inserts_default_timestep_when_absent() {
let mut engine = Engine::new();
let ticks = engine.tick(0.05); assert_eq!(ticks, 0);
}
#[test]
fn tick_respects_existing_timestep() {
let mut engine = Engine::new();
engine.world_mut().insert_resource(FixedTimestep::new(10.0));
engine.world_mut().spawn((Counter(0),));
engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
let ticks = engine.tick(0.35);
assert_eq!(ticks, 3);
let counts: Vec<u32> = engine
.world()
.query::<&Counter>()
.map(|(_, c)| c.0)
.collect();
assert_eq!(counts, vec![3]);
}
#[test]
fn set_tick_rate_overrides_default() {
let mut engine = Engine::new();
engine.set_tick_rate(30.0);
engine.world_mut().spawn((Counter(0),));
engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
let ticks = engine.tick(0.1);
assert_eq!(ticks, 3);
}
#[test]
fn tick_returns_correct_tick_count() {
let mut engine = Engine::new();
let ticks = engine.tick(0.25);
assert_eq!(ticks, 2);
}
#[test]
fn pause_and_resume() {
let mut engine = Engine::new();
assert!(!engine.is_paused());
engine.pause();
assert!(engine.is_paused());
engine.resume();
assert!(!engine.is_paused());
}
#[test]
fn pause_stops_ticks() {
let mut engine = Engine::new();
engine.world_mut().spawn((Counter(0),));
engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
engine.pause();
engine.tick(1.0);
let counts: Vec<u32> = engine
.world()
.query::<&Counter>()
.map(|(_, c)| c.0)
.collect();
assert_eq!(counts, vec![0]);
}
#[test]
fn set_speed_doubles_ticks() {
let mut engine = Engine::new();
engine.world_mut().spawn((Counter(0),));
engine.add_system::<(QueryMut<'_, Counter>,)>("update", "increment", increment);
engine.set_speed(2.0);
let ticks = engine.tick(0.1);
assert_eq!(ticks, 2);
}
#[test]
fn set_speed_persists() {
let mut engine = Engine::new();
engine.set_speed(4.0);
let vt = engine.world().resource::<VirtualTime>();
assert!((vt.scale - 4.0).abs() < f64::EPSILON);
}
#[test]
fn lazy_insert_virtual_time() {
let mut engine = Engine::new();
assert!(engine.world().try_resource::<VirtualTime>().is_none());
engine.pause();
assert!(engine.world().try_resource::<VirtualTime>().is_some());
}
#[derive(Debug)]
struct ShaderParams {
intensity: f32,
}
impl Component for ShaderParams {}
impl crate::render_channel::ExtractToFloats for ShaderParams {
const STRIDE: usize = 1;
fn extract(&self, buf: &mut [f32]) {
buf[0] = self.intensity;
}
}
#[test]
fn register_render_channel_creates_registry() {
let mut engine = Engine::new();
engine.register_render_channel::<ShaderParams>("shader");
let reg = engine
.world()
.try_resource::<crate::render_channel::RenderChannelRegistry>()
.expect("registry should exist");
assert_eq!(reg.len(), 1);
}
#[test]
fn register_render_channel_is_chainable() {
let mut engine = Engine::new();
engine
.register_render_channel::<ShaderParams>("shader")
.insert_resource(Counter(0));
assert_eq!(engine.world().resource::<Counter>().0, 0);
}
}