The App and the Plugin System
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 (covered next chapter) 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 (covered in 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 Opening a Window).
There is no “Startup” stage
Frameworks with a fixed set of lifecycle stages usually have a dedicated Startup one. Pebble doesn’t — anything that should run once is an ordinary system wrapped in .once(), covered in the next chapter. This keeps the mental model to one thing (“systems run on stages, every tick”) instead of two (“systems run on stages, except the ones that only run at the start, which are different”).