~2 min read

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:

KindLives onLifecycleUse for
Globalgame.pluginsCreated with the game; persists.Cross-scene services (save, analytics, networking).
Scenescene.pluginsCreated per scene; tied to its lifecycle.Per-scene helpers (an FX manager, a dialogue runner).
Game-objectExtends this.add / this.makeAttaches 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.scene before boot. Scene plugins are created early; boot is when this.scene is 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.