Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added changeScene API #4626

Merged
merged 22 commits into from
Sep 21, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4142656
Added changeScene API
steveny-sc Sep 8, 2022
2a80b76
Fixed scope issue
steveny-sc Sep 9, 2022
439d7f8
Updated error message for when a scene cannot be found
steveny-sc Sep 9, 2022
0cef99c
Updated deletion of all entities/nodes under app.root
steveny-sc Sep 9, 2022
b0941dd
Update src/framework/scene-registry.js
yaustar Sep 9, 2022
61081f4
Update src/framework/scene-registry.js
yaustar Sep 14, 2022
69d46ae
Reduce the number of property lookups
steveny-sc Sep 14, 2022
e27799b
Fixed missing self reference
steveny-sc Sep 14, 2022
3bd4414
Merge branch 'change-scenes-api' of github.com:playcanvas/engine into…
steveny-sc Sep 14, 2022
9b8853e
Removed trailing whitespace
steveny-sc Sep 14, 2022
0abda98
LoadScene* functions now are able to take a scene name
steveny-sc Sep 15, 2022
421b453
Added tests to find scene data by name and url
steveny-sc Sep 15, 2022
b7b2656
Moved to ES6 closures
steveny-sc Sep 16, 2022
5948c02
Abstracted loadHierarchy to DRY
steveny-sc Sep 16, 2022
79bcb76
Fixed bug where the wrong param was passed
steveny-sc Sep 16, 2022
3a85de1
Removed uneeded extra function
steveny-sc Sep 16, 2022
dcd280f
Removed temp variable for closure
steveny-sc Sep 20, 2022
dd83a5b
Backwards compatibility fix for being able to use URLs that are not p…
steveny-sc Sep 20, 2022
1777c31
Fixed ES6 function scope issue
steveny-sc Sep 20, 2022
c126b2d
Removed invalid test
steveny-sc Sep 20, 2022
749604e
Update src/framework/scene-registry.js
yaustar Sep 20, 2022
9e5ce5f
Update src/framework/scene-registry.js
yaustar Sep 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 128 additions & 81 deletions src/framework/scene-registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ import { SceneRegistryItem } from './scene-registry-item.js';
* @param {string|null} err - The error message in the case where the loading or parsing fails.
*/

/**
* Callback used by {@link SceneRegistry#changeScene}.
*
* @callback ChangeSceneCallback
* @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.
*/

