A Phaser 3 plugin to make it easier to have custom objects hook into Phaser's lifecycle events - preupdate, postupdate, etc.
Note: this plugin is still in progress. It's something we've been using internally and will update to an official release with docs soon!
class CustomPlayer {
update() {
console.log("Update!");
}
preUpdate() {
console.log("Before update!");
}
postUpdate() {
console.log("After update!");
}
}
const player = new CustomPlayer();
// Hook the player's update, preUpdate and postUpdate up to Scene events
this.lifecycle.add(player);
// ...
// Some time later, you can unsubscribe:
this.lifecycle.remove(player);
Check out the HTML documentation here.
Two main reasons:
- This reduces boilerplate for creating custom game objects and components that don't subclass Phaser's game objects. TODO: demo the component pattern.
- The plugin wraps around the event system (which is based on EventEmitter3) and proxies the events, fixing a common problem that can arise with EventEmitter3. Emitters cache their listeners at the start of an event, which can lead to unsubscribed listeners still being invoked one more time post-unsubscribing. With this plugin, any objects that are removed are removed immediately - no extra events.
You can install this plugin globally as a script, or locally as a module using your bundler of choice.
You can drop in any of the transpiled code into your project as a standalone script. Choose the version that you want:
- minified code & optional source map
- unminified code & optional source map
E.g. if you wanted the minified code, you would add this to your HTML:
<script src="phaser-lifecycle-plugin.min.js"></script>
Or use the jsdelivr CDN:
<script src="//cdn.jsdelivr.net/npm/phaser-lifecycle-plugin"></script>
Now you can use the global PhaserLifecyclePlugin
. See usage for how to use the plugin.
Install via npm:
npm install --save phaser-lifecycle-plugin
To use the transpiled and minified distribution of the library:
import PhaserLifecyclePlugin from "phaser-lifecycle-plugin";
To use the raw library (so you can transpile it to match your own project settings):
import PhaserLifecyclePlugin from "phaser-lifecycle-plugin/src";
See usage for how to use the plugin.
When setting up your game config, add the plugin:
const config = {
// ...
// Install the scene plugin
plugins: {
scene: [
{
plugin: PhaserLifecyclePlugin, // The plugin class
key: "lifecycle", // Where to store in Scene.Systems, e.g. scene.sys.lifecycle
mapping: "lifecycle" // Where to store in the Scene, e.g. scene.lifecycle
}
]
}
};
const game = new Phaser.Game(config);
Now, within a scene, you can use this.lifecycle
to access the plugin instance.
Within a scene, you can now:
class CustomPlayer {
update() {
console.log("Update!");
}
preUpdate() {
console.log("Before update!");
}
postUpdate() {
console.log("After update!");
}
}
const player = new CustomPlayer();
this.lifecycle.add(player);
And the player's update
, preUpdate
and postUpdate
methods will be invoked in sync with the scene events. Running this.lifecycle.remove(player)
will stop those methods from being invoked.
If you don't pass in a second parameter to LifeCyclePlugin#add(...)
, it will check the given object for any of the following methods (which correspond to scene events): update
, preUpdate
, postUpdate
, render
, shutdown
, destroy
, start
, ready
, boot
, sleep
, wake
, pause
, resume
, resize
, transitionInit
, transitionStart
, transitionOut
and transitionComplete
. If they are found, they are automatically subscribed to the corresponding scene event. The plugin will look for lowercase names like postupdate
as well as camelCase like postUpdate
. If you don't care about the whole suite of scene events, you can use setEventsToTrack
and pass in an array of the scene events that you care about, e.g. this.lifecycle.setEventsToTrack(["update", "postUpdate"])
.
Alternatively, you can specify a custom mapping of Scene event name to method name when adding an object to the plugin:
class CustomPlayer {
draw() {
console.log("Alias for render");
}
kill() {
console.log("Alias for destroy!");
}
}
const player = new CustomPlayer();
this.lifecycle.add(player, {
render: object.draw,
destroy: object.kill
});
TODO: better example with custom mapping & showing how each method hook is optional.
The project is controlled by npm scripts and uses cypress & jest for testing. Cypress is used for end-to-end verification that the plugin works as expected with Phaser. Jest is used for unit testing the plugin (via heavy mocking since Phaser headless mode is not complete).
- The
watch
andbuild
tasks will build the plugin source in library/ or the projects in end-to-end-tests/ - The
serve
task opens the whole project (starting at the root) in a server - The
dev
task will build & watch the library, tests and open up the server. This is useful for creating tests and updating the library. - The
dev:cypress
task will build & watch the library & tests, as well as open up cypress in headed mode. This is useful for checking out individual tests and debugging them. - The
test:cypress
task will build the tests and run cypress in headless mode to check all end-to-end tests. - The
test:jest
will run the jest tests.
The cypress tests rely on a particular structure:
- Each test game inside of "end-to-end-tests/" should have an "index.html" file as the entry point. "src/js/index.js" will be compiled to "build/js/index.js" by webpack. (Cypress doesn't support
type="module"
on scripts, so this is necessary if we need modules.) - Each test has access to
test-utils.js
which providesstartTest
,passTest
andfailTest
methods. CallstartTest
at the beginning and pass/fail when the test passes/fails. This manipulates in the DOM in a way that cypress is expecting. - Each test in "cypress/integration/" simply loads up the specified URL and waits for it to pass or timeout. (Technically, startTest and failTest are ignored, but they are useful for visual inspection of a test.)
The jest unit tests rely on a simple mocking of Phaser. They are stored inside "src/". Once Phaser headless is available, this testing structure could be re-evaluated.
samme's nice phaser-plugin-update is similar, but just focused on update, whereas our use case required more of Phaser's life cycle hooks.