Plugins
Authoring and consuming Phaser 4 plugins — the three plugin shapes (global, scene, game-object), and a worked save-system example.
A plugin is a self-contained module that hooks into Phaser’s lifecycle. Phaser 4 supports three shapes, each with different scope and lifecycle:
| Kind | Lives on | Lifecycle | Use for |
|---|---|---|---|
| Global | game.plugins | Created with the game; persists. | Cross-scene services (save, analytics, networking). |
| Scene | scene.plugins | Created per scene; tied to its lifecycle. | Per-scene helpers (an FX manager, a dialogue runner). |
| Game-object | Extends this.add / this.make | Attaches a factory to scenes that opt in. | Custom game object types (a HealthBar, a Minimap). |
Pick the smallest scope that fits. A global plugin that only one scene uses is just a leak.
Global plugin
A class extending Phaser.Plugins.BasePlugin. It’s instantiated once when the game starts and exposed via this.plugins.get('Name') from anywhere.
class SavePlugin extends Phaser.Plugins.BasePlugin {
private readonly KEY = 'save-v1';
save(state: object) {
localStorage.setItem(this.KEY, JSON.stringify(state));
}
load<T = unknown>(): T | null {
const raw = localStorage.getItem(this.KEY);
return raw ? (JSON.parse(raw) as T) : null;
}
clear() {
localStorage.removeItem(this.KEY);
}
}
Register it in the game config:
new Phaser.Game({
plugins: {
global: [{ key: 'Save', plugin: SavePlugin, start: true, mapping: 'save' }],
},
});
start: true instantiates immediately; mapping: 'save' exposes it as this.save on every scene. Without mapping, you reach for it via this.plugins.get('Save').
Use:
this.save.save({ level: 3, hp: 80 });
const restored = this.save.load<{ level: number; hp: number }>();
Scene plugin
A class extending Phaser.Plugins.ScenePlugin. Each scene gets its own instance, and the plugin can hook into the scene’s lifecycle events:
class DialogPlugin extends Phaser.Plugins.ScenePlugin {
private activeBox?: Phaser.GameObjects.Container;
boot() {
this.systems.events.on('shutdown', this.shutdown, this);
this.systems.events.on('destroy', this.destroy, this);
}
say(text: string) {
this.activeBox?.destroy();
const scene = this.scene!;
this.activeBox = scene.add.container(scene.scale.width / 2, scene.scale.height - 80);
this.activeBox.add(scene.add.rectangle(0, 0, 600, 100, 0x000000, 0.7));
this.activeBox.add(scene.add.text(0, 0, text, { fontSize: '20px' }).setOrigin(0.5));
}
private shutdown() {
this.activeBox?.destroy();
this.activeBox = undefined;
}
}
Register per scene:
plugins: {
scene: [{ key: 'Dialog', plugin: DialogPlugin, mapping: 'dialog' }],
}
Now this.dialog.say('Hello.') works from any scene that received the registration.
Game-object plugin
The third shape adds new factories to this.add and this.make. The canonical Phaser 4 pattern is a plain function registered via Phaser.GameObjects.GameObjectFactory.register:
class HealthBar extends Phaser.GameObjects.Container {
private readonly fg: Phaser.GameObjects.Rectangle;
constructor(scene: Phaser.Scene, x: number, y: number, max: number) {
super(scene, x, y);
const bg = scene.add.rectangle(0, 0, 120, 10, 0x222222);
this.fg = scene.add.rectangle(-60, 0, 120, 10, 0x44dd44).setOrigin(0, 0.5);
this.add([bg, this.fg]);
this.setData('max', max);
this.setData('value', max);
}
set(value: number) {
this.setData('value', value);
this.fg.width = 120 * (value / (this.getData('max') as number));
}
}
Phaser.GameObjects.GameObjectFactory.register('healthBar', function (x, y, max) {
const hb = new HealthBar(this.scene, x, y, max);
this.displayList.add(hb);
this.updateList.add(hb);
return hb;
});
Now this.add.healthBar(20, 20, 100) works in any scene.
For TypeScript users, augment the factory’s type:
declare global {
namespace Phaser.GameObjects {
interface GameObjectFactory {
healthBar(x: number, y: number, max: number): HealthBar;
}
}
}
Lifecycle gotchas
- Don’t reach into
this.scenebeforeboot. Scene plugins are created early;bootis whenthis.sceneis set. - Clean up subscriptions in
shutdown. Scenes can be started and stopped repeatedly; plugins that bind to scene events without unbinding will leak. - Global plugin singletons. A global plugin is a singleton — store per-scene state inside scenes, not on the plugin.
Packaging
For sharing across projects, ship as an ESM module with the plugin class as the default export plus the registration call (or a register(game) helper if you want consumers to do it explicitly). Keep zero Phaser dependencies in the package’s runtime imports — Phaser is the consumer’s; declare it as a peerDependency.
Related
- Scenes — where scene plugins live.
- Game Objects — what game-object plugins extend.