UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

388 lines (385 loc) 16.4 kB
import { path } from '../core/path.js'; import { Debug } from '../core/debug.js'; import { ABSOLUTE_URL } from './asset/constants.js'; import { SceneRegistryItem } from './scene-registry-item.js'; /** * @import { AppBase } from './app-base.js' * @import { Entity } from './entity.js' */ /** * @callback LoadHierarchyCallback * Callback used by {@link SceneRegistry#loadSceneHierarchy}. * @param {string|null} err - The error message in the case where the loading or parsing fails. * @param {Entity} [entity] - The loaded root entity if no errors were encountered. * @returns {void} */ /** * @callback LoadSettingsCallback * Callback used by {@link SceneRegistry#loadSceneSettings}. * @param {string|null} err - The error message in the case where the loading or parsing fails. * @returns {void} */ /** * @callback ChangeSceneCallback * Callback used by {@link SceneRegistry#changeScene}. * @param {string|null} err - The error message in the case where the loading or parsing fails. * @param {Entity} [entity] - The loaded root entity if no errors were encountered. * @returns {void} */ /** * @callback LoadSceneCallback * Callback used by {@link SceneRegistry#loadScene}. * @param {string|null} err - The error message in the case where the loading or parsing fails. * @param {Entity} [entity] - The loaded root entity if no errors were encountered. * @returns {void} */ /** * @callback LoadSceneDataCallback * Callback used by {@link SceneRegistry#loadSceneData}. * @param {string|null} err - The error message in the case where the loading or parsing fails. * @param {SceneRegistryItem} [sceneItem] - The scene registry item if no errors were encountered. * @returns {void} */ /** * Container for storing and loading of scenes. An instance of the registry is created on the * {@link AppBase} object as {@link AppBase#scenes}. * * @category Graphics */ class SceneRegistry { /** * Create a new SceneRegistry instance. * * @param {AppBase} app - The application. */ constructor(app){ /** * @type {SceneRegistryItem[]} * @private */ this._list = []; /** @private */ this._index = {}; /** @private */ this._urlIndex = {}; this._app = app; } /** @ignore */ destroy() { this._app = null; } /** * Return the list of scene. * * @returns {SceneRegistryItem[]} All items in the registry. */ list() { return this._list; } /** * Add a new item to the scene registry. * * @param {string} name - The name of the scene. * @param {string} url - The url of the scene file. * @returns {boolean} Returns true if the scene was successfully added to the registry, false otherwise. */ add(name, url) { if (this._index.hasOwnProperty(name)) { Debug.warn(`pc.SceneRegistry: trying to add more than one scene called: ${name}`); return false; } const item = new SceneRegistryItem(name, url); const i = this._list.push(item); this._index[item.name] = i - 1; this._urlIndex[item.url] = i - 1; return true; } /** * Find a Scene by name and return the {@link SceneRegistryItem}. * * @param {string} name - The name of the scene. * @returns {SceneRegistryItem|null} The stored data about a scene or null if no scene with * that name exists. */ find(name) { if (this._index.hasOwnProperty(name)) { return this._list[this._index[name]]; } return null; } /** * Find a scene by the URL and return the {@link SceneRegistryItem}. * * @param {string} url - The URL to search by. * @returns {SceneRegistryItem|null} The stored data about a scene or null if no scene with * that URL exists. */ findByUrl(url) { if (this._urlIndex.hasOwnProperty(url)) { return this._list[this._urlIndex[url]]; } return null; } /** * Remove an item from the scene registry. * * @param {string} name - The name of the scene. */ remove(name) { if (this._index.hasOwnProperty(name)) { const idx = this._index[name]; let item = this._list[idx]; delete this._urlIndex[item.url]; // remove from index delete this._index[name]; // remove from list this._list.splice(idx, 1); // refresh index for(let i = 0; i < this._list.length; i++){ item = this._list[i]; this._index[item.name] = i; this._urlIndex[item.url] = i; } } } /** * Private function to load scene data with the option to cache. This allows us to retain * expected behavior of loadSceneSettings and loadSceneHierarchy where they don't store loaded * data which may be undesired behavior with projects that have many scenes. * * @param {SceneRegistryItem | string} sceneItem - The scene item (which can be found with * {@link SceneRegistry#find}, URL of the scene file (e.g."scene_id.json") or name of the scene. * @param {boolean} storeInCache - Whether to store the loaded data in the scene item. * @param {LoadSceneDataCallback} callback - The function to call after loading, * passed (err, sceneItem) where err is null if no errors occurred. * @private */ _loadSceneData(sceneItem, storeInCache, callback) { const app = this._app; // If it's a sceneItem, we want to be able to cache the data that is loaded so we don't do // a subsequent http requests on the same scene later // If it's just a URL or scene name then attempt to find the scene item in the registry // else create a temp SceneRegistryItem to use for this function as the scene may not have // been added to the registry let url = sceneItem; if (typeof sceneItem === 'string') { sceneItem = this.findByUrl(url) || this.find(url) || new SceneRegistryItem('Untitled', url); } url = sceneItem.url; if (!url) { callback('Cannot find scene to load'); return; } // If we have the data already loaded, no need to do another HTTP request if (sceneItem.loaded) { callback(null, sceneItem); return; } // include asset prefix if present if (app.assets && app.assets.prefix && !ABSOLUTE_URL.test(url)) { url = path.join(app.assets.prefix, url); } sceneItem._onLoadedCallbacks.push(callback); if (!sceneItem._loading) { // Because we need to load scripts before we instance the hierarchy (i.e. before we // create script components), split loading into load and open const handler = app.loader.getHandler('hierarchy'); handler.load(url, (err, data)=>{ sceneItem.data = data; sceneItem._loading = false; for(let i = 0; i < sceneItem._onLoadedCallbacks.length; i++){ sceneItem._onLoadedCallbacks[i](err, sceneItem); } // Remove the data if it's not been requested to store in cache if (!storeInCache) { sceneItem.data = null; } sceneItem._onLoadedCallbacks.length = 0; }); } sceneItem._loading = true; } /** * Loads and stores the scene data to reduce the number of the network requests when the same * scenes are loaded multiple times. Can also be used to load data before calling * {@link SceneRegistry#loadSceneHierarchy} and {@link SceneRegistry#loadSceneSettings} to make * scene loading quicker for the user. * * @param {SceneRegistryItem | string} sceneItem - The scene item (which can be found with * {@link SceneRegistry#find}, URL of the scene file (e.g."scene_id.json") or name of the scene. * @param {LoadSceneDataCallback} callback - The function to call after loading, * passed (err, sceneItem) where err is null if no errors occurred. * @example * const sceneItem = app.scenes.find("Scene Name"); * app.scenes.loadSceneData(sceneItem, (err, sceneItem) => { * if (err) { * // error * } * }); */ loadSceneData(sceneItem, callback) { this._loadSceneData(sceneItem, true, callback); } /** * Unloads scene data that has been loaded previously using {@link SceneRegistry#loadSceneData}. * * @param {SceneRegistryItem | string} sceneItem - The scene item (which can be found with * {@link SceneRegistry#find} or URL of the scene file. Usually this will be "scene_id.json". * @example * const sceneItem = app.scenes.find("Scene Name"); * app.scenes.unloadSceneData(sceneItem); */ unloadSceneData(sceneItem) { if (typeof sceneItem === 'string') { sceneItem = this.findByUrl(sceneItem); } if (sceneItem) { sceneItem.data = null; } } _loadSceneHierarchy(sceneItem, onBeforeAddHierarchy, callback) { this._loadSceneData(sceneItem, false, (err, sceneItem)=>{ if (err) { if (callback) { callback(err); } return; } if (onBeforeAddHierarchy) { onBeforeAddHierarchy(sceneItem); } const app = this._app; // called after scripts are preloaded const _loaded = ()=>{ // Because we need to load scripts before we instance the hierarchy (i.e. before we create script components) // Split loading into load and open const handler = app.loader.getHandler('hierarchy'); app.systems.script.preloading = true; const entity = handler.open(sceneItem.url, sceneItem.data); app.systems.script.preloading = false; // clear from cache because this data is modified by entity operations (e.g. destroy) app.loader.clearCache(sceneItem.url, 'hierarchy'); // add to hierarchy app.root.addChild(entity); // initialize components app.systems.fire('initialize', entity); app.systems.fire('postInitialize', entity); app.systems.fire('postPostInitialize', entity); if (callback) callback(null, entity); }; // load priority and referenced scripts before opening scene app._preloadScripts(sceneItem.data, _loaded); }); } /** * Load a scene file, create and initialize the Entity hierarchy and add the hierarchy to the * application root Entity. * * @param {SceneRegistryItem | string} sceneItem - The scene item (which can be found with * {@link SceneRegistry#find}, URL of the scene file (e.g."scene_id.json") or name of the scene. * @param {LoadHierarchyCallback} callback - The function to call after loading, * passed (err, entity) where err is null if no errors occurred. * @example * const sceneItem = app.scenes.find("Scene Name"); * app.scenes.loadSceneHierarchy(sceneItem, (err, entity) => { * if (!err) { * const e = app.root.find("My New Entity"); * } else { * // error * } * }); */ loadSceneHierarchy(sceneItem, callback) { this._loadSceneHierarchy(sceneItem, null, callback); } /** * Load a scene file and apply the scene settings to the current scene. * * @param {SceneRegistryItem | string} sceneItem - The scene item (which can be found with * {@link SceneRegistry#find}, URL of the scene file (e.g."scene_id.json") or name of the scene. * @param {LoadSettingsCallback} callback - The function called after the settings * are applied. Passed (err) where err is null if no error occurred. * @example * const sceneItem = app.scenes.find("Scene Name"); * app.scenes.loadSceneSettings(sceneItem, (err) => { * if (!err) { * // success * } else { * // error * } * }); */ loadSceneSettings(sceneItem, callback) { this._loadSceneData(sceneItem, false, (err, sceneItem)=>{ if (!err) { this._app.applySceneSettings(sceneItem.data.settings); if (callback) { callback(null); } } else { if (callback) { callback(err); } } }); } /** * Change to a new scene. Calling this function will load the scene data, delete all * entities and graph nodes under `app.root` and load the scene settings and hierarchy. * * @param {SceneRegistryItem | string} sceneItem - The scene item (which can be found with * {@link SceneRegistry#find}, URL of the scene file (e.g."scene_id.json") or name of the scene. * @param {ChangeSceneCallback} [callback] - The function to call after loading, * passed (err, entity) where err is null if no errors occurred. * @example * app.scenes.changeScene("Scene Name", (err, entity) => { * if (!err) { * // success * } else { * // error * } * }); */ changeScene(sceneItem, callback) { const app = this._app; const onBeforeAddHierarchy = (sceneItem)=>{ // Destroy all nodes on the app.root const { children } = app.root; while(children.length){ children[0].destroy(); } app.applySceneSettings(sceneItem.data.settings); }; this._loadSceneHierarchy(sceneItem, onBeforeAddHierarchy, callback); } /** * Load the scene hierarchy and scene settings. This is an internal method used by the * {@link AppBase}. * * @param {string} url - The URL of the scene file. * @param {LoadSceneCallback} callback - The function called after the settings are * applied. Passed (err, scene) where err is null if no error occurred and scene is the * {@link Scene}. */ loadScene(url, callback) { const app = this._app; const handler = app.loader.getHandler('scene'); // include asset prefix if present if (app.assets && app.assets.prefix && !ABSOLUTE_URL.test(url)) { url = path.join(app.assets.prefix, url); } handler.load(url, (err, data)=>{ if (!err) { const _loaded = ()=>{ // parse and create scene app.systems.script.preloading = true; const scene = handler.open(url, data); // Cache the data as we are loading via URL only const sceneItem = this.findByUrl(url); if (sceneItem && !sceneItem.loaded) { sceneItem.data = data; } app.systems.script.preloading = false; // clear scene from cache because we'll destroy it when we load another one // so data will be invalid app.loader.clearCache(url, 'scene'); app.loader.patch({ resource: scene, type: 'scene' }, app.assets); app.root.addChild(scene.root); // Initialize pack settings if (app.systems.rigidbody && typeof Ammo !== 'undefined') { app.systems.rigidbody.gravity.set(scene._gravity.x, scene._gravity.y, scene._gravity.z); } if (callback) { callback(null, scene); } }; // preload scripts before opening scene app._preloadScripts(data, _loaded); } else { if (callback) { callback(err); } } }); } } export { SceneRegistry };