Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,503 @@
|
||||
//! The application core: an [`App`] assembled by registering [`Module`]s.
|
||||
//!
|
||||
//! Stage 5 ties the core framework together. An `App` owns the shared engine
|
||||
//! state — the [`Scene`], the [`AssetServer`], the [`TypeRegistry`], the
|
||||
//! [`LayerRegistry`], frame [`Time`], and arbitrary user resources — plus a
|
||||
//! [`Schedule`] of systems. Functionality is added by **modules**: each
|
||||
//! [`Module::build`] registers systems, component types, asset loaders, and
|
||||
//! resources, so the engine is composed rather than hard-wired and an exported
|
||||
//! game compiles in only the modules it uses.
|
||||
//!
|
||||
//! ```
|
||||
//! use oxide_engine::app::{App, DefaultModules};
|
||||
//!
|
||||
//! let mut app = App::new();
|
||||
//! app.add_modules(DefaultModules);
|
||||
//! app.update(1.0 / 60.0); // advance one frame
|
||||
//! ```
|
||||
|
||||
mod module;
|
||||
mod schedule;
|
||||
|
||||
pub use module::{CoreModule, DefaultModules, Module, RenderModule};
|
||||
pub use schedule::Schedule;
|
||||
|
||||
use std::any::{Any, TypeId};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use schedule::{run_phase, SystemEntry, Systems};
|
||||
|
||||
use crate::asset::{AssetLoader, AssetServer};
|
||||
use crate::layer::LayerRegistry;
|
||||
use crate::reflect::TypeRegistry;
|
||||
use crate::scene::Scene;
|
||||
|
||||
/// The default fixed-timestep duration (60 Hz) for [`Schedule::FixedUpdate`].
|
||||
pub const DEFAULT_FIXED_TIMESTEP: f32 = 1.0 / 60.0;
|
||||
|
||||
/// An upper bound on fixed steps per frame, so a long stall (e.g. a breakpoint)
|
||||
/// cannot trigger an unbounded catch-up "spiral of death".
|
||||
const MAX_FIXED_STEPS_PER_FRAME: u32 = 8;
|
||||
|
||||
/// Per-frame timing, refreshed by [`App::update`] and readable by systems.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Time {
|
||||
/// Seconds elapsed since the previous frame.
|
||||
pub delta: f32,
|
||||
/// Seconds elapsed since the app started.
|
||||
pub elapsed: f32,
|
||||
/// The fixed-timestep duration used by [`Schedule::FixedUpdate`].
|
||||
pub fixed_delta: f32,
|
||||
/// Frames advanced so far.
|
||||
pub frame: u64,
|
||||
}
|
||||
|
||||
impl Default for Time {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
delta: 0.0,
|
||||
elapsed: 0.0,
|
||||
fixed_delta: DEFAULT_FIXED_TIMESTEP,
|
||||
frame: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The application core. See the [module docs](self).
|
||||
pub struct App {
|
||||
/// The active scene graph.
|
||||
pub scene: Scene,
|
||||
/// The shared asset server (built-in loaders registered).
|
||||
pub assets: AssetServer,
|
||||
/// The reflection/type registry for dual-editable components.
|
||||
pub types: TypeRegistry,
|
||||
/// The project's named layers.
|
||||
pub layers: LayerRegistry,
|
||||
/// Per-frame timing.
|
||||
pub time: Time,
|
||||
|
||||
resources: HashMap<TypeId, Box<dyn Any>>,
|
||||
systems: Systems,
|
||||
|
||||
/// Registered modules → enabled flag.
|
||||
modules: BTreeMap<&'static str, bool>,
|
||||
/// The module currently being built, so registrations can be attributed.
|
||||
current_module: Option<&'static str>,
|
||||
/// Per-module bookkeeping for clean removal.
|
||||
module_types: HashMap<&'static str, Vec<&'static str>>,
|
||||
module_loaders: HashMap<&'static str, Vec<String>>,
|
||||
module_resources: HashMap<&'static str, Vec<TypeId>>,
|
||||
|
||||
fixed_accumulator: f32,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// A new app with empty core state and no modules. The [`AssetServer`] comes
|
||||
/// with the engine's built-in loaders already registered.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
scene: Scene::new(),
|
||||
assets: AssetServer::new(),
|
||||
types: TypeRegistry::new(),
|
||||
layers: LayerRegistry::new(),
|
||||
time: Time::default(),
|
||||
resources: HashMap::new(),
|
||||
systems: Systems::default(),
|
||||
modules: BTreeMap::new(),
|
||||
current_module: None,
|
||||
module_types: HashMap::new(),
|
||||
module_loaders: HashMap::new(),
|
||||
module_resources: HashMap::new(),
|
||||
fixed_accumulator: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Modules -----------------------------------------------------------
|
||||
|
||||
/// Adds a module, running its [`Module::build`] and attributing everything
|
||||
/// it registers to it.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if a module with the same [`name`](Module::name) is already added.
|
||||
pub fn add_module<M: Module>(&mut self, module: M) -> &mut Self {
|
||||
let name = module.name();
|
||||
assert!(
|
||||
!self.modules.contains_key(name),
|
||||
"module '{name}' is already added"
|
||||
);
|
||||
self.modules.insert(name, true);
|
||||
let previous = self.current_module.replace(name);
|
||||
module.build(self);
|
||||
self.current_module = previous;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a bundle of modules (e.g. [`DefaultModules`]).
|
||||
pub fn add_modules<B: ModuleBundle>(&mut self, bundle: B) -> &mut Self {
|
||||
bundle.add_to(self);
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether a module is registered.
|
||||
pub fn has_module(&self, name: &str) -> bool {
|
||||
self.modules.contains_key(name)
|
||||
}
|
||||
|
||||
/// The registered module names, sorted.
|
||||
pub fn modules(&self) -> impl Iterator<Item = &'static str> + '_ {
|
||||
self.modules.keys().copied()
|
||||
}
|
||||
|
||||
/// Whether a registered module is enabled. Unknown modules report `false`.
|
||||
pub fn is_module_enabled(&self, name: &str) -> bool {
|
||||
self.modules.get(name).copied().unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Enables or disables a module's systems without removing them. Disabled
|
||||
/// modules' systems are skipped each frame. Returns whether the module exists.
|
||||
pub fn set_module_enabled(&mut self, name: &str, enabled: bool) -> bool {
|
||||
match self.modules.get_mut(name) {
|
||||
Some(flag) => {
|
||||
*flag = enabled;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a module and everything it contributed — systems, registered
|
||||
/// component types, asset loaders, and resources — leaving no dangling
|
||||
/// references. Returns whether the module existed.
|
||||
pub fn remove_module(&mut self, name: &str) -> bool {
|
||||
if self.modules.remove(name).is_none() {
|
||||
return false;
|
||||
}
|
||||
self.systems.remove_module(name);
|
||||
for type_name in self.module_types.remove(name).unwrap_or_default() {
|
||||
self.types.unregister(type_name);
|
||||
}
|
||||
for ext in self.module_loaders.remove(name).unwrap_or_default() {
|
||||
self.assets.unregister_loader(&ext);
|
||||
}
|
||||
for type_id in self.module_resources.remove(name).unwrap_or_default() {
|
||||
self.resources.remove(&type_id);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether a system contributed by `module` should run this frame: systems
|
||||
/// with no owning module always run; module-owned systems run only while
|
||||
/// their module is enabled.
|
||||
pub(crate) fn is_system_enabled(&self, module: Option<&'static str>) -> bool {
|
||||
match module {
|
||||
None => true,
|
||||
Some(name) => self.is_module_enabled(name),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Registration (attributed to the current module) -------------------
|
||||
|
||||
/// Adds a system to a schedule phase. Systems run in phase order, then in
|
||||
/// registration order within a phase.
|
||||
pub fn add_system(
|
||||
&mut self,
|
||||
phase: Schedule,
|
||||
system: impl FnMut(&mut App) + 'static,
|
||||
) -> &mut Self {
|
||||
self.systems.push(
|
||||
phase,
|
||||
SystemEntry {
|
||||
module: self.current_module,
|
||||
run: Box::new(system),
|
||||
},
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Registers a reflected component type under `name` (see [`TypeRegistry`]).
|
||||
pub fn register_type<T>(&mut self, name: &'static str) -> &mut Self
|
||||
where
|
||||
T: hecs::Component + serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
self.types.register::<T>(name);
|
||||
if let Some(module) = self.current_module {
|
||||
self.module_types.entry(module).or_default().push(name);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Registers an asset loader (see [`AssetServer::register_loader`]).
|
||||
pub fn add_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self {
|
||||
if let Some(module) = self.current_module {
|
||||
let exts = loader.extensions().iter().map(|e| e.to_lowercase());
|
||||
self.module_loaders.entry(module).or_default().extend(exts);
|
||||
}
|
||||
self.assets.register_loader(loader);
|
||||
self
|
||||
}
|
||||
|
||||
// --- Resources ---------------------------------------------------------
|
||||
|
||||
/// Inserts (or replaces) a shared resource of type `T`.
|
||||
pub fn insert_resource<T: 'static>(&mut self, value: T) -> &mut Self {
|
||||
let id = TypeId::of::<T>();
|
||||
if let Some(module) = self.current_module {
|
||||
self.module_resources.entry(module).or_default().push(id);
|
||||
}
|
||||
self.resources.insert(id, Box::new(value));
|
||||
self
|
||||
}
|
||||
|
||||
/// Borrows a resource of type `T`, or `None` if absent.
|
||||
pub fn get_resource<T: 'static>(&self) -> Option<&T> {
|
||||
self.resources
|
||||
.get(&TypeId::of::<T>())
|
||||
.and_then(|b| b.downcast_ref::<T>())
|
||||
}
|
||||
|
||||
/// Mutably borrows a resource of type `T`, or `None` if absent.
|
||||
pub fn get_resource_mut<T: 'static>(&mut self) -> Option<&mut T> {
|
||||
self.resources
|
||||
.get_mut(&TypeId::of::<T>())
|
||||
.and_then(|b| b.downcast_mut::<T>())
|
||||
}
|
||||
|
||||
/// Removes and returns the resource of type `T`, or `None` if absent.
|
||||
///
|
||||
/// Lets a system take exclusive ownership of a resource for the duration of
|
||||
/// a call — e.g. the physics step takes the `PhysicsWorld` out so it can
|
||||
/// borrow the [`Scene`] mutably at the same time — then re-inserts it.
|
||||
pub fn remove_resource<T: 'static>(&mut self) -> Option<T> {
|
||||
self.resources
|
||||
.remove(&TypeId::of::<T>())
|
||||
.and_then(|b| b.downcast::<T>().ok())
|
||||
.map(|b| *b)
|
||||
}
|
||||
|
||||
/// Whether a resource of type `T` is present.
|
||||
pub fn has_resource<T: 'static>(&self) -> bool {
|
||||
self.resources.contains_key(&TypeId::of::<T>())
|
||||
}
|
||||
|
||||
// --- Running -----------------------------------------------------------
|
||||
|
||||
/// Sets the fixed-timestep duration used by [`Schedule::FixedUpdate`].
|
||||
pub fn set_fixed_timestep(&mut self, seconds: f32) -> &mut Self {
|
||||
assert!(seconds > 0.0, "fixed timestep must be positive");
|
||||
self.time.fixed_delta = seconds;
|
||||
self
|
||||
}
|
||||
|
||||
/// The number of systems registered across all phases.
|
||||
pub fn system_count(&self) -> usize {
|
||||
self.systems.total()
|
||||
}
|
||||
|
||||
/// Advances one frame by `delta` seconds: runs the per-frame phases once and
|
||||
/// [`FixedUpdate`](Schedule::FixedUpdate) as many whole fixed steps as the
|
||||
/// accumulated time allows (capped to avoid a catch-up spiral).
|
||||
pub fn update(&mut self, delta: f32) {
|
||||
self.time.delta = delta;
|
||||
self.time.elapsed += delta;
|
||||
self.time.frame += 1;
|
||||
|
||||
// How many fixed steps to run this frame.
|
||||
self.fixed_accumulator += delta;
|
||||
let mut steps = (self.fixed_accumulator / self.time.fixed_delta) as u32;
|
||||
if steps > MAX_FIXED_STEPS_PER_FRAME {
|
||||
steps = MAX_FIXED_STEPS_PER_FRAME;
|
||||
self.fixed_accumulator = 0.0;
|
||||
} else {
|
||||
self.fixed_accumulator -= steps as f32 * self.time.fixed_delta;
|
||||
}
|
||||
|
||||
self.run_frame(steps);
|
||||
}
|
||||
|
||||
/// Advances **exactly one fixed timestep**: bumps frame time by
|
||||
/// [`fixed_delta`](Time::fixed_delta) and runs the per-frame phases once with
|
||||
/// a single [`FixedUpdate`](Schedule::FixedUpdate), bypassing the
|
||||
/// accumulator. This is the editor play-mode **Step** primitive — single-step
|
||||
/// the simulation while paused — and yields one deterministic tick.
|
||||
pub fn step(&mut self) {
|
||||
let dt = self.time.fixed_delta;
|
||||
self.time.delta = dt;
|
||||
self.time.elapsed += dt;
|
||||
self.time.frame += 1;
|
||||
self.run_frame(1);
|
||||
}
|
||||
|
||||
/// Runs the per-frame phases once with `fixed_steps` runs of
|
||||
/// [`FixedUpdate`](Schedule::FixedUpdate). The systems are moved out first so
|
||||
/// each gets exclusive `&mut App`, then anything registered mid-frame is
|
||||
/// folded back. Shared by [`update`](Self::update) and [`step`](Self::step).
|
||||
fn run_frame(&mut self, fixed_steps: u32) {
|
||||
let mut systems = std::mem::take(&mut self.systems);
|
||||
run_phase(&mut systems, self, Schedule::First);
|
||||
run_phase(&mut systems, self, Schedule::Input);
|
||||
run_phase(&mut systems, self, Schedule::PreUpdate);
|
||||
for _ in 0..fixed_steps {
|
||||
run_phase(&mut systems, self, Schedule::FixedUpdate);
|
||||
}
|
||||
run_phase(&mut systems, self, Schedule::Update);
|
||||
run_phase(&mut systems, self, Schedule::PostUpdate);
|
||||
run_phase(&mut systems, self, Schedule::Render);
|
||||
run_phase(&mut systems, self, Schedule::Last);
|
||||
|
||||
// Fold back anything registered during the frame, then restore.
|
||||
systems.merge(std::mem::take(&mut self.systems));
|
||||
self.systems = systems;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// A group of modules added together. Implemented for [`DefaultModules`] and for
|
||||
/// tuples, so `app.add_modules((ModuleA, ModuleB))` works.
|
||||
pub trait ModuleBundle {
|
||||
/// Adds every module in the bundle to `app`.
|
||||
fn add_to(self, app: &mut App);
|
||||
}
|
||||
|
||||
impl<A: Module> ModuleBundle for (A,) {
|
||||
fn add_to(self, app: &mut App) {
|
||||
app.add_module(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Module, B: Module> ModuleBundle for (A, B) {
|
||||
fn add_to(self, app: &mut App) {
|
||||
app.add_module(self.0);
|
||||
app.add_module(self.1);
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Module, B: Module, C: Module> ModuleBundle for (A, B, C) {
|
||||
fn add_to(self, app: &mut App) {
|
||||
app.add_module(self.0);
|
||||
app.add_module(self.1);
|
||||
app.add_module(self.2);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::math::{Transform, Vec3};
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[test]
|
||||
fn empty_app_updates_and_advances_time() {
|
||||
let mut app = App::new();
|
||||
assert_eq!(app.time.frame, 0);
|
||||
app.update(0.5);
|
||||
assert_eq!(app.time.frame, 1);
|
||||
assert!((app.time.elapsed - 0.5).abs() < 1e-6);
|
||||
assert!((app.time.delta - 0.5).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn systems_run_in_phase_then_registration_order() {
|
||||
let log = Rc::new(std::cell::RefCell::new(Vec::new()));
|
||||
let mut app = App::new();
|
||||
let l = log.clone();
|
||||
app.add_system(Schedule::Update, move |_| l.borrow_mut().push("update-1"));
|
||||
let l = log.clone();
|
||||
app.add_system(Schedule::Update, move |_| l.borrow_mut().push("update-2"));
|
||||
let l = log.clone();
|
||||
app.add_system(Schedule::First, move |_| l.borrow_mut().push("first"));
|
||||
app.update(0.0);
|
||||
assert_eq!(*log.borrow(), vec!["first", "update-1", "update-2"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_update_runs_by_accumulated_time() {
|
||||
let count = Rc::new(Cell::new(0u32));
|
||||
let mut app = App::new();
|
||||
app.set_fixed_timestep(0.1);
|
||||
let c = count.clone();
|
||||
app.add_system(Schedule::FixedUpdate, move |_| c.set(c.get() + 1));
|
||||
|
||||
app.update(0.25); // 0.25 / 0.1 = 2 whole steps, ~0.05 left over
|
||||
assert_eq!(count.get(), 2);
|
||||
// 0.05 carried + 0.06 = 0.11 -> 1 more step (kept off the exact float
|
||||
// boundary so the result is robust to f32 rounding).
|
||||
app.update(0.06);
|
||||
assert_eq!(count.get(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_update_is_capped_against_spiral() {
|
||||
let count = Rc::new(Cell::new(0u32));
|
||||
let mut app = App::new();
|
||||
app.set_fixed_timestep(0.001);
|
||||
let c = count.clone();
|
||||
app.add_system(Schedule::FixedUpdate, move |_| c.set(c.get() + 1));
|
||||
app.update(10.0); // would be 10000 steps; capped
|
||||
assert_eq!(count.get(), MAX_FIXED_STEPS_PER_FRAME);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_runs_one_fixed_tick_and_the_per_frame_phases_once() {
|
||||
let fixed = Rc::new(Cell::new(0u32));
|
||||
let update = Rc::new(Cell::new(0u32));
|
||||
let mut app = App::new();
|
||||
app.set_fixed_timestep(0.1);
|
||||
let f = fixed.clone();
|
||||
app.add_system(Schedule::FixedUpdate, move |_| f.set(f.get() + 1));
|
||||
let u = update.clone();
|
||||
app.add_system(Schedule::Update, move |_| u.set(u.get() + 1));
|
||||
|
||||
app.step();
|
||||
// Exactly one fixed step and one Update, regardless of accumulator.
|
||||
assert_eq!(fixed.get(), 1);
|
||||
assert_eq!(update.get(), 1);
|
||||
assert_eq!(app.time.frame, 1);
|
||||
assert!((app.time.elapsed - 0.1).abs() < 1e-6);
|
||||
assert!((app.time.delta - 0.1).abs() < 1e-6);
|
||||
|
||||
// A second step advances exactly one more, deterministically.
|
||||
app.step();
|
||||
assert_eq!(fixed.get(), 2);
|
||||
assert_eq!(update.get(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resources_round_trip() {
|
||||
let mut app = App::new();
|
||||
app.insert_resource(42u32);
|
||||
assert_eq!(app.get_resource::<u32>(), Some(&42));
|
||||
*app.get_resource_mut::<u32>().unwrap() += 1;
|
||||
assert_eq!(app.get_resource::<u32>(), Some(&43));
|
||||
assert!(app.get_resource::<String>().is_none());
|
||||
|
||||
// remove_resource takes ownership and clears the slot.
|
||||
assert_eq!(app.remove_resource::<u32>(), Some(43));
|
||||
assert!(!app.has_resource::<u32>());
|
||||
assert_eq!(app.remove_resource::<u32>(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_system_can_mutate_the_scene_each_frame() {
|
||||
let mut app = App::new();
|
||||
app.scene.spawn("a", Transform::IDENTITY);
|
||||
// Each Update, nudge every entity's transform.
|
||||
app.add_system(Schedule::Update, |app| {
|
||||
let entities: Vec<_> = app.scene.entities().collect();
|
||||
for e in entities {
|
||||
if let Some(mut t) = app.scene.get_mut::<Transform>(e) {
|
||||
t.translation += Vec3::X;
|
||||
}
|
||||
}
|
||||
});
|
||||
app.update(0.0);
|
||||
app.update(0.0);
|
||||
let e = app.scene.entities().next().unwrap();
|
||||
assert!((app.scene.local_transform(e).unwrap().translation.x - 2.0).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
//! The [`Module`] trait and the engine's built-in modules.
|
||||
//!
|
||||
//! A module is the unit of engine extension: it bundles systems, component
|
||||
//! types, asset loaders, and resources behind one documented entry point, so
|
||||
//! anyone — including AI agents — can add a capability by writing a module, and
|
||||
//! an exported game compiles in only the modules it registers. The editor
|
||||
//! integration half of the trait arrives in Stage 6.
|
||||
|
||||
use super::{App, ModuleBundle, Schedule};
|
||||
use crate::layer::{Layer, Tags};
|
||||
use crate::math::Transform;
|
||||
use crate::render::MeshRenderer;
|
||||
use crate::scene::Node;
|
||||
|
||||
/// A self-contained unit of engine functionality.
|
||||
///
|
||||
/// Implement [`build`](Self::build) to register everything the module provides
|
||||
/// via the [`App`] facade ([`add_system`](App::add_system),
|
||||
/// [`register_type`](App::register_type), [`add_loader`](App::add_loader),
|
||||
/// [`insert_resource`](App::insert_resource)). Everything registered during
|
||||
/// `build` is attributed to the module, so it can be enabled, disabled, or
|
||||
/// removed as a unit.
|
||||
pub trait Module: 'static {
|
||||
/// A stable, unique name (used to enable/disable/remove the module and, in
|
||||
/// later stages, to express dependencies).
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Registers the module's systems, types, loaders, and resources on `app`.
|
||||
fn build(&self, app: &mut App);
|
||||
}
|
||||
|
||||
/// The core module: registers the always-present scene component types for
|
||||
/// reflection (dual-editability), so the editor and scripts can address them.
|
||||
///
|
||||
/// This is the runtime "wrapper" for the math/scene/layer building blocks that
|
||||
/// already exist as plain library types — it does not add behavior, it exposes
|
||||
/// those types through the [`TypeRegistry`](crate::reflect::TypeRegistry).
|
||||
pub struct CoreModule;
|
||||
|
||||
impl Module for CoreModule {
|
||||
fn name(&self) -> &'static str {
|
||||
"core"
|
||||
}
|
||||
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<Transform>("Transform");
|
||||
app.register_type::<Node>("Node");
|
||||
app.register_type::<Layer>("Layer");
|
||||
app.register_type::<Tags>("Tags");
|
||||
}
|
||||
}
|
||||
|
||||
/// The render module: registers the renderable scene components for reflection.
|
||||
///
|
||||
/// The forward renderer itself is driven by the editor/host today; this module
|
||||
/// is what makes [`MeshRenderer`] a first-class, dual-editable component. As the
|
||||
/// data-driven render pipeline grows it will register its render-phase systems
|
||||
/// here too.
|
||||
pub struct RenderModule;
|
||||
|
||||
impl Module for RenderModule {
|
||||
fn name(&self) -> &'static str {
|
||||
"render"
|
||||
}
|
||||
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<MeshRenderer>("MeshRenderer");
|
||||
// Placeholder render-phase system so the phase is exercised; real passes
|
||||
// land with the Stage 5 render pipeline piece.
|
||||
app.add_system(Schedule::Render, |_app| {});
|
||||
}
|
||||
}
|
||||
|
||||
/// The engine's standard set of built-in modules, added with
|
||||
/// [`App::add_modules`](super::App::add_modules).
|
||||
///
|
||||
/// ```
|
||||
/// use oxide_engine::app::{App, DefaultModules};
|
||||
/// let mut app = App::new();
|
||||
/// app.add_modules(DefaultModules);
|
||||
/// assert!(app.has_module("core") && app.has_module("render"));
|
||||
/// ```
|
||||
pub struct DefaultModules;
|
||||
|
||||
impl ModuleBundle for DefaultModules {
|
||||
fn add_to(self, app: &mut App) {
|
||||
app.add_module(CoreModule);
|
||||
app.add_module(RenderModule);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::render::PrimitiveShape;
|
||||
|
||||
#[test]
|
||||
fn default_modules_register_core_types() {
|
||||
let mut app = App::new();
|
||||
app.add_modules(DefaultModules);
|
||||
assert!(app.has_module("core"));
|
||||
assert!(app.has_module("render"));
|
||||
assert!(app.types.is_registered("Transform"));
|
||||
assert!(app.types.is_registered("MeshRenderer"));
|
||||
assert_eq!(app.modules().collect::<Vec<_>>(), vec!["core", "render"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_a_module_removes_its_contributions() {
|
||||
let mut app = App::new();
|
||||
app.add_modules(DefaultModules);
|
||||
assert!(app.types.is_registered("MeshRenderer"));
|
||||
let systems_before = app.system_count();
|
||||
|
||||
assert!(app.remove_module("render"));
|
||||
// Its registered type is gone, its render system is gone, core remains.
|
||||
assert!(!app.has_module("render"));
|
||||
assert!(!app.types.is_registered("MeshRenderer"));
|
||||
assert!(app.types.is_registered("Transform"));
|
||||
assert!(app.system_count() < systems_before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabling_a_module_skips_its_systems_without_removing() {
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
struct Ticker(Rc<Cell<u32>>);
|
||||
impl Module for Ticker {
|
||||
fn name(&self) -> &'static str {
|
||||
"ticker"
|
||||
}
|
||||
fn build(&self, app: &mut App) {
|
||||
let counter = self.0.clone();
|
||||
app.add_system(Schedule::Update, move |_| counter.set(counter.get() + 1));
|
||||
}
|
||||
}
|
||||
|
||||
let count = Rc::new(Cell::new(0u32));
|
||||
let mut app = App::new();
|
||||
app.add_module(Ticker(count.clone()));
|
||||
|
||||
app.update(0.0);
|
||||
assert_eq!(count.get(), 1);
|
||||
|
||||
app.set_module_enabled("ticker", false);
|
||||
app.update(0.0); // skipped
|
||||
assert_eq!(count.get(), 1);
|
||||
|
||||
app.set_module_enabled("ticker", true);
|
||||
app.update(0.0); // runs again
|
||||
assert_eq!(count.get(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_module_can_add_a_loader_removed_with_it() {
|
||||
// A module registers MeshRenderer + uses a primitive, then is removed.
|
||||
let mut app = App::new();
|
||||
app.add_module(RenderModule);
|
||||
// Sanity: the primitive enum the render component references is usable.
|
||||
assert_eq!(PrimitiveShape::ALL.len(), 3);
|
||||
assert!(app.remove_module("render"));
|
||||
assert!(!app.types.is_registered("MeshRenderer"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
//! The system schedule: the ordered phases an [`App`](super::App) runs each
|
||||
//! frame, and the per-phase lists of systems modules attach to.
|
||||
|
||||
use super::App;
|
||||
|
||||
/// The ordered phases of one frame.
|
||||
///
|
||||
/// Systems are attached to a phase and run in phase order; within a phase they
|
||||
/// run in registration order, so behavior is fully deterministic. The phases
|
||||
/// mirror a conventional game loop:
|
||||
///
|
||||
/// - [`First`](Self::First) — start-of-frame bookkeeping.
|
||||
/// - [`Input`](Self::Input) — gather input (Stage 7).
|
||||
/// - [`PreUpdate`](Self::PreUpdate) — engine work before game logic.
|
||||
/// - [`FixedUpdate`](Self::FixedUpdate) — fixed-timestep work; runs **zero or
|
||||
/// more** times per frame so simulation is frame-rate independent. Physics
|
||||
/// (Stage 9) lives here.
|
||||
/// - [`Update`](Self::Update) — per-frame game logic.
|
||||
/// - [`PostUpdate`](Self::PostUpdate) — engine work after game logic.
|
||||
/// - [`Render`](Self::Render) — drawing (Stage 5 pipeline onward).
|
||||
/// - [`Last`](Self::Last) — end-of-frame cleanup.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum Schedule {
|
||||
First,
|
||||
Input,
|
||||
PreUpdate,
|
||||
FixedUpdate,
|
||||
Update,
|
||||
PostUpdate,
|
||||
Render,
|
||||
Last,
|
||||
}
|
||||
|
||||
impl Schedule {
|
||||
/// The once-per-frame phases, in order (everything except `FixedUpdate`,
|
||||
/// which is driven separately by the fixed-timestep accumulator).
|
||||
pub(crate) const PER_FRAME: [Schedule; 7] = [
|
||||
Schedule::First,
|
||||
Schedule::Input,
|
||||
Schedule::PreUpdate,
|
||||
Schedule::Update,
|
||||
Schedule::PostUpdate,
|
||||
Schedule::Render,
|
||||
Schedule::Last,
|
||||
];
|
||||
}
|
||||
|
||||
/// One registered system: a closure plus the module that contributed it (so the
|
||||
/// module can be disabled or removed).
|
||||
pub(crate) struct SystemEntry {
|
||||
pub(crate) module: Option<&'static str>,
|
||||
pub(crate) run: Box<dyn FnMut(&mut App)>,
|
||||
}
|
||||
|
||||
/// The collection of systems, grouped by phase, in registration order.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Systems {
|
||||
first: Vec<SystemEntry>,
|
||||
input: Vec<SystemEntry>,
|
||||
pre_update: Vec<SystemEntry>,
|
||||
fixed_update: Vec<SystemEntry>,
|
||||
update: Vec<SystemEntry>,
|
||||
post_update: Vec<SystemEntry>,
|
||||
render: Vec<SystemEntry>,
|
||||
last: Vec<SystemEntry>,
|
||||
}
|
||||
|
||||
impl Systems {
|
||||
fn phase_mut(&mut self, phase: Schedule) -> &mut Vec<SystemEntry> {
|
||||
match phase {
|
||||
Schedule::First => &mut self.first,
|
||||
Schedule::Input => &mut self.input,
|
||||
Schedule::PreUpdate => &mut self.pre_update,
|
||||
Schedule::FixedUpdate => &mut self.fixed_update,
|
||||
Schedule::Update => &mut self.update,
|
||||
Schedule::PostUpdate => &mut self.post_update,
|
||||
Schedule::Render => &mut self.render,
|
||||
Schedule::Last => &mut self.last,
|
||||
}
|
||||
}
|
||||
|
||||
fn phase(&self, phase: Schedule) -> &[SystemEntry] {
|
||||
match phase {
|
||||
Schedule::First => &self.first,
|
||||
Schedule::Input => &self.input,
|
||||
Schedule::PreUpdate => &self.pre_update,
|
||||
Schedule::FixedUpdate => &self.fixed_update,
|
||||
Schedule::Update => &self.update,
|
||||
Schedule::PostUpdate => &self.post_update,
|
||||
Schedule::Render => &self.render,
|
||||
Schedule::Last => &self.last,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn push(&mut self, phase: Schedule, entry: SystemEntry) {
|
||||
self.phase_mut(phase).push(entry);
|
||||
}
|
||||
|
||||
pub(crate) fn total(&self) -> usize {
|
||||
Schedule::PER_FRAME
|
||||
.iter()
|
||||
.chain(std::iter::once(&Schedule::FixedUpdate))
|
||||
.map(|p| self.phase(*p).len())
|
||||
.sum()
|
||||
}
|
||||
|
||||
const ALL_PHASES: [Schedule; 8] = [
|
||||
Schedule::First,
|
||||
Schedule::Input,
|
||||
Schedule::PreUpdate,
|
||||
Schedule::FixedUpdate,
|
||||
Schedule::Update,
|
||||
Schedule::PostUpdate,
|
||||
Schedule::Render,
|
||||
Schedule::Last,
|
||||
];
|
||||
|
||||
/// Drops every system contributed by `module`.
|
||||
pub(crate) fn remove_module(&mut self, module: &str) {
|
||||
for phase in Self::ALL_PHASES {
|
||||
self.phase_mut(phase).retain(|e| e.module != Some(module));
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends all of `other`'s systems (used to fold back systems registered
|
||||
/// while the frame was running).
|
||||
pub(crate) fn merge(&mut self, mut other: Systems) {
|
||||
for phase in Self::ALL_PHASES {
|
||||
let tail = std::mem::take(other.phase_mut(phase));
|
||||
self.phase_mut(phase).extend(tail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs one phase: every enabled system in registration order.
|
||||
///
|
||||
/// The [`Systems`] are moved out of the [`App`] before phases run (so systems
|
||||
/// get exclusive `&mut App` access), so `systems` and `app` here are disjoint.
|
||||
/// Systems from a disabled module are skipped without being removed.
|
||||
pub(crate) fn run_phase(systems: &mut Systems, app: &mut App, phase: Schedule) {
|
||||
for entry in systems.phase_mut(phase) {
|
||||
if app.is_system_enabled(entry.module) {
|
||||
(entry.run)(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user