Files
Oxide/docs/modules.md
T
Homer Simpson 9eead719b0 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>
2026-07-05 20:41:02 +02:00

5.5 KiB

App, Modules & Scheduling

oxide_engine::app is where the Stage-5 core framework comes together. An App owns the shared engine state and a Schedule of systems; functionality is added by modules. This is the spine the rest of the engine plugs into: the engine is composed rather than hard-wired, and an exported game compiles in only the modules it registers.

The App

An App owns:

use oxide_engine::app::{App, DefaultModules};

let mut app = App::new();
app.add_modules(DefaultModules);
app.update(1.0 / 60.0); // advance one frame

Note: the application core is oxide_engine::app::App. It is intentionally not in the prelude, to avoid clashing with the windowing App trait (the per-window event handler). Import it directly.

Resources

Resources are shared singletons addressed by type — the home for state that isn't per-entity (an input map, a physics world, game settings):

# use oxide_engine::app::App;
# let mut app = App::new();
app.insert_resource(0u32);
*app.get_resource_mut::<u32>().unwrap() += 1;
assert_eq!(app.get_resource::<u32>(), Some(&1));

Systems and the schedule

A system is any FnMut(&mut App) attached to a Schedule phase. Phases run in a fixed order each frame; within a phase, systems run in registration order, so behavior is fully deterministic.

Phase Purpose
First start-of-frame bookkeeping
Input gather input (Stage 7)
PreUpdate engine work before game logic
FixedUpdate fixed-timestep work; runs 0..n times per frame (physics, Stage 9)
Update per-frame game logic
PostUpdate engine work after game logic
Render drawing (Stage 5 pipeline onward)
Last end-of-frame cleanup
use oxide_engine::app::{App, Schedule};
use oxide_engine::prelude::*;

let mut app = App::new();
app.scene.spawn("spinner", Transform::IDENTITY);
app.add_system(Schedule::Update, |app| {
    let dt = app.time.delta;
    for e in app.scene.entities().collect::<Vec<_>>() {
        if let Some(mut t) = app.scene.get_mut::<Transform>(e) {
            t.translation.x += dt;
        }
    }
});

Systems get exclusive &mut App while running (the schedule is moved out of the app for the duration), so a system can freely read and mutate the scene, resources, and assets.

Fixed timestep

FixedUpdate is driven by an accumulator so simulation is frame-rate independent: each update(dt) runs as many whole fixed_delta steps as the accumulated time allows. The number of steps per frame is capped so a long stall (a breakpoint, a hitch) cannot trigger an unbounded catch-up "spiral of death". Set the rate with app.set_fixed_timestep(seconds) (default 1/60).

The whole frame's scheduling overhead is a few dozen nanoseconds even with several systems registered (see cargo bench -p oxide-engine --bench app), so it is lost in the noise next to any real per-frame work.

Modules

A Module is the unit of engine extension. Its build method registers systems, component types, asset loaders, and resources through the App facade. Everything registered during build is attributed to the module, so it can be enabled, disabled, or removed as one unit.

use oxide_engine::app::{App, Module, Schedule};

struct HeartbeatModule;
impl Module for HeartbeatModule {
    fn name(&self) -> &'static str { "heartbeat" }
    fn build(&self, app: &mut App) {
        app.insert_resource(0u64);
        app.add_system(Schedule::Update, |app| {
            *app.get_resource_mut::<u64>().unwrap() += 1;
        });
    }
}

let mut app = App::new();
app.add_module(HeartbeatModule);

Enable / disable / remove

These power the editor's module management (Stage 6) and the "ship only what you use" principle:

# use oxide_engine::app::{App, Module, Schedule};
# struct HeartbeatModule;
# impl Module for HeartbeatModule {
#     fn name(&self) -> &'static str { "heartbeat" }
#     fn build(&self, app: &mut App) {}
# }
# let mut app = App::new();
# app.add_module(HeartbeatModule);
app.set_module_enabled("heartbeat", false); // systems skipped, nothing removed
app.set_module_enabled("heartbeat", true);  // resumes
app.remove_module("heartbeat");             // systems, types, loaders, resources gone

remove_module undoes every registration the module made — its systems, reflected types, asset loaders, and resources — leaving no dangling references. Disabling is the cheap, reversible version (systems are skipped but kept).

Built-in modules

DefaultModules bundles the engine's standard set:

  • core — registers the always-present scene component types (Transform, Node, Layer, Tags) for reflection, exposing them to the editor and scripts.
  • render — registers the renderable MeshRenderer component and (as the data-driven render pipeline grows) the render-phase systems.

Subsystems from Stage 9 on (physics, audio, …) are built as their own feature-gated crates, each exposing a Module, so a project pays for them only by registering them.