phaser/src/plugins/PluginManager.js
2022-02-28 14:29:51 +00:00

901 lines
28 KiB
JavaScript

/**
* @author Richard Davey <rich@photonstorm.com>
* @copyright 2022 Photon Storm Ltd.
* @license {@link https://opensource.org/licenses/MIT|MIT License}
*/
var Class = require('../utils/Class');
var GameEvents = require('../core/events');
var EventEmitter = require('eventemitter3');
var FileTypesManager = require('../loader/FileTypesManager');
var GameObjectCreator = require('../gameobjects/GameObjectCreator');
var GameObjectFactory = require('../gameobjects/GameObjectFactory');
var GetFastValue = require('../utils/object/GetFastValue');
var PluginCache = require('./PluginCache');
var Remove = require('../utils/array/Remove');
/**
* @classdesc
* The PluginManager is responsible for installing and adding plugins to Phaser.
*
* It is a global system and therefore belongs to the Game instance, not a specific Scene.
*
* It works in conjunction with the PluginCache. Core internal plugins automatically register themselves
* with the Cache, but it's the Plugin Manager that is responsible for injecting them into the Scenes.
*
* There are two types of plugin:
*
* 1. A Global Plugin
* 2. A Scene Plugin
*
* A Global Plugin is a plugin that lives within the Plugin Manager rather than a Scene. You can get
* access to it by calling `PluginManager.get` and providing a key. Any Scene that requests a plugin in
* this way will all get access to the same plugin instance, allowing you to use a single plugin across
* multiple Scenes.
*
* A Scene Plugin is a plugin dedicated to running within a Scene. These are different to Global Plugins
* in that their instances do not live within the Plugin Manager, but within the Scene Systems class instead.
* And that every Scene created is given its own unique instance of a Scene Plugin. Examples of core Scene
* Plugins include the Input Plugin, the Tween Plugin and the physics Plugins.
*
* You can add a plugin to Phaser in three different ways:
*
* 1. Preload it
* 2. Include it in your source code and install it via the Game Config
* 3. Include it in your source code and install it within a Scene
*
* For examples of all of these approaches please see the Phaser 3 Examples Repo `plugins` folder.
*
* For information on creating your own plugin please see the Phaser 3 Plugin Template.
*
* @class PluginManager
* @memberof Phaser.Plugins
* @constructor
* @since 3.0.0
*
* @param {Phaser.Game} game - The game instance that owns this Plugin Manager.
*/
var PluginManager = new Class({
Extends: EventEmitter,
initialize:
function PluginManager (game)
{
EventEmitter.call(this);
/**
* The game instance that owns this Plugin Manager.
*
* @name Phaser.Plugins.PluginManager#game
* @type {Phaser.Game}
* @since 3.0.0
*/
this.game = game;
/**
* The global plugins currently running and managed by this Plugin Manager.
* A plugin must have been started at least once in order to appear in this list.
*
* @name Phaser.Plugins.PluginManager#plugins
* @type {Phaser.Types.Plugins.GlobalPlugin[]}
* @since 3.8.0
*/
this.plugins = [];
/**
* A list of plugin keys that should be installed into Scenes as well as the Core Plugins.
*
* @name Phaser.Plugins.PluginManager#scenePlugins
* @type {string[]}
* @since 3.8.0
*/
this.scenePlugins = [];
/**
* A temporary list of plugins to install when the game has booted.
*
* @name Phaser.Plugins.PluginManager#_pendingGlobal
* @private
* @type {array}
* @since 3.8.0
*/
this._pendingGlobal = [];
/**
* A temporary list of scene plugins to install when the game has booted.
*
* @name Phaser.Plugins.PluginManager#_pendingScene
* @private
* @type {array}
* @since 3.8.0
*/
this._pendingScene = [];
if (game.isBooted)
{
this.boot();
}
else
{
game.events.once(GameEvents.BOOT, this.boot, this);
}
},
/**
* Run once the game has booted and installs all of the plugins configured in the Game Config.
*
* @method Phaser.Plugins.PluginManager#boot
* @protected
* @since 3.0.0
*/
boot: function ()
{
var i;
var entry;
var key;
var plugin;
var start;
var mapping;
var data;
var config = this.game.config;
// Any plugins to install?
var list = config.installGlobalPlugins;
// Any plugins added outside of the game config, but before the game booted?
list = list.concat(this._pendingGlobal);
for (i = 0; i < list.length; i++)
{
entry = list[i];
// { key: 'TestPlugin', plugin: TestPlugin, start: true, mapping: 'test', data: { msg: 'The plugin is alive' } }
key = GetFastValue(entry, 'key', null);
plugin = GetFastValue(entry, 'plugin', null);
start = GetFastValue(entry, 'start', false);
mapping = GetFastValue(entry, 'mapping', null);
data = GetFastValue(entry, 'data', null);
if (key)
{
if (plugin)
{
this.install(key, plugin, start, mapping, data);
}
else
{
console.warn('Missing `plugin` for key: ' + key);
}
}
}
// Any scene plugins to install?
list = config.installScenePlugins;
// Any plugins added outside of the game config, but before the game booted?
list = list.concat(this._pendingScene);
for (i = 0; i < list.length; i++)
{
entry = list[i];
// { key: 'moveSpritePlugin', plugin: MoveSpritePlugin, , mapping: 'move' }
key = GetFastValue(entry, 'key', null);
plugin = GetFastValue(entry, 'plugin', null);
mapping = GetFastValue(entry, 'mapping', null);
if (key)
{
if (plugin)
{
this.installScenePlugin(key, plugin, mapping);
}
else
{
console.warn('Missing `plugin` for key: ' + key);
}
}
}
this._pendingGlobal = [];
this._pendingScene = [];
this.game.events.once(GameEvents.DESTROY, this.destroy, this);
},
/**
* Called by the Scene Systems class. Tells the plugin manager to install all Scene plugins into it.
*
* First it will install global references, i.e. references from the Game systems into the Scene Systems (and Scene if mapped.)
* Then it will install Core Scene Plugins followed by Scene Plugins registered with the PluginManager.
* Finally it will install any references to Global Plugins that have a Scene mapping property into the Scene itself.
*
* @method Phaser.Plugins.PluginManager#addToScene
* @protected
* @since 3.8.0
*
* @param {Phaser.Scenes.Systems} sys - The Scene Systems class to install all the plugins in to.
* @param {array} globalPlugins - An array of global plugins to install.
* @param {array} scenePlugins - An array of scene plugins to install.
*/
addToScene: function (sys, globalPlugins, scenePlugins)
{
var i;
var pluginKey;
var pluginList;
var game = this.game;
var scene = sys.scene;
var map = sys.settings.map;
var isBooted = sys.settings.isBooted;
// Reference the GlobalPlugins from Game into Scene.Systems
for (i = 0; i < globalPlugins.length; i++)
{
pluginKey = globalPlugins[i];
if (game[pluginKey])
{
sys[pluginKey] = game[pluginKey];
// Scene level injection
if (map.hasOwnProperty(pluginKey))
{
scene[map[pluginKey]] = sys[pluginKey];
}
}
else if (pluginKey === 'game' && map.hasOwnProperty(pluginKey))
{
scene[map[pluginKey]] = game;
}
}
for (var s = 0; s < scenePlugins.length; s++)
{
pluginList = scenePlugins[s];
for (i = 0; i < pluginList.length; i++)
{
pluginKey = pluginList[i];
if (!PluginCache.hasCore(pluginKey))
{
continue;
}
var source = PluginCache.getCore(pluginKey);
var mapKey = source.mapping;
var plugin = new source.plugin(scene, this, mapKey);
sys[mapKey] = plugin;
// Scene level injection
if (source.custom)
{
scene[mapKey] = plugin;
}
else if (map.hasOwnProperty(mapKey))
{
scene[map[mapKey]] = plugin;
}
// Scene is already booted, usually because this method is being called at run-time, so boot the plugin
if (isBooted)
{
plugin.boot();
}
}
}
// And finally, inject any 'global scene plugins'
pluginList = this.plugins;
for (i = 0; i < pluginList.length; i++)
{
var entry = pluginList[i];
if (entry.mapping)
{
scene[entry.mapping] = entry.plugin;
}
}
},
/**
* Called by the Scene Systems class. Returns a list of plugins to be installed.
*
* @method Phaser.Plugins.PluginManager#getDefaultScenePlugins
* @protected
* @since 3.8.0
*
* @return {string[]} A list keys of all the Scene Plugins to install.
*/
getDefaultScenePlugins: function ()
{
var list = this.game.config.defaultPlugins;
// Merge in custom Scene plugins
list = list.concat(this.scenePlugins);
return list;
},
/**
* Installs a new Scene Plugin into the Plugin Manager and optionally adds it
* to the given Scene as well. A Scene Plugin added to the manager in this way
* will be automatically installed into all new Scenes using the key and mapping given.
*
* The `key` property is what the plugin is injected into Scene.Systems as.
* The `mapping` property is optional, and if specified is what the plugin is installed into
* the Scene as. For example:
*
* ```javascript
* this.plugins.installScenePlugin('powerupsPlugin', pluginCode, 'powerups');
*
* // and from within the scene:
* this.sys.powerupsPlugin; // key value
* this.powerups; // mapping value
* ```
*
* This method is called automatically by Phaser if you install your plugins using either the
* Game Configuration object, or by preloading them via the Loader.
*
* @method Phaser.Plugins.PluginManager#installScenePlugin
* @since 3.8.0
*
* @param {string} key - The property key that will be used to add this plugin to Scene.Systems.
* @param {function} plugin - The plugin code. This should be the non-instantiated version.
* @param {string} [mapping] - If this plugin is injected into the Phaser.Scene class, this is the property key to use.
* @param {Phaser.Scene} [addToScene] - Optionally automatically add this plugin to the given Scene.
* @param {boolean} [fromLoader=false] - Is this being called by the Loader?
*/
installScenePlugin: function (key, plugin, mapping, addToScene, fromLoader)
{
if (fromLoader === undefined) { fromLoader = false; }
if (typeof plugin !== 'function')
{
console.warn('Invalid Scene Plugin: ' + key);
return;
}
if (!PluginCache.hasCore(key))
{
// Plugin is freshly loaded
PluginCache.register(key, plugin, mapping, true);
}
if (this.scenePlugins.indexOf(key) === -1)
{
this.scenePlugins.push(key);
}
else if (!fromLoader && PluginCache.hasCore(key))
{
// Plugin wasn't from the loader but already exists
console.warn('Scene Plugin key in use: ' + key);
return;
}
if (addToScene)
{
var instance = new plugin(addToScene, this, key);
addToScene.sys[key] = instance;
if (mapping && mapping !== '')
{
addToScene[mapping] = instance;
}
instance.boot();
}
},
/**
* Installs a new Global Plugin into the Plugin Manager and optionally starts it running.
* A global plugin belongs to the Plugin Manager, rather than a specific Scene, and can be accessed
* and used by all Scenes in your game.
*
* The `key` property is what you use to access this plugin from the Plugin Manager.
*
* ```javascript
* this.plugins.install('powerupsPlugin', pluginCode);
*
* // and from within the scene:
* this.plugins.get('powerupsPlugin');
* ```
*
* This method is called automatically by Phaser if you install your plugins using either the
* Game Configuration object, or by preloading them via the Loader.
*
* The same plugin can be installed multiple times into the Plugin Manager by simply giving each
* instance its own unique key.
*
* @method Phaser.Plugins.PluginManager#install
* @since 3.8.0
*
* @param {string} key - The unique handle given to this plugin within the Plugin Manager.
* @param {function} plugin - The plugin code. This should be the non-instantiated version.
* @param {boolean} [start=false] - Automatically start the plugin running? This is always `true` if you provide a mapping value.
* @param {string} [mapping] - If this plugin is injected into the Phaser.Scene class, this is the property key to use.
* @param {any} [data] - A value passed to the plugin's `init` method.
*
* @return {?Phaser.Plugins.BasePlugin} The plugin that was started, or `null` if `start` was false, or game isn't yet booted.
*/
install: function (key, plugin, start, mapping, data)
{
if (start === undefined) { start = false; }
if (mapping === undefined) { mapping = null; }
if (data === undefined) { data = null; }
if (typeof plugin !== 'function')
{
console.warn('Invalid Plugin: ' + key);
return null;
}
if (PluginCache.hasCustom(key))
{
console.warn('Plugin key in use: ' + key);
return null;
}
if (mapping !== null)
{
start = true;
}
if (!this.game.isBooted)
{
this._pendingGlobal.push({ key: key, plugin: plugin, start: start, mapping: mapping, data: data });
}
else
{
// Add it to the plugin store
PluginCache.registerCustom(key, plugin, mapping, data);
if (start)
{
return this.start(key);
}
}
return null;
},
/**
* Gets an index of a global plugin based on the given key.
*
* @method Phaser.Plugins.PluginManager#getIndex
* @protected
* @since 3.8.0
*
* @param {string} key - The unique plugin key.
*
* @return {number} The index of the plugin within the plugins array.
*/
getIndex: function (key)
{
var list = this.plugins;
for (var i = 0; i < list.length; i++)
{
var entry = list[i];
if (entry.key === key)
{
return i;
}
}
return -1;
},
/**
* Gets a global plugin based on the given key.
*
* @method Phaser.Plugins.PluginManager#getEntry
* @protected
* @since 3.8.0
*
* @param {string} key - The unique plugin key.
*
* @return {Phaser.Types.Plugins.GlobalPlugin} The plugin entry.
*/
getEntry: function (key)
{
var idx = this.getIndex(key);
if (idx !== -1)
{
return this.plugins[idx];
}
},
/**
* Checks if the given global plugin, based on its key, is active or not.
*
* @method Phaser.Plugins.PluginManager#isActive
* @since 3.8.0
*
* @param {string} key - The unique plugin key.
*
* @return {boolean} `true` if the plugin is active, otherwise `false`.
*/
isActive: function (key)
{
var entry = this.getEntry(key);
return (entry && entry.active);
},
/**
* Starts a global plugin running.
*
* If the plugin was previously active then calling `start` will reset it to an active state and then
* call its `start` method.
*
* If the plugin has never been run before a new instance of it will be created within the Plugin Manager,
* its active state set and then both of its `init` and `start` methods called, in that order.
*
* If the plugin is already running under the given key then nothing happens.
*
* @method Phaser.Plugins.PluginManager#start
* @since 3.8.0
*
* @param {string} key - The key of the plugin to start.
* @param {string} [runAs] - Run the plugin under a new key. This allows you to run one plugin multiple times.
*
* @return {?Phaser.Plugins.BasePlugin} The plugin that was started, or `null` if invalid key given or plugin is already stopped.
*/
start: function (key, runAs)
{
if (runAs === undefined) { runAs = key; }
var entry = this.getEntry(runAs);
// Plugin already running under this key?
if (entry && !entry.active)
{
// It exists, we just need to start it up again
entry.active = true;
entry.plugin.start();
}
else if (!entry)
{
entry = this.createEntry(key, runAs);
}
return (entry) ? entry.plugin : null;
},
/**
* Creates a new instance of a global plugin, adds an entry into the plugins array and returns it.
*
* @method Phaser.Plugins.PluginManager#createEntry
* @private
* @since 3.9.0
*
* @param {string} key - The key of the plugin to create an instance of.
* @param {string} [runAs] - Run the plugin under a new key. This allows you to run one plugin multiple times.
*
* @return {?Phaser.Plugins.BasePlugin} The plugin that was started, or `null` if invalid key given.
*/
createEntry: function (key, runAs)
{
var entry = PluginCache.getCustom(key);
if (entry)
{
var instance = new entry.plugin(this);
entry = {
key: runAs,
plugin: instance,
active: true,
mapping: entry.mapping,
data: entry.data
};
this.plugins.push(entry);
instance.init(entry.data);
instance.start();
}
return entry;
},
/**
* Stops a global plugin from running.
*
* If the plugin is active then its active state will be set to false and the plugins `stop` method
* will be called.
*
* If the plugin is not already running, nothing will happen.
*
* @method Phaser.Plugins.PluginManager#stop
* @since 3.8.0
*
* @param {string} key - The key of the plugin to stop.
*
* @return {this} The Plugin Manager.
*/
stop: function (key)
{
var entry = this.getEntry(key);
if (entry && entry.active)
{
entry.active = false;
entry.plugin.stop();
}
return this;
},
/**
* Gets a global plugin from the Plugin Manager based on the given key and returns it.
*
* If it cannot find an active plugin based on the key, but there is one in the Plugin Cache with the same key,
* then it will create a new instance of the cached plugin and return that.
*
* @method Phaser.Plugins.PluginManager#get
* @since 3.8.0
*
* @param {string} key - The key of the plugin to get.
* @param {boolean} [autoStart=true] - Automatically start a new instance of the plugin if found in the cache, but not actively running.
*
* @return {?(Phaser.Plugins.BasePlugin|function)} The plugin, or `null` if no plugin was found matching the key.
*/
get: function (key, autoStart)
{
if (autoStart === undefined) { autoStart = true; }
var entry = this.getEntry(key);
if (entry)
{
return entry.plugin;
}
else
{
var plugin = this.getClass(key);
if (plugin && autoStart)
{
entry = this.createEntry(key, key);
return (entry) ? entry.plugin : null;
}
else if (plugin)
{
return plugin;
}
}
return null;
},
/**
* Returns the plugin class from the cache.
* Used internally by the Plugin Manager.
*
* @method Phaser.Plugins.PluginManager#getClass
* @since 3.8.0
*
* @param {string} key - The key of the plugin to get.
*
* @return {Phaser.Plugins.BasePlugin} A Plugin object
*/
getClass: function (key)
{
return PluginCache.getCustomClass(key);
},
/**
* Removes a global plugin from the Plugin Manager and Plugin Cache.
*
* It is up to you to remove all references to this plugin that you may hold within your game code.
*
* @method Phaser.Plugins.PluginManager#removeGlobalPlugin
* @since 3.8.0
*
* @param {string} key - The key of the plugin to remove.
*/
removeGlobalPlugin: function (key)
{
var entry = this.getEntry(key);
if (entry)
{
Remove(this.plugins, entry);
}
PluginCache.removeCustom(key);
},
/**
* Removes a scene plugin from the Plugin Manager and Plugin Cache.
*
* This will not remove the plugin from any active Scenes that are already using it.
*
* It is up to you to remove all references to this plugin that you may hold within your game code.
*
* @method Phaser.Plugins.PluginManager#removeScenePlugin
* @since 3.8.0
*
* @param {string} key - The key of the plugin to remove.
*/
removeScenePlugin: function (key)
{
Remove(this.scenePlugins, key);
PluginCache.remove(key);
},
/**
* Registers a new type of Game Object with the global Game Object Factory and / or Creator.
* This is usually called from within your Plugin code and is a helpful short-cut for creating
* new Game Objects.
*
* The key is the property that will be injected into the factories and used to create the
* Game Object. For example:
*
* ```javascript
* this.plugins.registerGameObject('clown', clownFactoryCallback, clownCreatorCallback);
* // later in your game code:
* this.add.clown();
* this.make.clown();
* ```
*
* The callbacks are what are called when the factories try to create a Game Object
* matching the given key. It's important to understand that the callbacks are invoked within
* the context of the GameObjectFactory. In this context there are several properties available
* to use:
*
* this.scene - A reference to the Scene that owns the GameObjectFactory.
* this.displayList - A reference to the Display List the Scene owns.
* this.updateList - A reference to the Update List the Scene owns.
*
* See the GameObjectFactory and GameObjectCreator classes for more details.
* Any public property or method listed is available from your callbacks under `this`.
*
* @method Phaser.Plugins.PluginManager#registerGameObject
* @since 3.8.0
*
* @param {string} key - The key of the Game Object that the given callbacks will create, i.e. `image`, `sprite`.
* @param {function} [factoryCallback] - The callback to invoke when the Game Object Factory is called.
* @param {function} [creatorCallback] - The callback to invoke when the Game Object Creator is called.
*/
registerGameObject: function (key, factoryCallback, creatorCallback)
{
if (factoryCallback)
{
GameObjectFactory.register(key, factoryCallback);
}
if (creatorCallback)
{
GameObjectCreator.register(key, creatorCallback);
}
return this;
},
/**
* Removes a previously registered Game Object from the global Game Object Factory and / or Creator.
* This is usually called from within your Plugin destruction code to help clean-up after your plugin has been removed.
*
* @method Phaser.Plugins.PluginManager#removeGameObject
* @since 3.19.0
*
* @param {string} key - The key of the Game Object to be removed from the factories.
* @param {boolean} [removeFromFactory=true] - Should the Game Object be removed from the Game Object Factory?
* @param {boolean} [removeFromCreator=true] - Should the Game Object be removed from the Game Object Creator?
*/
removeGameObject: function (key, removeFromFactory, removeFromCreator)
{
if (removeFromFactory === undefined) { removeFromFactory = true; }
if (removeFromCreator === undefined) { removeFromCreator = true; }
if (removeFromFactory)
{
GameObjectFactory.remove(key);
}
if (removeFromCreator)
{
GameObjectCreator.remove(key);
}
return this;
},
/**
* Registers a new file type with the global File Types Manager, making it available to all Loader
* Plugins created after this.
*
* This is usually called from within your Plugin code and is a helpful short-cut for creating
* new loader file types.
*
* The key is the property that will be injected into the Loader Plugin and used to load the
* files. For example:
*
* ```javascript
* this.plugins.registerFileType('wad', doomWadLoaderCallback);
* // later in your preload code:
* this.load.wad();
* ```
*
* The callback is what is called when the loader tries to load a file matching the given key.
* It's important to understand that the callback is invoked within
* the context of the LoaderPlugin. In this context there are several properties / methods available
* to use:
*
* this.addFile - A method to add the new file to the load queue.
* this.scene - The Scene that owns the Loader Plugin instance.
*
* See the LoaderPlugin class for more details. Any public property or method listed is available from
* your callback under `this`.
*
* @method Phaser.Plugins.PluginManager#registerFileType
* @since 3.8.0
*
* @param {string} key - The key of the Game Object that the given callbacks will create, i.e. `image`, `sprite`.
* @param {function} callback - The callback to invoke when the Game Object Factory is called.
* @param {Phaser.Scene} [addToScene] - Optionally add this file type into the Loader Plugin owned by the given Scene.
*/
registerFileType: function (key, callback, addToScene)
{
FileTypesManager.register(key, callback);
if (addToScene && addToScene.sys.load)
{
addToScene.sys.load[key] = callback;
}
},
/**
* Destroys this Plugin Manager and all associated plugins.
* It will iterate all plugins found and call their `destroy` methods.
*
* The PluginCache will remove all custom plugins.
*
* @method Phaser.Plugins.PluginManager#destroy
* @since 3.8.0
*/
destroy: function ()
{
for (var i = 0; i < this.plugins.length; i++)
{
this.plugins[i].plugin.destroy();
}
PluginCache.destroyCustomPlugins();
if (this.game.noReturn)
{
PluginCache.destroyCorePlugins();
}
this.game = null;
this.plugins = [];
this.scenePlugins = [];
}
});
/*
* "Sometimes, the elegant implementation is just a function.
* Not a method. Not a class. Not a framework. Just a function."
* -- John Carmack
*/
module.exports = PluginManager;