UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

213 lines (210 loc) 7.82 kB
import { EventHandler } from '../../core/event-handler.js'; import { Asset } from './asset.js'; /** * @import { AssetRegistry } from './asset-registry.js' */ /** * Used to load a group of assets and fires a callback when all assets are loaded. * * ```javascript * const assets = [ * new Asset('model', 'container', { url: `http://example.com/asset.glb` }), * new Asset('styling', 'css', { url: `http://example.com/asset.css` }) * ]; * const assetListLoader = new AssetListLoader(assets, app.assets); * assetListLoader.load((err, failed) => { * if (err) { * console.error(`${failed.length} assets failed to load`); * } else { * console.log(`${assets.length} assets loaded`); * } * }); * ``` * * @category Asset */ class AssetListLoader extends EventHandler { /** * Create a new AssetListLoader using a list of assets to load and the asset registry used to * load and manage them. * * @param {Asset[]|number[]} assetList - An array of {@link Asset} objects to load or an array * of Asset IDs to load. * @param {AssetRegistry} assetRegistry - The application's asset registry. * @example * const assetListLoader = new pc.AssetListLoader([ * new pc.Asset("texture1", "texture", { url: 'http://example.com/my/assets/here/texture1.png') }), * new pc.Asset("texture2", "texture", { url: 'http://example.com/my/assets/here/texture2.png') }) * ], app.assets); */ constructor(assetList, assetRegistry){ super(), /** * @type {Set<Asset>} * @private */ this._assets = new Set(), /** * @type {Set<Asset>} * @private */ this._loadingAssets = new Set(), /** * @type {Set<Asset>} * @private */ this._waitingAssets = new Set(), /** @private */ this._loading = false, /** @private */ this._loaded = false, /** * Array of assets that failed to load. * * @type {Asset[]} * @private */ this._failed = []; this._registry = assetRegistry; assetList.forEach((a)=>{ if (a instanceof Asset) { if (!a.registry) { a.registry = assetRegistry; } this._assets.add(a); } else { const asset = assetRegistry.get(a); if (asset) { this._assets.add(asset); } else { this._waitForAsset(a); } } }); } /** * Removes all references to this asset list loader. */ destroy() { // remove any outstanding listeners this._registry.off('load', this._onLoad); this._registry.off('error', this._onError); this._waitingAssets.forEach((id)=>{ this._registry.off(`add:${id}`, this._onAddAsset); }); this.off('progress'); this.off('load'); } _assetHasDependencies(asset) { return asset.type === 'model' && asset.file?.url && asset.file.url && asset.file.url.match(/.json$/g); } /** * Start loading asset list and call `done()` when all assets have loaded or failed to load. * * @param {Function} done - Callback called when all assets in the list are loaded. Passed * `(err, failed)` where `err` is `undefined` if no errors are encountered and failed contains * an array of assets that failed to load. * @param {object} [scope] - Scope to use when calling callback. */ load(done, scope) { if (this._loading) { console.debug('AssetListLoader: Load function called multiple times.'); return; } this._loading = true; this._callback = done; this._scope = scope; this._registry.on('load', this._onLoad, this); this._registry.on('error', this._onError, this); let loadingAssets = false; this._assets.forEach((asset)=>{ // Track assets that are not loaded or are currently loading // as some assets may be loading by this call if (!asset.loaded) { loadingAssets = true; // json based models should be loaded with the loadFromUrl function so that their dependencies can be loaded too. if (this._assetHasDependencies(asset)) { this._registry.loadFromUrl(asset.file.url, asset.type, (err, loadedAsset)=>{ if (err) { this._onError(err, asset); return; } this._onLoad(asset); }); } this._loadingAssets.add(asset); this._registry.add(asset); } }); this._loadingAssets.forEach((asset)=>{ if (!this._assetHasDependencies(asset)) { this._registry.load(asset); } }); if (!loadingAssets && this._waitingAssets.size === 0) { this._loadingComplete(); } } /** * Sets a callback which will be called when all assets in the list have been loaded. * * @param {Function} done - Callback called when all assets in the list are loaded. * @param {object} [scope] - Scope to use when calling callback. */ ready(done, scope = this) { if (this._loaded) { done.call(scope, Array.from(this._assets)); } else { this.once('load', (assets)=>{ done.call(scope, assets); }); } } // called when all assets are loaded _loadingComplete() { if (this._loaded) return; this._loaded = true; this._registry.off('load', this._onLoad, this); this._registry.off('error', this._onError, this); if (this._failed.length) { if (this._callback) { this._callback.call(this._scope, 'Failed to load some assets', this._failed); } this.fire('error', this._failed); } else { if (this._callback) { this._callback.call(this._scope); } this.fire('load', Array.from(this._assets)); } } // called when an (any) asset is loaded _onLoad(asset) { // check this is an asset we care about if (this._loadingAssets.has(asset)) { this.fire('progress', asset); this._loadingAssets.delete(asset); } if (this._loadingAssets.size === 0) { // call next tick because we want // this to be fired after any other // asset load events setTimeout(()=>{ this._loadingComplete(); }, 0); } } // called when an asset fails to load _onError(err, asset) { // check this is an asset we care about if (this._loadingAssets.has(asset)) { this._failed.push(asset); this._loadingAssets.delete(asset); } if (this._loadingAssets.size === 0) { // call next tick because we want // this to be fired after any other // asset load events setTimeout(()=>{ this._loadingComplete(); }, 0); } } // called when a expected asset is added to the asset registry _onAddAsset(asset) { // remove from waiting list this._waitingAssets.delete(asset); this._assets.add(asset); if (!asset.loaded) { this._loadingAssets.add(asset); this._registry.load(asset); } } _waitForAsset(assetId) { this._waitingAssets.add(assetId); this._registry.once(`add:${assetId}`, this._onAddAsset, this); } } export { AssetListLoader };