phaser/src/loader/LoaderPlugin.js
Richard Davey 6b2307594a Scene plugin flow overhaul
Every Plugin has been updated to correctly follow the same flow through the Scene lifecycle. Instead of listening for the Scene 'boot' event, which is only dispatched once (when the Scene is first created), they will now listen for the Scene 'start' event, which occurs every time the Scene is started. All plugins now consistently follow the same Shutdown and Destroy patterns too, meaning they tidy-up after themselves on a shutdown, not just a destroy. Overall, this change means that there should be less issues when returning to previously closed Scenes, as the plugins will restart themselves properly.
2018-04-13 17:12:17 +01:00

1027 lines
26 KiB
JavaScript

/**
* @author Richard Davey <rich@photonstorm.com>
* @copyright 2018 Photon Storm Ltd.
* @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
*/
var Class = require('../utils/Class');
var CONST = require('./const');
var CustomSet = require('../structs/Set');
var EventEmitter = require('eventemitter3');
var FileTypesManager = require('./FileTypesManager');
var GetFastValue = require('../utils/object/GetFastValue');
var ParseXMLBitmapFont = require('../gameobjects/bitmaptext/ParseXMLBitmapFont');
var PluginManager = require('../boot/PluginManager');
var XHRSettings = require('./XHRSettings');
/**
* @typedef {object} LinkFileObject
*
* @property {string} type - [description]
* @property {Phaser.Loader.File} fileA - [description]
* @property {Phaser.Loader.File} fileB - [description]
*/
/**
* @typedef {object} LoaderFileObject
*
* @property {string} key - [description]
* @property {string} type - [description]
* @property {string} [url] - [description]
* @property {string[]} [urls] - [description]
* @property {string} [textureURL] - [description]
* @property {string} [atlasURL] - [description]
* @property {string} [xmlURL] - [description]
* @property {string[]} [textureURLs] - [description]
* @property {string[]} [atlasURLs] - [description]
* @property {object} [config] - [description]
* @property {object} [json] - [description]
* @property {XHRSettingsObject} [xhrSettings] - [description]
* @property {XHRSettingsObject} [textureXhrSettings] - [description]
* @property {XHRSettingsObject} [atlasXhrSettings] - [description]
* @property {XHRSettingsObject} [xmlXhrSettings] - [description]
* @property {XHRSettingsObject} [audioXhrSettings] - [description]
* @property {XHRSettingsObject} [jsonXhrSettings] - [description]
*/
/**
* @classdesc
* [description]
*
* @class LoaderPlugin
* @extends Phaser.Events.EventEmitter
* @memberOf Phaser.Loader
* @constructor
* @since 3.0.0
*
* @param {Phaser.Scene} scene - [description]
*/
var LoaderPlugin = new Class({
Extends: EventEmitter,
initialize:
function LoaderPlugin (scene)
{
EventEmitter.call(this);
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#scene
* @type {Phaser.Scene}
* @since 3.0.0
*/
this.scene = scene;
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#systems
* @type {Phaser.Scenes.Systems}
* @since 3.0.0
*/
this.systems = scene.sys;
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#_multilist
* @type {object}
* @private
* @default {}
* @since 3.0.0
*/
this._multilist = {};
// Inject the available filetypes into the Loader
FileTypesManager.install(this);
var gameConfig = this.systems.game.config;
var sceneConfig = this.systems.settings.loader;
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#path
* @type {string}
* @default ''
* @since 3.0.0
*/
this.path = '';
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#baseURL
* @type {string}
* @default ''
* @since 3.0.0
*/
this.baseURL = '';
this.setBaseURL(GetFastValue(sceneConfig, 'baseURL', gameConfig.loaderBaseURL));
this.setPath(GetFastValue(sceneConfig, 'path', gameConfig.loaderPath));
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#enableParallel
* @type {boolean}
* @since 3.0.0
*/
this.enableParallel = GetFastValue(sceneConfig, 'enableParallel', gameConfig.loaderEnableParallel);
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#maxParallelDownloads
* @type {integer}
* @since 3.0.0
*/
this.maxParallelDownloads = GetFastValue(sceneConfig, 'maxParallelDownloads', gameConfig.loaderMaxParallelDownloads);
/**
* xhr specific global settings (can be overridden on a per-file basis)
*
* @name Phaser.Loader.LoaderPlugin#xhr
* @type {XHRSettingsObject}
* @since 3.0.0
*/
this.xhr = XHRSettings(
GetFastValue(sceneConfig, 'responseType', gameConfig.loaderResponseType),
GetFastValue(sceneConfig, 'async', gameConfig.loaderAsync),
GetFastValue(sceneConfig, 'user', gameConfig.loaderUser),
GetFastValue(sceneConfig, 'password', gameConfig.loaderPassword),
GetFastValue(sceneConfig, 'timeout', gameConfig.loaderTimeout)
);
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#crossOrigin
* @type {string}
* @since 3.0.0
*/
this.crossOrigin = GetFastValue(sceneConfig, 'crossOrigin', gameConfig.loaderCrossOrigin);
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#totalToLoad
* @type {number}
* @default 0
* @since 3.0.0
*/
this.totalToLoad = 0;
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#progress
* @type {number}
* @default 0
* @since 3.0.0
*/
this.progress = 0;
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#list
* @type {Phaser.Structs.Set.<Phaser.Loader.File>}
* @since 3.0.0
*/
this.list = new CustomSet();
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#inflight
* @type {Phaser.Structs.Set.<Phaser.Loader.File>}
* @since 3.0.0
*/
this.inflight = new CustomSet();
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#failed
* @type {Phaser.Structs.Set.<Phaser.Loader.File>}
* @since 3.0.0
*/
this.failed = new CustomSet();
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#queue
* @type {Phaser.Structs.Set.<Phaser.Loader.File>}
* @since 3.0.0
*/
this.queue = new CustomSet();
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#storage
* @type {Phaser.Structs.Set.<(Phaser.Loader.File|LinkFileObject)>}
* @since 3.0.0
*/
this.storage = new CustomSet();
/**
* [description]
*
* @name Phaser.Loader.LoaderPlugin#state
* @type {integer}
* @since 3.0.0
*/
this.state = CONST.LOADER_IDLE;
scene.sys.events.on('start', this.boot, this);
},
/**
* This method is called automatically by the Scene when it is starting up.
* It is responsible for creating local systems, properties and listening for Scene events.
* Do not invoke it directly.
*
* @method Phaser.Loader.LoaderPlugin#boot
* @private
* @since 3.0.0
*/
boot: function ()
{
var eventEmitter = this.systems.events;
eventEmitter.once('shutdown', this.shutdown, this);
eventEmitter.once('destroy', this.destroy, this);
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#setBaseURL
* @since 3.0.0
*
* @param {string} url - [description]
*
* @return {Phaser.Loader.LoaderPlugin} This Loader object.
*/
setBaseURL: function (url)
{
if (url !== '' && url.substr(-1) !== '/')
{
url = url.concat('/');
}
this.baseURL = url;
return this;
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#setPath
* @since 3.0.0
*
* @param {string} path - [description]
*
* @return {Phaser.Loader.LoaderPlugin} This Loader object.
*/
setPath: function (path)
{
if (path !== '' && path.substr(-1) !== '/')
{
path = path.concat('/');
}
this.path = path;
return this;
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#setCORS
* @since 3.0.0
*
* @param {string} crossOrigin - [description]
*
* @return {Phaser.Loader.LoaderPlugin} This Loader object.
*/
setCORS: function (crossOrigin)
{
this.crossOrigin = crossOrigin;
return this;
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#addFile
* @since 3.0.0
*
* @param {Phaser.Loader.File} file - [description]
*
* @return {Phaser.Loader.File} [description]
*/
addFile: function (file)
{
if (!this.isReady())
{
return -1;
}
file.path = this.path;
this.list.set(file);
return file;
},
/**
* Is the Loader actively loading (or processing loaded files)
*
* @method Phaser.Loader.LoaderPlugin#isLoading
* @since 3.0.0
*
* @return {boolean} [description]
*/
isLoading: function ()
{
return (this.state === CONST.LOADER_LOADING || this.state === CONST.LOADER_PROCESSING);
},
/**
* Is the Loader ready to start a new load?
*
* @method Phaser.Loader.LoaderPlugin#isReady
* @since 3.0.0
*
* @return {boolean} [description]
*/
isReady: function ()
{
return (this.state === CONST.LOADER_IDLE || this.state === CONST.LOADER_COMPLETE);
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#start
* @since 3.0.0
*/
start: function ()
{
if (!this.isReady())
{
return;
}
this.progress = 0;
this.totalToLoad = this.list.size;
this.emit('start', this);
if (this.list.size === 0)
{
this.finishedLoading();
}
else
{
this.state = CONST.LOADER_LOADING;
this.failed.clear();
this.inflight.clear();
this.queue.clear();
this.queue.debug = true;
this.updateProgress();
this.processLoadQueue();
}
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#updateProgress
* @since 3.0.0
*/
updateProgress: function ()
{
this.progress = 1 - ((this.list.size + this.inflight.size) / this.totalToLoad);
this.emit('progress', this.progress);
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#processLoadQueue
* @since 3.0.0
*/
processLoadQueue: function ()
{
this.list.each(function (file)
{
if (file.state === CONST.FILE_POPULATED || (file.state === CONST.FILE_PENDING && this.inflight.size < this.maxParallelDownloads))
{
this.inflight.set(file);
this.list.delete(file);
this.loadFile(file);
}
if (this.inflight.size === this.maxParallelDownloads)
{
// Tells the Set iterator to abort
return false;
}
}, this);
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#loadFile
* @since 3.0.0
*
* @param {Phaser.Loader.File} file - [description]
*/
loadFile: function (file)
{
// If the file doesn't have its own crossOrigin set,
// we'll use the Loaders (which is undefined by default)
if (!file.crossOrigin)
{
file.crossOrigin = this.crossOrigin;
}
file.load(this);
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#nextFile
* @since 3.0.0
*
* @param {Phaser.Loader.File} previousFile - [description]
* @param {boolean} success - [description]
*/
nextFile: function (previousFile, success)
{
// Move the file that just loaded from the inflight list to the queue or failed Set
if (success)
{
this.emit('load', previousFile);
this.queue.set(previousFile);
}
else
{
this.emit('loaderror', previousFile);
this.failed.set(previousFile);
}
this.inflight.delete(previousFile);
this.updateProgress();
if (this.list.size > 0)
{
this.processLoadQueue();
}
else if (this.inflight.size === 0)
{
this.finishedLoading();
}
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#finishedLoading
* @since 3.0.0
*/
finishedLoading: function ()
{
if (this.state === CONST.LOADER_PROCESSING)
{
return;
}
this.progress = 1;
this.state = CONST.LOADER_PROCESSING;
this.storage.clear();
if (this.queue.size === 0)
{
// Everything failed, so nothing to process
this.processComplete();
}
else
{
this.queue.each(function (file)
{
file.onProcess(this.processUpdate.bind(this));
}, this);
}
},
/**
* Called automatically by the File when it has finished processing.
*
* @method Phaser.Loader.LoaderPlugin#processUpdate
* @since 3.0.0
*
* @param {Phaser.Loader.File} file - [description]
*/
processUpdate: function (file)
{
// This file has failed to load, so move it to the failed Set
if (file.state === CONST.FILE_ERRORED)
{
this.failed.set(file);
if (file.linkFile)
{
this.queue.delete(file.linkFile);
}
return this.removeFromQueue(file);
}
// If we got here, then the file loaded
// Special handling for multi-part files
if (file.linkFile)
{
if (file.state === CONST.FILE_COMPLETE && file.linkFile.state === CONST.FILE_COMPLETE)
{
// Partner has loaded, so add them both to Storage
this.storage.set({ type: file.linkType, fileA: file, fileB: file.linkFile });
this.queue.delete(file.linkFile);
this.removeFromQueue(file);
}
}
else
{
this.storage.set(file);
this.removeFromQueue(file);
}
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#removeFromQueue
* @since 3.0.0
*
* @param {Phaser.Loader.File} file - [description]
*/
removeFromQueue: function (file)
{
this.queue.delete(file);
if (this.queue.size === 0 && this.state === CONST.LOADER_PROCESSING)
{
// We've processed all the files we loaded
this.processComplete();
}
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#processComplete
* @since 3.0.0
*/
processComplete: function ()
{
this.list.clear();
this.inflight.clear();
this.queue.clear();
this.processCallback();
this.state = CONST.LOADER_COMPLETE;
this.emit('complete', this, this.storage.size, this.failed.size);
// Move to a User setting:
// this.removeAllListeners();
},
/**
* The Loader has finished.
*
* @method Phaser.Loader.LoaderPlugin#processCallback
* @since 3.0.0
*/
processCallback: function ()
{
if (this.storage.size === 0)
{
return;
}
// The global Texture Manager
var cache = this.scene.sys.cache;
var textures = this.scene.sys.textures;
var anims = this.scene.sys.anims;
// Process multiatlas groups first
var file;
var fileA;
var fileB;
for (var key in this._multilist)
{
var data = [];
var images = [];
var keys = this._multilist[key];
for (var i = 0; i < keys.length; i++)
{
file = this.storage.get('key', keys[i]);
if (file)
{
if (file.type === 'image')
{
images.push(file.data);
}
else if (file.type === 'json')
{
data.push(file.data);
}
this.storage.delete(file);
}
}
// Do we have everything needed?
if (images.length + data.length === keys.length)
{
// Yup, add them to the Texture Manager
// Is the data JSON Hash or JSON Array?
if (Array.isArray(data[0].textures) || Array.isArray(data[0].frames))
{
textures.addAtlasJSONArray(key, images, data);
}
else
{
textures.addAtlasJSONHash(key, images, data);
}
}
}
// Process all of the files
// Because AnimationJSON may require images to be loaded first, we process them last
var animJSON = [];
this.storage.each(function (file)
{
switch (file.type)
{
case 'animationJSON':
animJSON.push(file);
break;
case 'image':
case 'svg':
case 'html':
textures.addImage(file.key, file.data);
break;
case 'atlasjson':
fileA = file.fileA;
fileB = file.fileB;
if (fileA.type === 'image')
{
textures.addAtlas(fileA.key, fileA.data, fileB.data);
}
else
{
textures.addAtlas(fileB.key, fileB.data, fileA.data);
}
break;
case 'dataimage':
fileA = file.fileA;
fileB = file.fileB;
if (fileA.linkParent)
{
textures.addImage(fileA.key, fileA.data, fileB.data);
}
else
{
textures.addImage(fileB.key, fileB.data, fileA.data);
}
break;
case 'unityatlas':
fileA = file.fileA;
fileB = file.fileB;
if (fileA.type === 'image')
{
textures.addUnityAtlas(fileA.key, fileA.data, fileB.data);
}
else
{
textures.addUnityAtlas(fileB.key, fileB.data, fileA.data);
}
break;
case 'bitmapfont':
fileA = file.fileA;
fileB = file.fileB;
if (fileA.type === 'image')
{
cache.bitmapFont.add(fileB.key, { data: ParseXMLBitmapFont(fileB.data), texture: fileA.key, frame: null });
textures.addImage(fileA.key, fileA.data);
}
else
{
cache.bitmapFont.add(fileA.key, { data: ParseXMLBitmapFont(fileA.data), texture: fileB.key, frame: null });
textures.addImage(fileB.key, fileB.data);
}
break;
case 'spritesheet':
textures.addSpriteSheet(file.key, file.data, file.config);
break;
case 'json':
cache.json.add(file.key, file.data);
break;
case 'xml':
cache.xml.add(file.key, file.data);
break;
case 'text':
cache.text.add(file.key, file.data);
break;
case 'obj':
cache.obj.add(file.key, file.data);
break;
case 'binary':
cache.binary.add(file.key, file.data);
break;
case 'audio':
cache.audio.add(file.key, file.data);
break;
case 'audioSprite':
var files = [ file.fileA, file.fileB ];
files.forEach(function (file)
{
cache[file.type].add(file.key, file.data);
});
break;
case 'glsl':
cache.shader.add(file.key, file.data);
break;
case 'tilemapCSV':
case 'tilemapJSON':
cache.tilemap.add(file.key, { format: file.tilemapFormat, data: file.data });
break;
}
});
animJSON.forEach(function (file)
{
anims.fromJSON(file.data);
});
this.storage.clear();
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#saveJSON
* @since 3.0.0
*
* @param {*} data - [description]
* @param {string} [filename=file.json] - [description]
*
* @return {Phaser.Loader.LoaderPlugin} This Loader plugin.
*/
saveJSON: function (data, filename)
{
return this.save(JSON.stringify(data), filename);
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#save
* @since 3.0.0
*
* @param {*} data - [description]
* @param {string} [filename=file.json] - [description]
* @param {string} [filetype=application/json] - [description]
*
* @return {Phaser.Loader.LoaderPlugin} This Loader plugin.
*/
save: function (data, filename, filetype)
{
if (filename === undefined) { filename = 'file.json'; }
if (filetype === undefined) { filetype = 'application/json'; }
var blob = new Blob([ data ], { type: filetype });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.download = filename;
a.textContent = 'Download ' + filename;
a.href = url;
a.click();
return this;
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#reset
* @since 3.0.0
*/
reset: function ()
{
this.list.clear();
this.inflight.clear();
this.failed.clear();
this.queue.clear();
this.storage.clear();
var gameConfig = this.systems.game.config;
var sceneConfig = this.systems.settings.loader;
this.setBaseURL(GetFastValue(sceneConfig, 'baseURL', gameConfig.loaderBaseURL));
this.setPath(GetFastValue(sceneConfig, 'path', gameConfig.loaderPath));
this.state = CONST.LOADER_IDLE;
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#loadArray
* @since 3.0.0
*
* @param {LoaderFileObject[]} files - [description]
*
* @return {boolean} [description]
*/
loadArray: function (files)
{
if (Array.isArray(files))
{
for (var i = 0; i < files.length; i++)
{
this.file(files[i]);
}
}
return (this.list.size > 0);
},
/**
* [description]
*
* @method Phaser.Loader.LoaderPlugin#file
* @since 3.0.0
*
* @param {LoaderFileObject} file - [description]
*
* @return {Phaser.Loader.File} [description]
*/
file: function (file)
{
var entry;
var key = file.key;
switch (file.type)
{
case 'spritesheet':
entry = this.spritesheet(key, file.url, file.config, file.xhrSettings);
break;
case 'atlas':
entry = this.atlas(key, file.textureURL, file.atlasURL, file.textureXhrSettings, file.atlasXhrSettings);
break;
case 'bitmapFont':
entry = this.bitmapFont(key, file.textureURL, file.xmlURL, file.textureXhrSettings, file.xmlXhrSettings);
break;
case 'multiatlas':
entry = this.multiatlas(key, file.textureURLs, file.atlasURLs, file.textureXhrSettings, file.atlasXhrSettings);
break;
case 'audioSprite':
entry = this.audioSprite(key, file.urls, file.json, file.config, file.audioXhrSettings, file.jsonXhrSettings);
break;
// image, json, xml, binary, text, glsl, svg, obj
default:
entry = this[file.type](key, file.url, file.xhrSettings);
break;
}
return entry;
},
/**
* The Scene that owns this plugin is shutting down.
* We need to kill and reset all internal properties as well as stop listening to Scene events.
*
* @method Phaser.Loader.LoaderPlugin#shutdown
* @private
* @since 3.0.0
*/
shutdown: function ()
{
this.reset();
this.state = CONST.LOADER_SHUTDOWN;
var eventEmitter = this.systems.events;
eventEmitter.off('shutdown', this.shutdown, this);
},
/**
* The Scene that owns this plugin is being destroyed.
* We need to shutdown and then kill off all external references.
*
* @method Phaser.Loader.LoaderPlugin#destroy
* @private
* @since 3.0.0
*/
destroy: function ()
{
this.shutdown();
this.state = CONST.LOADER_DESTROYED;
this.scene.sys.events.off('start', this.start, this);
this.list = null;
this.inflight = null;
this.failed = null;
this.queue = null;
this.storage = null;
this.scene = null;
this.systems = null;
}
});
PluginManager.register('Loader', LoaderPlugin, 'load');
module.exports = LoaderPlugin;