Apps and Plugins
App owns everything: the ECS world, resources, the registered systems, and the runner that drives the main loop. You build one by chaining calls, then hand it off:
use pebble::prelude::*;
fn main() {
App::new()
.add_plugin(MyWindowPlugin)
.add_plugin(MyBackendPlugin)
.add_plugin(MyGamePlugin)
.build()
.run();
}
Plugins are the unit of composition
A Plugin is anything implementing one method:
pub trait Plugin {
fn build(&self, app: &mut App);
}
That’s the entire extension point. Windowing, the graphics backend, every asset type, and your own game-specific setup are all just plugins. build receives &mut App and can add resources, register systems, or queue further plugins — plugins can register other plugins, and App::build() keeps draining the queue (up to a hard limit of 64 passes, to catch an accidental registration cycle) until nothing new shows up.
Here’s a minimal one, from the orbit_camera example:
struct TimePlugin;
impl Plugin for TimePlugin {
fn build(&self, app: &mut App) {
app.add_resource(Time {
time: Instant::now(),
last_time: Instant::now(),
delta_time: 0.0,
})
.add_system(SystemStage::PreUpdate, update_delta_time);
}
}
Nothing here is special-cased by the framework — TimePlugin uses exactly the two calls (add_resource, add_system) available to any other code with a &mut App. Writing your own plugins is how you organize a real project: one plugin per subsystem (input, camera, physics, UI), each self-contained, added to App in main in whatever order makes sense.
What build() actually does
Three things, in order:
- Runs every queued plugin’s
build, repeating until no plugin queues another. - Validates resource requirements. Every system’s declared dependencies (see Resources) are checked against what plugins have declared they’ll eventually provide. A system requiring a resource that nothing will ever insert is a near-certain bug —
build()panics immediately, naming every offending system and resource, rather than letting it surface as a runtime panic several ticks intorun(). - Settles the asset pipeline as far as it can go synchronously (see The Asset Pipeline and Handles) — so resources that don’t need to wait for anything asynchronous (a GPU backend arriving on another thread, say) are ready the moment
build()returns, not one tick later.
run() is deliberately thin: it hands self to whatever runner is currently installed and does nothing else. The default runner just loops app.update() forever; a window plugin normally replaces it with one that also pumps the OS event loop (see Windows and Backends).
There is no “Startup” stage
Pebble has a Startup stage for one-time initialization. Anything that should run once anywhere else is an ordinary system that returns Option<()>, covered in Systems and Stages — Some(()) retires the system permanently, None retries next tick.