Home Reference Source

src/phaser-lifecycle-plugin.js

// TODO:
// - add config option to add() to allow custom mapping to update/preupdate/postupdate
// - add priority/
// - docs
// - more tests

import Phaser from "phaser";

/**
 * Plugin that wraps around Phaser's event system. This allows us to avoid the issues around the
 * EventEmitter library Phaser uses where the EE caches the listeners at the start of an event. This
 * leads to bugs if a listener gets unsubscribed during an event (e.g. the physics system
 * unsubscribing a listener via a CB before the listener's update method). This plugin immediately
 * unsubscribes the listener.
 */
export default class LifecyclePlugin extends Phaser.Plugins.ScenePlugin {
  constructor(scene, pluginManager) {
    super(scene, pluginManager);

    this.scene = scene;
    this.systems = scene.sys;

    this.hasStarted = false;

    const camelCaseEvents = [
      "update",
      "preUpdate",
      "postUpdate",
      "render",
      "shutdown",
      "destroy",
      "start",
      "ready",
      "boot",
      "sleep",
      "wake",
      "pause",
      "resume",
      "resize",
      "transitionInit",
      "transitionStart",
      "transitionOut",
      "transitionComplete"
    ];
    this.setEventsToTrack(camelCaseEvents);

    if (!scene.sys.settings.isBooted) this.systems.events.once("boot", this.boot, this);
  }

  setEventsToTrack(camelCaseEvents) {
    // Rather than selectively unsubing & resubing, nuke all and resub later
    if (this.hasStarted) this.unsubscribeSceneEvents();

    this.eventNames = camelCaseEvents.map(s => s.toLowerCase());
    this.possibleMethodNames = new Set([...camelCaseEvents, ...this.eventNames]);

    const oldListeners = this.listeners || {};
    this.listeners = {};
    this.eventNames.forEach(name => {
      this.listeners[name] = oldListeners[name] ? oldListeners[name] : new Map();
    });

    this.eventHandlers = {};
    this.eventNames.forEach(name => {
      this.eventHandlers[name] = this.onSceneEvent.bind(this, name);
    });
    if (this.hasStarted) this.subscribeSceneEvents();
  }

  boot() {
    const emitter = this.systems.events;
    emitter.on("shutdown", this.shutdown, this);
    emitter.on("start", this.start, this);
    emitter.once("destroy", this.destroy, this);
  }

  start() {
    this.hasStarted = true;
    this.subscribeSceneEvents();
  }

  subscribeSceneEvents() {
    const emitter = this.systems.events;
    this.eventNames.forEach(name => emitter.on(name, this.eventHandlers[name]));
  }

  unsubscribeSceneEvents() {
    const emitter = this.systems.events;
    this.eventNames.forEach(name => emitter.off(name, this.eventHandlers[name]));
  }

  onSceneEvent(eventName, ...args) {
    this.listeners[eventName].forEach((method, object) => method.apply(object, args));
  }

  add(object, eventMapping) {
    // No mapping given, default to checking for methods named after the event
    if (!eventMapping) {
      eventMapping = {};
      this.possibleMethodNames.forEach(methodName => {
        if (typeof object[methodName] === "function") {
          const eventName = methodName.toLowerCase();
          eventMapping[eventName] = object[methodName];
        }
      });
    }

    Object.entries(eventMapping).forEach(([eventName, method]) => {
      const listenerMap = this.listeners[eventName];
      if (listenerMap) listenerMap.set(object, method);
    });
  }

  remove(object) {
    this.eventNames.forEach(name => this.listeners[name].delete(object));
  }

  removeAll() {
    this.eventNames.forEach(name => this.listeners[name].clear());
  }

  shutdown() {
    this.hasStarted = false;
    this.removeAll();
    this.unsubscribeSceneEvents();
  }

  destroy() {
    if (this.eventHandlers["destroy"]) this.eventHandlers["destroy"]();
    const emitter = this.systems.events;
    emitter.off("shutdown", this.onShutdown, this);
    this.unsubscribeSceneEvents();
    this.removeAll();
  }
}