跳到主要内容

加载场景

本页介绍如何通过代码加载场景,以及在项目中组织场景的不同方法。

主要有两种方式:完全切换场景,以及叠加加载场景。

完全切换场景​

这是最常见的方法,每个场景构成游戏中独立的一部分。例如,一个场景用作标题画面,每个关卡各使用一个场景。

此示例允许用户在标题画面和不同关卡之间切换。

只需将场景名称传给 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:

以下示例展示如何加载层级或设置:

// 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 将场景与资源分开管理,需要分别加载,以便开发者灵活控制。

推荐为资源添加所属场景名称作为标签。加载场景时,先加载带有相应标签的资源,全部就绪后再加载场景。

有关资源标签和加载,请参阅资源预加载。

下面的示例项目在进入场景时加载资源,返回主菜单时卸载。