use bevy_ecs::prelude::*;
use bevy_ecs::schedule::IntoScheduleConfigs;
use bevy_ecs::system::ScheduleSystem;
use crate::engine::Engine;
use crate::game_loop::{GameLoop, TickRate};
use crate::schedule::UpdateStage;
use crate::state::EngineState;
#[derive(Resource, Clone, Copy, PartialEq, Eq)]
pub struct TickRateConfig {
pub rate: TickRate,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct LoopConfig {
pub frame_cap: u32,
pub tick_rate: TickRate,
}
impl Default for LoopConfig {
fn default() -> Self {
Self {
frame_cap: 0,
tick_rate: TickRate::Hz60,
}
}
}
#[derive(Resource)]
pub struct Time {
delta_seconds: f32,
raw_delta_seconds: f32,
real_delta_seconds: f32,
elapsed_seconds: f32,
scale: f32,
frame_count: u64,
interp_alpha: f32,
}
impl Time {
#[must_use]
pub fn new() -> Self {
Self {
delta_seconds: 0.0,
raw_delta_seconds: 0.0,
real_delta_seconds: 0.0,
elapsed_seconds: 0.0,
scale: 1.0,
frame_count: 0,
interp_alpha: 0.0,
}
}
#[must_use]
pub const fn delta_seconds(&self) -> f32 {
self.delta_seconds
}
#[must_use]
pub const fn raw_delta_seconds(&self) -> f32 {
self.raw_delta_seconds
}
#[must_use]
pub const fn real_delta_seconds(&self) -> f32 {
self.real_delta_seconds
}
#[must_use]
pub const fn elapsed_seconds(&self) -> f32 {
self.elapsed_seconds
}
#[must_use]
pub const fn time_scale(&self) -> f32 {
self.scale
}
pub fn set_time_scale(&mut self, scale: f32) {
self.scale = scale.max(0.0);
}
#[must_use]
pub const fn frame_count(&self) -> u64 {
self.frame_count
}
pub fn set_delta_seconds(&mut self, delta: f32) {
self.delta_seconds = delta;
self.raw_delta_seconds = delta;
}
pub fn advance(&mut self, fixed_delta: f32) {
self.raw_delta_seconds = fixed_delta;
self.delta_seconds = fixed_delta * self.scale;
self.elapsed_seconds += self.delta_seconds;
self.frame_count += 1;
}
pub fn set_real_delta(&mut self, real_delta: f32) {
self.real_delta_seconds = real_delta;
}
#[must_use]
pub const fn interp_alpha(&self) -> f32 {
self.interp_alpha
}
pub fn set_interp_alpha(&mut self, alpha: f32) {
self.interp_alpha = alpha;
}
}
impl Default for Time {
fn default() -> Self {
Self::new()
}
}
pub struct App {
engine: Engine,
pending_plugins: Vec<Box<dyn GamePlugin>>,
built_plugins: Vec<String>,
startup_run: bool,
}
impl App {
#[must_use]
pub fn new() -> Self {
let mut engine = Engine::new();
engine.world_mut().insert_resource(Time::new());
engine.world_mut().insert_resource(EngineState::Running);
Self {
engine,
pending_plugins: Vec::new(),
built_plugins: Vec::new(),
startup_run: false,
}
}
pub const fn world_mut(&mut self) -> &mut World {
self.engine.world_mut()
}
pub fn insert_resource<R: Resource>(&mut self, resource: R) -> &mut Self {
self.engine.world_mut().insert_resource(resource);
self
}
pub fn add_system<M>(
&mut self,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self {
self.add_system_to_stage(UpdateStage::Update, systems)
}
pub fn add_system_to_stage<M>(
&mut self,
stage: UpdateStage,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self {
self.engine.stage_schedule_mut(stage).add_systems(systems);
self
}
pub fn add_ordered_systems<M>(
&mut self,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self {
self.add_system(systems.chain())
}
pub fn add_ordered_systems_to_stage<M>(
&mut self,
stage: UpdateStage,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self {
self.add_system_to_stage(stage, systems.chain())
}
pub fn add_startup_system<M>(
&mut self,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self {
self.engine.startup_schedule_mut().add_systems(systems);
self
}
pub fn add_plugin(&mut self, plugin: impl GamePlugin + 'static) -> &mut Self {
self.pending_plugins.push(Box::new(plugin));
self
}
fn build_plugins(&mut self) {
let mut built = std::mem::take(&mut self.built_plugins);
let mut pending = std::mem::take(&mut self.pending_plugins);
let mut ready: Vec<Box<dyn GamePlugin>> = Vec::new();
loop {
pending.append(&mut self.pending_plugins);
if pending.is_empty() {
break;
}
let mut progressed = false;
for mut plugin in std::mem::take(&mut pending) {
let deps_met = plugin
.dependencies()
.iter()
.all(|dep| built.iter().any(|name| name.as_str() == *dep));
if deps_met {
plugin.build(self);
built.push(plugin.name().to_string());
ready.push(plugin);
progressed = true;
} else {
pending.push(plugin);
}
}
if !progressed && self.pending_plugins.is_empty() {
break;
}
}
self.pending_plugins = pending;
self.built_plugins = built;
if !self.pending_plugins.is_empty() {
log::warn!(
"{} plugins could not be built (missing dependencies or circular deps)",
self.pending_plugins.len()
);
}
for mut plugin in ready {
plugin.finish(self);
}
}
pub const fn engine(&self) -> &Engine {
&self.engine
}
pub const fn engine_mut(&mut self) -> &mut Engine {
&mut self.engine
}
pub fn run(&mut self, config: LoopConfig) {
self.run_with_events(config, |_| {});
}
pub fn run_with_events<F>(&mut self, config: LoopConfig, mut process_events: F)
where
F: FnMut(&mut World),
{
self.build_plugins();
if !self.startup_run {
self.engine.run_startup();
self.startup_run = true;
}
self.engine.world_mut().insert_resource(TickRateConfig {
rate: config.tick_rate,
});
let mut fixed_delta = config.tick_rate.delta_seconds();
let mut game_loop = GameLoop::new(config.frame_cap, config.tick_rate);
while game_loop.is_running() {
if let Some(cfg) = self.engine.world().get_resource::<TickRateConfig>()
&& cfg.rate != game_loop.tick_rate()
{
game_loop.set_tick_rate(cfg.rate);
fixed_delta = cfg.rate.delta_seconds();
}
let (ticks, frame_delta) = game_loop.tick();
let alpha = game_loop.interpolation_alpha();
if let Some(mut time) = self.engine.world_mut().get_resource_mut::<Time>() {
time.set_real_delta(frame_delta);
time.set_interp_alpha(alpha);
}
for _ in 0..ticks {
if let Some(mut time) = self.engine.world_mut().get_resource_mut::<Time>() {
time.advance(fixed_delta);
}
self.engine.run_logic_tick();
}
self.engine.run_render_and_post();
if let Some(state) = self.engine.world().get_resource::<EngineState>()
&& state.is_stopping()
{
break;
}
game_loop.apply_frame_cap();
process_events(self.engine.world_mut());
}
}
pub fn tick(&mut self, fixed_delta: f32) {
if !self.pending_plugins.is_empty() {
self.build_plugins();
}
if !self.startup_run {
self.engine.run_startup();
self.startup_run = true;
}
if let Some(mut time) = self.engine.world_mut().get_resource_mut::<Time>() {
time.advance(fixed_delta);
}
self.engine.run_stages();
}
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
pub trait GamePlugin: Send {
fn name(&self) -> &str;
fn dependencies(&self) -> &[&str] {
&[]
}
fn build(&mut self, _app: &mut App) {}
fn finish(&mut self, _app: &mut App) {}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
type Log = Arc<Mutex<Vec<String>>>;
type Spawn = Box<dyn FnOnce(&mut App) + Send>;
struct Recorder {
name: &'static str,
deps: Vec<&'static str>,
log: Log,
spawn: Option<Spawn>,
}
impl GamePlugin for Recorder {
fn name(&self) -> &str {
self.name
}
fn dependencies(&self) -> &[&str] {
&self.deps
}
fn build(&mut self, app: &mut App) {
self.log
.lock()
.unwrap()
.push(format!("build:{}", self.name));
if let Some(spawn) = self.spawn.take() {
spawn(app);
}
}
fn finish(&mut self, _app: &mut App) {
self.log
.lock()
.unwrap()
.push(format!("finish:{}", self.name));
}
}
fn calls(log: &Log) -> Vec<String> {
log.lock().unwrap().clone()
}
#[test]
fn builds_dependencies_before_dependents() {
let log: Log = Arc::new(Mutex::new(Vec::new()));
let mut app = App::new();
app.add_plugin(Recorder {
name: "b",
deps: vec!["a"],
log: log.clone(),
spawn: None,
});
app.add_plugin(Recorder {
name: "a",
deps: vec![],
log: log.clone(),
spawn: None,
});
app.build_plugins();
let c = calls(&log);
let build_a = c.iter().position(|x| x == "build:a").expect("a built");
let build_b = c.iter().position(|x| x == "build:b").expect("b built");
assert!(build_a < build_b, "a must build before b: {c:?}");
let last_build = c.iter().rposition(|x| x.starts_with("build:")).unwrap();
let first_finish = c.iter().position(|x| x.starts_with("finish:")).unwrap();
assert!(
last_build < first_finish,
"all builds precede finishes: {c:?}"
);
}
#[test]
fn absorbs_plugin_registered_during_build() {
let log: Log = Arc::new(Mutex::new(Vec::new()));
let log_for_child = log.clone();
let mut app = App::new();
app.add_plugin(Recorder {
name: "parent",
deps: vec![],
log: log.clone(),
spawn: Some(Box::new(move |app| {
app.add_plugin(Recorder {
name: "child",
deps: vec!["parent"],
log: log_for_child.clone(),
spawn: None,
});
})),
});
app.build_plugins();
let c = calls(&log);
assert!(
c.contains(&"build:parent".to_string()),
"parent built: {c:?}"
);
assert!(
c.contains(&"build:child".to_string()),
"child registered during build must be built: {c:?}"
);
assert!(app.pending_plugins.is_empty(), "no plugins left pending");
}
#[test]
fn leaves_unresolved_dependency_unbuilt() {
let log: Log = Arc::new(Mutex::new(Vec::new()));
let mut app = App::new();
app.add_plugin(Recorder {
name: "needy",
deps: vec!["missing"],
log: log.clone(),
spawn: None,
});
app.build_plugins();
assert!(
!calls(&log).iter().any(|x| x == "build:needy"),
"plugin with a missing dep must not build"
);
assert_eq!(
app.pending_plugins.len(),
1,
"the unresolved plugin stays pending"
);
}
}