Plugins & options
How to load and manage engine plugins at runtime, and how to define and persist hierarchical reactive options with auto-generated UI.
Meep extends the core engine through two parallel systems: plugins, which add services to a running engine and declare their own dependencies, and options, a hierarchical reactive settings tree that can persist to storage and render into a built-in UI panel.
Plugins
What a plugin is
An EnginePlugin is a lifecycle object managed by engine.plugins
(EnginePluginManager). Each plugin has three steady states — initialized,
running, and finalized — and an async transition between each:
| Method | Called when |
|---|---|
initialize(engine) | plugin first acquired; receives the Engine reference |
startup() | engine starts (or immediately, if the engine is already running) |
shutdown() | last reference to the plugin is released |
Plugins can declare dependencies (an array of other plugin classes). The
manager initializes dependencies first and shuts them down last, so a plugin
can safely call engine.plugins.getPlugin(OtherPlugin) in its own startup.
import { EnginePlugin } from "@woosh/meep-engine/src/engine/plugin/EnginePlugin.js";
class AudioPlugin extends EnginePlugin {
constructor() {
super();
this.id = "audio";
}
get dependencies() {
return []; // list other EnginePlugin classes here
}
async startup() {
await super.startup();
// set up services on this.engine
}
async shutdown() {
await super.shutdown();
}
}
Acquiring a plugin
engine.plugins.acquire(PluginClass) is the only way to obtain a plugin
instance. It returns a Promise<Reference<T>>:
const ref = await engine.plugins.acquire(AudioPlugin);
const plugin = ref.getValue(); // the AudioPlugin instance
// when you no longer need it:
ref.release();
The manager is reference-counted. The first call to acquire instantiates the
plugin and brings it to the running state. Subsequent calls with the same class
increment the reference count and return a new Reference to the same
instance. When the count reaches zero, the plugin is shut down and removed.
acquireMany(classes) is the parallel form — it calls acquire for each
class and resolves when all transitions are complete.
Registering plugins at configuration time
The more common pattern is to declare plugins in the EngineConfiguration
callback passed to EngineHarness.bootstrap. That runs acquire for each
declared plugin before the engine starts:
import { EngineHarness } from "@woosh/meep-engine/src/engine/EngineHarness.js";
import { AmbientOcclusionPostProcessEffect }
from "@woosh/meep-engine/src/engine/graphics/ecs/fx/ao/AmbientOcclusionPostProcessEffect.js";
const engine = await EngineHarness.bootstrap({
configuration: (config, engine) => {
config.addPlugin(AmbientOcclusionPostProcessEffect);
}
});
Locating an active plugin
// by class (returns the instance or undefined)
const ao = engine.plugins.getPlugin(AmbientOcclusionPostProcessEffect);
// by string id
const audio = engine.plugins.getById("audio");
Options
The tree
engine.options is an OptionGroup — the root of the settings tree. The tree
is a hierarchy of groups (branches) and leaf Option nodes. Each node has a
string id; the path from the root uniquely identifies it.
import { OptionGroup } from "@woosh/meep-engine/src/engine/options/OptionGroup.js";
// engine.options is already an OptionGroup at engine startup.
// Add a sub-group and two options:
const graphics = engine.options.addGroup("graphics");
graphics.add(
"shadowQuality",
() => shadowQuality, // read
(v) => { shadowQuality = v; }, // write
{ values: ["low", "medium", "high"] }
);
graphics.add(
"fov",
() => camera.fov,
(v) => { camera.fov = v; },
{ min: 30, max: 120 }
);
addGroup(id) returns the new OptionGroup so you can chain child additions.
add(id, read, write, settings) attaches a leaf Option and also returns the
group, so further .add(…) calls can be chained on the same group.
Option settings
The settings object on a leaf option controls both persistence and optional
UI rendering:
| Setting key | Type | Effect |
|---|---|---|
transient | boolean | true — excluded from toJSON / fromJSON serialization |
min | number | Lower bound hint for the auto-generated slider |
max | number | Upper bound hint |
values | string[] / number[] | Allowed discrete values — renders as a dropdown |
Reactive writes
Every Option exposes an on.written signal that fires after the value is
successfully written, and an on.writeFailed signal that fires if the write
throws or rejects:
const fov = engine.options.resolve(["graphics", "fov"]);
fov.on.written.add((v) => console.log("FOV changed to", v));
fov.write(75); // fires on.written with 75
resolve(path) walks the tree by segment array and returns the matching group
or option, throwing if any segment is not found.
Persistence
OptionGroup.attachToStorage(key, storage) wires the whole subtree to a
storage backend (the engine’s engine.storage). On attach it loads and
deserializes any previously stored values; from that point every subsequent
write serializes the tree back to storage automatically:
// The engine does this internally for its own options:
await engine.options.attachToStorage("myapp.options", engine.storage);
Transient options are excluded. toJSON serializes the tree to a plain object;
fromJSON restores it without triggering on.written.
Auto-generated UI
OptionsView renders any OptionGroup into a
dat.GUI panel. It needs the options
tree and a Localization instance (option labels come from localization keys
of the form system_option.<path>, e.g. system_option.graphics.fov):
import OptionsView from "@woosh/meep-engine/src/engine/options/OptionsView.js";
const view = new OptionsView({
options: engine.options,
localization: engine.localization,
// inclusions: [["graphics"]] // optional: only show a sub-tree
});
document.body.appendChild(view.el);
view.link();
Pass inclusions as a list of path arrays to limit which branches appear.
When omitted, every non-transient option in the tree is shown. The panel
updates immediately when any option is written externally, and localizes its
own labels whenever the locale changes.
Traversing options in code
engine.options.traverseOptions((option) => {
console.log(option.computePath().join("."), "=", option.read());
});
computePath() returns the segment array from the root to the node.