加载场景
本页介绍如何通过代码加载场景,以及在项目中组织场景的不同方法。
主要有两种方式:完全切换场景,以及叠加加载场景。
完全切换场景
这是最常见的方法,每个场景构成游戏中独立的一部分。例如,一个场景用作标题画面,每个关卡各使用一个场景。
此示例允许用户在标题画面和不同关卡之间切换。
只需将场景名称传给 SceneRegistry.changeScene 即可。
this.app.scenes.changeScene('Some Scene Name');
如果场景数据尚未加载,该函数会:
- 异步发送网络请求,获取新场景数据。
- 数据加载完成后,删除应用根节点下的全部子实体,销毁现有场景层级。
- 调用
loadSceneSettings;由于数据已加载,此时为同步执行。 - 调用
loadSceneHierarchy;由于数据已加载,此时为同步执行。
如果需要知道场景何时加载完成,或获取错误信息,请提供回调:
this.app.scenes.changeScene('Some Scene Name', (err, loadedSceneRootEntity) => {
if (err) {
console.error(err);
} else {
// Scene hierarchy has successfully been loaded
}
});
要避免 changeScene 时发送异步网络请求,可以提前调用 SceneRegistry.loadSceneData。这样 changeScene 就会同步执行,立即调用 loadSceneSettings 和 loadSceneHierarchy。
例如,已知玩家完成第 1 关后会进入第 2 关,就可以在第 1 关运行期间预先加载第 2 关数据。玩家通关后无需等待数据下载,即可立即进入下一关。
叠加加载场景
可以叠加加载多个场景层级,而不完全替换场景。常见用途是拆分大型世界,随时间逐步加载,避免一开始就加载全部内容。
另一种做法是让每个场景代表世界的一个区域,随着玩家移动而加载或销毁。系统只加载最近的相邻区域及其相关资源,同时销毁不再需要的区域并卸载资源,有助于管理内存和显存。
有时也用此方法在正式游戏加载前先创建特定代码和实体,使其在整个游戏会话中都可全局访问。
下面是一个简化示例:左上角界面属于主场景,其余场景层级按需加载和销毁。
不能同时加载同一场景层级的多个实例。编辑器为实体分配唯一 GUID,多次加载会导致 GUID 冲突。
如果需要同一实体层级的多个实例,请改用模板。模板在实例化时会生成唯一 GUID。
理解场景的工作方式
要高效使用场景,需要了解项目中的场景结构和加载过程。下面详细介绍这些机制。
场景与资源相互独立,具有专用属性和 API。
场景以场景注册项的形式保存在场景注册表中,可通过 Application 对象访问。根据名称查找注册项后,即可加载对应层级或设置。
应用根节点与编辑器中默认名为 Root 的场景层级根实体不同;后者是应用根节点的子实体。
加载场景层级和设置分别使用以下 API:
SceneRegistry.loadSceneHierarchy:加载场景层级。SceneRegistry.loadSceneSettings:加载场景设置。
以下示例展示如何加载层级或设置:
// Find the Scene Registry Item by the name of the scene
const sceneItem = this.app.scenes.find('Some Scene Name');
// Load the scene hierarchy with a callback when it has finished
this.app.scenes.loadSceneHierarchy(sceneItem, (err, loadedSceneRootEntity) => {
if (err) {
console.error(err);
} else {
// Scene hierarchy has successfully been loaded
}
});
// Load the scene settings with a callback when it has finished
this.app.scenes.loadSceneSettings(sceneItem, (err) => {
if (err) {
console.error(err);
} else {
// Scene settings have successfully been loaded
}
});
loadSceneHierarchy 和 loadSceneSettings 获取所需数据的方式相似。
调用时会向服务器异步请求场景数据。从发起加载到浏览器完成请求之间存在延迟,取决于网络速度、连接状况和场景大小;等待期间应用仍会持续更新。
网络请求完成后,引擎执行以下操作:
loadSceneHierarchy
- 根据已加载场景创建实体和组件,将层级添加到应用根节点。
- 调用新场景中 ScriptType 的
initialize和postInitialize。 - 调用传入
loadSceneHierarchy的回调。 - 可选的回调会接收已加载场景的根实体,便于按需修改或更改父实体。
loadSceneSettings
- 将加载的场景设置应用到应用。
- 调用传入
loadSceneSettings的回调。
默认情况下,loadSceneHierarchy 始终采用叠加加载。要完全切换场景,需要开发者自行移除或销毁原场景。
可采用以下方式,各有优缺点:
先销毁应用根节点下的所有子实体
此方法将步骤清晰分开:先销毁当前场景,再加载并创建新场景,便于管理。
// Find the Scene Registry Item by the name of the scene
const sceneItem = this.app.scenes.find('Some Scene Name');
// Destroy all children under application root to remove the currently loaded scene hierarchy
const rootChildren = this.app.root.children;
while(rootChildren.length > 0) {
rootChildren[0].destroy();
}
// Load the scene hierarchy with a callback when it has finished
this.app.scenes.loadSceneHierarchy(sceneItem, (err, loadedSceneRootEntity) => {
if (err) {
console.error(err);
} else {
// Scene hierarchy has successfully been loaded
}
});
但如上所述,调用 loadSceneHierarchy 到场景数据加载完成之间有延迟。等待网络请求时,应用会有若干帧渲染为空白,因此也可以考虑另一种方法。
新场景加载后再销毁旧场景根实体
在新场景层级加入后,于回调中销毁旧层级,确保通过网络加载数据期间旧场景仍然存在。
// Find the Scene Registry Item by the name of the scene
const sceneItem = this.app.scenes.find('Some Scene Name');
// Assume the old scene hierarchy's root entity is named 'Root' which is the default name
const oldSceneRootEntity = this.app.root.findByName('Root');
// Load the scene hierarchy with a callback when it has finished
this.app.scenes.loadSceneHierarchy(sceneItem, (err, loadedSceneRootEntity) => {
if (err) {
console.error(err);
} else {
// Scene hierarchy has successfully been loaded
oldSceneRootEntity.destroy();
}
});
但新场景脚本执行 initialize 和 postInitialize 时,旧场景也仍在层级中。如果脚本假定当前只有一个场景,可能发生冲突。例如,initialize 按名称查找实体时,旧场景中也有同名实体,脚本就可能错误引用旧实体;旧层级被销毁后,会产生异常行为。
为缓解这些问题,可以使用 SceneRegistry.loadSceneData,将场景数据加载与场景层级创建分开。
管理场景资源
常见问题是:加载场景时,是否会一起加载场景用到的资源?PlayCanvas 将场景与资源分开管理,需要分别加载,以便开发者灵活控制。
推荐为资源添加所属场景名称作为标签。加载场景时,先加载带有相应标签的资源,全部就绪后再加载场景。
有关资源标签和加载,请参阅资源预加载。
下面的示例项目在进入场景时加载资源,返回主菜单时卸载。