Writing a Plugin - BaryoDev/dopaminejs GitHub Wiki
Writing a Plugin
A plugin is an object with a name and an init(kernel). It usually registers a system, listens to events, or both.
export const ComboPlugin = {
name: 'combo',
version: '1.0.0',
init(kernel) {
this.kernel = kernel;
kernel.systems.register('combo', new ComboSystem(), { priority: 40 });
},
destroy() {
this.kernel.systems.unregister('combo');
this.kernel = null;
}
};
import { Game } from 'dopaminejs';
const game = new Game();
game.kernel.plugins.use(ComboPlugin);
game.kernel.systems.get('combo');
The plugin registry
kernel.plugins:
| Method | Notes |
|---|---|
use(plugin) |
Throws if name or init is missing. A name already loaded is skipped with a warning. |
useAsync(plugin) |
Same, and awaits init. |
remove(name) |
Calls plugin.destroy(), unregisters the systems the plugin registered in init, and emits plugin_unloaded. |
get(name), has(name) |
Lookup |
getPluginNames(), getLoadOrder() |
Lists |
clear() |
Removes every plugin, last loaded first |
If init throws, the systems it had registered are unregistered and the error is rethrown. The plugin is not left half loaded.
Since 2.4.0 remove unregisters the systems a plugin registered during init. A system that something else has replaced since is left alone. Two cases are not tracked: a system registered after init returned, and one registered while two useAsync inits overlapped. Unregister in destroy anyway, as in the example above. It covers both cases and older cores, and unregistering twice is harmless.
Systems
A system is any object. All four methods are optional.
class ComboSystem {
init(kernel) { // on register
this.kernel = kernel;
this.count = 0;
}
update(dt) {} // every frame
fixedUpdate(dt) {} // every fixed step
destroy() {} // on unregister
}
kernel.systems:
| Method | Notes |
|---|---|
register(name, system, { priority = 0, dependencies = [] }) |
Calls init(kernel), emits system_registered |
unregister(name) |
Calls destroy(), emits system_unregistered |
get(name), has(name), getSystemNames() |
Lookup |
Update order: a system's dependencies run before it. Otherwise a higher priority runs earlier. The built-in systems use 100 down to 60 (see Engine).
Registering a name that exists replaces the old system, destroys it, and logs a warning. That is how a plugin swaps a built-in system.
A system whose update throws is logged once and the other systems keep running.
Listening to events
init(kernel) {
this.kernel = kernel;
this._onLevelUp = ({ newLevel }) => this.celebrate(newLevel);
kernel.events.on('level_up', this._onLevelUp);
},
destroy() {
this.kernel.events.off('level_up', this._onLevelUp);
}
kernel.events.on returns the bus, not an unsubscribe function. Keep the callback and pass it to off.
Reward events reach the bus only when the RewardSystem was given events or kernel in its config. Say so in your plugin's README. See Events.
Rules that keep a plugin safe to remove
- Everything
initadds (listeners, timers, DOM nodes, systems) is removed indestroy. - Look up caller-supplied keys with an own-property check, never
obj[key]alone. - A plugin object is usually a module-level singleton. Do not keep per-game state on it past
destroy.
Async setup
export const AssetsPlugin = {
name: 'assets',
async init(kernel) {
await kernel.loader.loadImage('hero', '/img/hero.png');
}
};
await game.kernel.plugins.useAsync(AssetsPlugin);
Packaging
Declare dopaminejs as a peer dependency, not a dependency, so the app and the plugin share one copy.
{
"peerDependencies": { "dopaminejs": "^2.3.0" }
}
A worked example of a system swap is in the repo at examples/plugins/CustomPhysicsPlugin.js.