/**
* Callback used by {@link SceneRegistry#loadScene}.
*
Expand Down Expand Up @@ -151,26 +159,23 @@ class SceneRegistry {
// 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.
_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 then attempt to find the scene item in
// the registry else create a temp SceneRegistryItem to use
// for this function
// 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
let url = sceneItem;

if (sceneItem instanceof SceneRegistryItem) {
url = sceneItem.url;
} else {
sceneItem = this.findByUrl(url);
if (!sceneItem) {
sceneItem = new SceneRegistryItem('Untitled', url);
}
if (typeof sceneItem === 'string') {
sceneItem = this.findByUrl(url) || this.find(url) || new SceneRegistryItem('Untitled', null);
}

url = sceneItem.url;

if (!sceneItem.url) {
yaustar marked this conversation as resolved.
Show resolved Hide resolved
yaustar marked this conversation as resolved.
Show resolved Hide resolved
callback("URL or SceneRegistryItem is null when loading a scene");
callback("Cannot find scene to load");
return;
}

Expand All @@ -180,19 +185,19 @@ class SceneRegistry {
return;
}

// 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 = this._app.loader.getHandler("hierarchy");

// include asset prefix if present
if (this._app.assets && this._app.assets.prefix && !ABSOLUTE_URL.test(url)) {
url = path.join(this._app.assets.prefix, url);
if (app.assets && app.assets.prefix && !ABSOLUTE_URL.test(url)) {
url = path.join(app.assets.prefix, url);
}

sceneItem._onLoadedCallbacks.push(callback);

if (!sceneItem._loading) {
handler.load(url, function (err, data) {
// 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;

Expand All @@ -219,7 +224,7 @@ class SceneRegistry {
* scene loading quicker for the user.
*
* @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".
* {@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
Expand Down Expand Up @@ -253,89 +258,96 @@ class SceneRegistry {
}
}

/**
* 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} or URL of the scene file. Usually this will be "scene_id.json".
* @param {LoadHierarchyCallback} callback - The function to call after loading,
* passed (err, entity) where err is null if no errors occurred.
* @example
* var sceneItem = app.scenes.find("Scene Name");
* app.scenes.loadSceneHierarchy(sceneItem, function (err, entity) {
* if (!err) {
* var e = app.root.find("My New Entity");
* } else {
* // error
* }
* });
*/
loadSceneHierarchy(sceneItem, callback) {
const self = this;

// 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 = this._app.loader.getHandler("hierarchy");

this._loadSceneData(sceneItem, false, function (err, sceneItem) {
_loadSceneHierarchy(sceneItem, onBeforeAddHierarchy, callback) {
this._loadSceneData(sceneItem, false, (err, sceneItem) => {
if (err) {
if (callback) callback(err);
if (callback) {
callback(err);
}
return;
}

const url = sceneItem.url;
const data = sceneItem.data;
if (onBeforeAddHierarchy) {
onBeforeAddHierarchy(sceneItem);
}

const app = this._app;

// called after scripts are preloaded
const _loaded = function () {
self._app.systems.script.preloading = true;
const entity = handler.open(url, data);
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);

self._app.systems.script.preloading = false;
app.systems.script.preloading = false;

// clear from cache because this data is modified by entity operations (e.g. destroy)
self._app.loader.clearCache(url, "hierarchy");
app.loader.clearCache(sceneItem.url, "hierarchy");

// add to hierarchy
self._app.root.addChild(entity);
app.root.addChild(entity);

// initialize components
self._app.systems.fire('initialize', entity);
self._app.systems.fire('postInitialize', entity);
self._app.systems.fire('postPostInitialize', entity);
app.systems.fire('initialize', entity);
app.systems.fire('postInitialize', entity);
app.systems.fire('postPostInitialize', entity);

if (callback) callback(err, entity);
if (callback) callback(null, entity);
};

// load priority and referenced scripts before opening scene
self._app._preloadScripts(data, _loaded);
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
* var sceneItem = app.scenes.find("Scene Name");
* app.scenes.loadSceneHierarchy(sceneItem, function (err, entity) {
* if (!err) {
* var 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} or URL of the scene file. Usually this will be "scene_id.json".
* {@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
* var sceneItem = app.scenes.find("Scene Name");
* app.scenes.loadSceneHierarchy(sceneItem, function (err, entity) {
* app.scenes.loadSceneSettings(sceneItem, function (err) {
* if (!err) {
* var e = app.root.find("My New Entity");
* // success
* } else {
* // error
* }
* });
*/
loadSceneSettings(sceneItem, callback) {
const self = this;
const app = this._app;

this._loadSceneData(sceneItem, false, function (err, sceneItem) {
this._loadSceneData(sceneItem, false, (err, sceneItem) => {
if (!err) {
self._app.applySceneSettings(sceneItem.data.settings);
app.applySceneSettings(sceneItem.data.settings);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are using arrow functions now, you can just use this and move that closure variable into the arrow function itself. And since app is only used once, it doesn't really need to be declared at all

if (callback) {
callback(null);
}
Expand All @@ -347,6 +359,41 @@ class SceneRegistry {
});
}

/**
* 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", function (err, entity) {
yaustar marked this conversation as resolved.
Show resolved Hide resolved
* if (!err) {
* // success
* } else {
* // error
* }
* });
*/
changeScene(sceneItem, callback) {
const app = this._app;

const onBeforeAddHierarchy = (sceneItem) => {
Comment on lines +379 to +381
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This app closure variable also has too much context and maybe add JSDoc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is JSDoc needed here for a private function that is only used in the scope of the current class?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess its not needed, I just use them as a mind crutch and when the types are checked, it can offer some insight about what is going wrong in which position

// Destroy/Remove all nodes on the app.root
const rootChildren = app.root.children;
while (rootChildren.length > 0) {
const child = rootChildren[0];
child.reparent(null);
child.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}.
Expand All @@ -357,44 +404,44 @@ class SceneRegistry {
* {@link Scene}.
*/
loadScene(url, callback) {
const self = this;
const app = this._app;

const handler = this._app.loader.getHandler("scene");
const handler = app.loader.getHandler("scene");

// include asset prefix if present
if (this._app.assets && this._app.assets.prefix && !ABSOLUTE_URL.test(url)) {
url = path.join(this._app.assets.prefix, url);
if (app.assets && app.assets.prefix && !ABSOLUTE_URL.test(url)) {
url = path.join(app.assets.prefix, url);
}

handler.load(url, function (err, data) {
handler.load(url, (err, data) => {
if (!err) {
const _loaded = function () {
// parse and create scene
self._app.systems.script.preloading = true;
app.systems.script.preloading = true;
const scene = handler.open(url, data);

// Cache the data as we are loading via URL only
const sceneItem = self.findByUrl(url);
const sceneItem = this.findByUrl(url);
if (sceneItem && !sceneItem.loaded) {
sceneItem.data = data;
}

self._app.systems.script.preloading = false;
app.systems.script.preloading = false;

// clear scene from cache because we'll destroy it when we load another one
// so data will be invalid
self._app.loader.clearCache(url, "scene");
app.loader.clearCache(url, "scene");

self._app.loader.patch({
app.loader.patch({
resource: scene,
type: "scene"
}, self._app.assets);
}, app.assets);

self._app.root.addChild(scene.root);
app.root.addChild(scene.root);

// Initialize pack settings
if (self._app.systems.rigidbody && typeof Ammo !== 'undefined') {
self._app.systems.rigidbody.gravity.set(scene._gravity.x, scene._gravity.y, scene._gravity.z);
if (app.systems.rigidbody && typeof Ammo !== 'undefined') {
app.systems.rigidbody.gravity.set(scene._gravity.x, scene._gravity.y, scene._gravity.z);
}

if (callback) {
Expand All @@ -403,7 +450,7 @@ class SceneRegistry {
};

// preload scripts before opening scene
self._app._preloadScripts(data, _loaded);
app._preloadScripts(data, _loaded);
} else {
if (callback) {
callback(err);
Expand Down
37 changes: 34 additions & 3 deletions test/framework/scene-registry.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ describe('SceneRegistry', function () {

});

const promisedLoadSceneData = function (registry, sceneItemOrUrl) {
const promisedLoadSceneData = function (registry, sceneItemOrNameOrUrl) {
return new Promise(function (resolve, reject) {
registry.loadSceneData(sceneItemOrUrl, function (err, sceneItem) {
registry.loadSceneData(sceneItemOrNameOrUrl, function (err, sceneItem) {
if (err) {
reject(err);
resolve(err);
}

resolve(sceneItem);
Expand Down Expand Up @@ -146,6 +146,37 @@ describe('SceneRegistry', function () {
expect(sceneItem._loading).to.equal(false);
});

it('try to load scene data that does not exist by name', async function () {
const registry = new SceneRegistry(app);
registry.add('New Scene 1', `${assetPath}scene.json`);

const err = await promisedLoadSceneData(registry, 'Scene 2');

expect(err).to.exist;
expect(err).to.equal('Cannot find scene to load');
});

it('try to load scene data that by name', async function () {
const registry = new SceneRegistry(app);
registry.add('New Scene 1', `${assetPath}scene.json`);

const sceneItem = await promisedLoadSceneData(registry, 'New Scene 1');

expect(sceneItem).to.exist;
expect(sceneItem.data).to.exist;
expect(sceneItem._loading).to.equal(false);
});

it('try to load scene data that by URL', async function () {
const registry = new SceneRegistry(app);
registry.add('New Scene 1', `${assetPath}scene.json`);

const sceneItem = await promisedLoadSceneData(registry, `${assetPath}scene.json`);

expect(sceneItem).to.exist;
expect(sceneItem.data).to.exist;
expect(sceneItem._loading).to.equal(false);
});
});

describe('#remove', function () {
Expand Down