Skip to content

Stage02|Engine Loop ​

Lesson014|Scene 是什么?加载和切换场景时发生了什么? ​

Tags: #Creator2.x #Stage02 #Scene #loadScene #LifecycleDifficulty: ⭐⭐⭐☆☆


一、本课目标 ​

本课解决三个问题:

  1. Scene 和 Node 的关系是什么?
  2. loadScene 期间引擎大概做了什么?
  3. 为什么切场景会触发大量生命周期、资源和内存变化?

完成本课后,你应该能够区分场景文件、运行时 Scene 对象和场景节点树,并能设计基本的场景切换流程。


二、Scene 不是一个普通的页面文件 ​

编辑器中的 Scene 文件是序列化数据,运行时的 Scene 是引擎管理的对象:

text
Scene 文件
    ↓ 反序列化
运行时 Scene
    ↓
根节点和子节点树
    ↓
Node / Component

Scene 主要承载:

  • 场景节点层级。
  • 节点和组件的序列化属性。
  • 当前场景的运行边界。
  • Canvas、Camera、UI 和游戏对象。

Scene 不等于页面 DOM,也不等于一个独立线程。它最终仍然由 Director 推进。


三、当前 Scene 和 Scene 文件的区别 ​

概念含义
Scene 资源文件编辑器保存的序列化描述
运行时 Scene引擎反序列化后管理的对象
Scene 根节点节点树的顶层结构
常驻节点跨场景保留的运行时对象
场景资源依赖Scene 使用的 Prefab、图片、脚本等

修改编辑器中的节点属性,最终会影响下次反序列化得到的运行时对象;运行时修改 Node 属性,通常不会自动写回 Scene 文件。


四、loadScene 的高层流程 ​

ts
cc.director.loadScene('Battle');

可以先按以下过程理解:

text
提出切换请求
    ↓
解析目标 Scene 资源
    ↓
加载 Scene 及依赖资源
    ↓
准备新 Scene 的序列化数据
    ↓
清理旧 Scene 中不再保留的对象
    ↓
创建新 Scene、Node 和 Component
    ↓
恢复常驻对象关系
    ↓
执行新场景生命周期
    ↓
设置为 Director 当前 Scene
    ↓
进入后续 mainLoop

不同 Creator 2.x 版本可能在加载、清理和事件细节上有所不同,但“请求—加载—创建—激活—接管主循环”的骨架是稳定的。


五、场景加载为什么会卡 ​

场景切换可能同时产生多个成本:

text
文件读取
资源解码
依赖加载
序列化解析
Node 创建
Component 创建
生命周期执行
纹理上传
渲染数据重建
旧对象清理

所以“场景切换卡顿”不一定只是 loadScene API 本身慢,可能是:

  • Scene 依赖图片过多。
  • 大纹理首次解码或上传。
  • 节点数量过多。
  • onLoad 中做了大量同步工作。
  • start 中批量生成对象。
  • 旧场景监听器或资源没有释放。

排查时要把加载、创建、生命周期和首次渲染拆开测量。


六、场景生命周期和节点生命周期 ​

新场景进入运行状态后,节点组件通常会经历:

text
创建对象
    ↓
onLoad
    ↓
onEnable
    ↓
start
    ↓
update / lateUpdate

旧场景离开时,可能涉及:

text
失去激活状态
    ↓
onDisable
    ↓
销毁或等待引用释放
    ↓
onDestroy

具体是否销毁,要看对象是否被常驻、外部是否仍有引用以及资源管理策略。

不要把 onLoad 当作“每次打开页面都必定执行”。如果对象没有被销毁而只是 active=false,再次启用通常不会重新走 onLoad。


七、常驻节点为什么存在 ​

登录信息、全局音频、网络管理器等对象可能需要跨场景保存:

text
Login Scene
    ↓ 切换
Main Scene
    ↓
Battle Scene

如果每次切场景都销毁网络连接和用户状态,会带来:

  • 重复初始化。
  • 状态丢失。
  • 连接反复建立。
  • 事件重复绑定。

常驻节点适合承载跨场景基础服务,但不代表所有对象都应该常驻。常驻对象过多会造成:

  • 内存持续增长。
  • 生命周期边界模糊。
  • 场景之间隐藏依赖。
  • 清理和测试困难。

原则是:只有真正跨场景共享且生命周期明确的对象才考虑常驻。


八、场景切换中的引用问题 ​

旧场景对象可能被这些地方继续引用:

text
全局单例
事件总线
定时器
闭包回调
网络回调
Promise 回调
对象池

即使 Scene 不再显示,这些引用也可能阻止对象和相关资源被回收。切场景时应检查:

text
onEnable 注册的事件是否在 onDisable 取消
schedule 是否在销毁前停止
异步回调是否检查对象有效性
全局管理器是否保留旧节点引用

