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

161 lines
5.5 KiB
Markdown

# 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:
- the active [`Scene`](scene.md),
- the shared [`AssetServer`](assets.md),
- the [`TypeRegistry`](reflection.md) (dual-editable components),
- the project's [`LayerRegistry`](layers.md),
- frame [`Time`], and
- arbitrary user **resources** (a type-keyed store).
```rust
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`](windowing.md) 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):
```rust
# 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 |
```rust
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`](Module::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.
```rust
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:
```rust
# 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.
[`App`]: ../engine/src/app/mod.rs
[`Time`]: ../engine/src/app/mod.rs
[`Schedule`]: ../engine/src/app/schedule.rs
[`Module`]: ../engine/src/app/module.rs
[`Scene`]: ../engine/src/scene/graph.rs