Skip to content

Writing a Plugin

Arnel Robles edited this page Oct 2, 2026 · 2 revisions

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 init adds (listeners, timers, DOM nodes, systems) is removed in destroy.
  • 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.

Clone this wiki locally