这就是“画面已经换了,但内存没有下降”的常见原因之一。


九、场景切换的工程写法 ​

ts
loadBattle() {
    this.loadingNode.active = true;

    cc.director.loadScene('Battle', (error) => {
        if (error) {
            cc.error('load Battle failed', error);
            return;
        }

        cc.log('Battle is ready');
    });
}

真实项目还应根据版本和平台补充:

  • 加载进度。
  • 重复点击保护。
  • 失败重试或回退。
  • 旧请求取消或过期检查。
  • 加载界面本身的常驻策略。

不要让多个按钮同时发起不可控的场景切换请求。


十、常见误区 ​

误区一:Scene 文件就是运行时 Scene ​

文件是序列化描述,运行时 Scene 是反序列化后的对象。

误区二:切场景只会替换一个根节点 ​

它还涉及资源、对象创建、生命周期、常驻对象和渲染状态。

误区三:切场景后旧场景所有东西立即释放 ​

外部引用、事件、定时器和资源缓存都可能让对象继续存活。

误区四:所有全局系统都应该做成常驻节点 ​

常驻是生命周期设计,不是方便取引用的快捷方式。

误区五:在 onLoad 里做所有初始化最安全 ​

大量同步工作会拉长场景进入时间。应区分基础初始化、依赖其他对象的初始化和可延迟工作。


十一、练习与答案 ​

练习 ​

  1. Scene 资源文件和运行时 Scene 有什么区别?
  2. 为什么切场景可能同时带来 IO、CPU、GPU 和内存成本?
  3. 什么对象适合做常驻节点?
  4. 场景已经切换,但旧场景内存没有下降,应检查哪些引用?
  5. 为什么 loadScene 的回调比调用下一行更适合判断加载完成?

参考答案 ​

  1. 资源文件是序列化描述,运行时 Scene 是引擎创建并管理的对象。
  2. 切换会读取和解码资源、反序列化、创建节点、执行生命周期、上传纹理并清理旧对象。
  3. 真正跨场景共享且生命周期明确的服务,如网络、音频或用户会话管理器。
  4. 检查全局引用、事件、定时器、闭包、异步回调和对象池。
  5. 场景切换通常包含异步加载和对象创建,调用下一行不代表新场景已完成初始化。

十二、Creator 2.4.x 可复现实验:切场景时间线 ​

实验代码 ​

在 A、B 两个场景根节点挂载同一个探针脚本:

ts
const { ccclass } = cc._decorator;

@ccclass
export default class SceneProbe extends cc.Component {
    onLoad() { cc.log('[scene]', cc.director.getScene().name, 'onLoad'); }
    onEnable() { cc.log('[scene]', cc.director.getScene().name, 'onEnable'); }
    start() { cc.log('[scene]', cc.director.getScene().name, 'start'); }
    onDestroy() { cc.log('[scene]', this.node.name, 'onDestroy'); }
}

在 A 中调用:

ts
const t0 = Date.now();
cc.director.loadScene('SceneB', () => {
    cc.log('[scene-switch-done]', Date.now() - t0, 'ms', cc.director.getScene().name);
});

观察与诊断 ​

预期能看到 A 的清理日志、B 的创建/激活日志以及最终回调。若切换卡顿,分别在请求前、加载回调、B 首个 start 和首帧 update 打时间戳;这样可以区分 IO、反序列化、生命周期和首次渲染成本。若旧场景内存不降,优先查常驻节点、全局事件、定时器、闭包、对象池和静态缓存。

十三、版本边界 ​

Creator 2.4.x 中 cc.director.loadScene 的异步回调是判断切换完成的主要公开入口。场景资源的具体缓存、释放时机、加载事件和内部过渡实现可能随 2.4.x 小版本或平台改变;课程不依赖这些内部顺序。不要直接修改 .fire/.scene 文件来做实验,使用编辑器创建和 Build Settings 配置场景。

十四、应用题与推导答案 ​

题目: B 场景已经显示,但内存比切换前高很多,如何判断是正常峰值还是泄漏?

答案: 先记录 A 常驻资源、B 加载峰值和清理后稳定值,连续往返 A/B 三次。若每次稳定值都接近同一基线,通常是加载峰值或缓存;若基线单调增长,再用引用链定位全局变量、事件监听、异步回调和对象池。destroy 节点只结束对象生命周期,不等于所有资源引用立即释放。

十五、本课总结 ​

text
Scene 文件是序列化数据。
运行时 Scene 是当前节点树的容器。
Director 负责切换并推进当前 Scene。
场景切换同时影响资源、对象、生命周期和内存。
常驻节点是生命周期选择,不是万能单例。

十六、下一课预告 ​

text
Lesson015|Scheduler 和 ComponentScheduler 分别负责什么?

下一课会把“每帧任务为什么能在正确时间执行”拆成 Scheduler 和 ComponentScheduler 两条线。