外观
Stage03|Runtime Objects & Assets
Lesson022|Scene 和 Prefab 的序列化数据如何还原成运行时对象?
Tags: #Creator2.x #Stage03 #Scene #Prefab #Serialization #RuntimeObjectDifficulty: ⭐⭐⭐⭐☆
一、本课从一个现象开始
你在 Creator 编辑器里制作了一个商城条目:
text
ShopItem
├── Icon
│ └── Sprite
├── Name
│ └── Label
├── Price
│ └── Label
└── BuyButton
└── Button根节点还挂了一个脚本:
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class ShopItemView extends cc.Component {
@property(cc.Sprite)
icon: cc.Sprite = null;
@property(cc.Label)
nameLabel: cc.Label = null;
@property(cc.Label)
priceLabel: cc.Label = null;
onLoad() {
cc.log('ShopItemView onLoad', this.node.name);
}
}你把三个组件拖到 Inspector 属性里,保存为 ShopItem.prefab。
运行时只写:
ts
const node = cc.instantiate(this.shopItemPrefab);
this.content.addChild(node);一个完整的 Node 子树就出现了,组件引用也已经恢复,onLoad 还会自动执行。
这不是“Prefab 文件自己在运行”。真正的问题是:
编辑器保存的结构化数据,如何重新变成有类、有引用、有生命周期的 Node 和 Component?
二、本课目标
完成本课后,你应该能够:
- 区分 Scene/Prefab 资源文件、资源对象和运行时实例。
- 解释序列化和反序列化各自解决什么问题。
- 画出从加载资源到创建 Node/Component 的完整数据流。
- 解释组件属性引用为什么能在实例化后自动恢复。
- 区分“对象已经创建”和“生命周期已经执行”。
- 分析 Missing Script、引用为 null、Prefab 实例状态错误等问题。
- 在不手工修改
.scene、.prefab和.meta的前提下完成验证实验。
三、先区分三个完全不同的东西
1. 编辑器中的资源文件
Scene 和 Prefab 文件保存的是结构化描述,例如:
text
有哪些对象
对象是什么类型
父子关系是什么
组件挂在哪个 Node 上
属性值是什么
属性引用指向谁
引用了哪些外部资源它们不是已经运行的 Node,也不会自己执行 update。
2. 加载后的资源对象
运行时加载 Prefab 后,得到的是一个 cc.Prefab 资源对象:
ts
@property(cc.Prefab)
shopItemPrefab: cc.Prefab = null;它代表“可以用于创建实例的模板资源”,还不是场景中的 ShopItem Node。
3. 运行时实例
调用:
ts
const itemNode = cc.instantiate(this.shopItemPrefab);得到的才是运行时 Node 子树。每次实例化通常产生新的 Node 和 Component 实例:
text
ShopItem.prefab 资源
├── instantiate → ShopItem 实例 A
├── instantiate → ShopItem 实例 B
└── instantiate → ShopItem 实例 C三个实例可以拥有不同的价格、名字、位置和 active 状态,但可能共享同一份 Texture、SpriteFrame 或其他资源。
四、什么是序列化
序列化解决的问题是:
如何把编辑器内存中的对象关系,保存为以后可以恢复的数据?
编辑器中的 ShopItem 不是一张扁平表,而是对象图:
text
ShopItem Node
├── children → Icon Node
├── children → Name Node
├── children → Price Node
├── children → BuyButton Node
└── components
└── ShopItemView
├── icon → Icon 上的 Sprite
├── nameLabel → Name 上的 Label
└── priceLabel → Price 上的 Label序列化需要保存:
text
对象类型
普通属性值
对象之间的引用
父子顺序
资源引用
Prefab 相关信息保存的是“如何恢复对象”的信息,不是 JavaScript 函数执行现场。
下面这些运行时内容通常不应该直接当作可持久化配置:
- 当前网络连接。
- 一个临时 Promise。
- 闭包函数。
- 当前帧的缓存数组。
- 已经创建的原生句柄。
- 运行时事件监听器。
五、什么是反序列化
反序列化是序列化的反方向:
text
读取结构化数据
↓
识别对象类型
↓
创建空的对象实例
↓
恢复普通属性
↓
恢复对象内部引用
↓
连接外部资源引用
↓
形成完整对象图这里最容易误解的是顺序。
假设 ShopItemView.icon 指向 Icon 节点上的 Sprite。如果 ShopItemView 创建时 Icon/Sprite 还没有准备好,不能立即完成引用。引擎可以先创建对象,再通过序列化索引或引用描述统一恢复关系。
高层模型可以写成:
text
第一遍:创建对象
Node #0
Node #1
Sprite #2
ShopItemView #3
第二遍:恢复关系
Node #0.children → Node #1
Node #1.components → Sprite #2
ShopItemView #3.icon → Sprite #2这只是帮助理解的模型,不代表 Creator 2.4.x 每一行源码都严格使用“两遍循环”。稳定结论是:对象引用需要在目标对象能够被识别后恢复。
六、从 Scene 文件到运行场景
当 Director 加载 Scene 时,可以先按下面的链路理解:
text
Director 请求加载 Scene
↓
资源系统找到 SceneAsset 和依赖
↓
加载并解析序列化数据
↓
创建 Scene / Node / Component 对象
↓
恢复父子关系和组件属性引用
↓
把新 Scene 接入 Director
↓
激活节点树
↓
执行 onLoad / onEnable / start
↓
进入 mainLoop注意三个边界:
text
资源加载完成
≠ 运行时对象全部激活
≠ 所有组件 start 已执行所以调用 loadScene 后,不能在下一行同步代码中假定新场景所有业务对象都已运行。
七、从 Prefab 资源到运行时实例
Prefab 的链路和 Scene 有相似之处,但职责不同:
text
加载 cc.Prefab 资源
↓
Prefab 模板数据驻留在资源系统
↓
cc.instantiate(prefab)
↓
克隆 Node / Component 对象图
↓
恢复实例内部引用
↓
得到尚未或已经接入场景树的 Node
↓
根据父节点和 active 状态进入生命周期Scene 主要描述一张完整运行场景,Prefab 主要描述可重复实例化的 Node 子树。
text
Scene:Director 当前管理的场景对象树
Prefab:用于生成 Node 子树实例的资源模板八、组件属性引用是怎么恢复的
Inspector 中拖拽:
text
ShopItemView.icon → Icon/Sprite编辑器保存的不是 JavaScript 内存地址。内存地址在下次启动时没有意义。
它保存的是能够在对象图中重新定位目标的信息。运行时恢复后:
ts
cc.log(this.icon === this.node.getChildByName('Icon').getComponent(cc.Sprite));预期输出:
text
true这说明 Inspector 引用恢复到了实例内真正的 Sprite 组件。
内部引用与外部资源引用
内部引用:
text
ShopItemView → Prefab 内部的 Label / Sprite / Node外部资源引用:
text
Sprite → assets 中的 SpriteFrame
Animation → AnimationClip
脚本属性 → AudioClip / Prefab / JsonAsset内部对象通常在实例化时被克隆并重新连接;外部 Asset 通常由多个实例共享,而不是每次复制一份纹理。
九、对象创建和生命周期不是同一步
设想:
ts
const item = cc.instantiate(this.shopItemPrefab);
cc.log('after instantiate');
this.content.addChild(item);需要区分:
text
对象实例已经存在
节点是否已经进入活动场景树
activeInHierarchy 是否为 true
onLoad 是否已经触发
onEnable 是否已经触发
start 是否已经触发生命周期时机受到:
- 实例是否已经挂到运行场景。
- 父节点是否 activeInHierarchy。
- Prefab 根节点自身 active。
- 当前引擎处于哪个激活/调度阶段。
影响。
稳定的工程做法是:
text
构造和引用恢复由引擎负责
业务数据通过显式 init(data) 注入
每次启用逻辑放 onEnable 或 open()
不要依赖模糊的“实例化后一定立刻 start”十、贯穿案例:商城条目完整运行链
1. 编辑器阶段
text
创建 ShopItem 节点树
→ 挂 Sprite / Label / Button / ShopItemView
→ 拖拽 icon、nameLabel、priceLabel 引用
→ 保存为 ShopItem.prefab
→ AssetDB 导入并维护资源身份2. 运行时加载
text
商城页面加载 ShopItem.prefab
→ 得到 cc.Prefab 资源
→ 依赖资源进入缓存或可用状态3. 实例化
text
cc.instantiate
→ 创建新的 Node / Component 图
→ 恢复 ShopItemView 到三个组件的引用
→ 外部 SpriteFrame 继续使用资源对象4. 接入场景和业务初始化
ts
const itemNode = cc.instantiate(this.shopItemPrefab);
const itemView = itemNode.getComponent(ShopItemView);
itemView.init(data);
this.content.addChild(itemNode);建议 init 只处理业务数据,不重复查找固定组件:
ts
init(data: ShopItemData) {
this.nameLabel.string = data.name;
this.priceLabel.string = String(data.price);
}5. 后续运行
text
进入 activeInHierarchy
→ 生命周期和事件注册
→ Layout / Renderer 读取状态
→ 用户看到并点击 ShopItem十一、编辑器验证实验
本实验只通过 Cocos Creator 编辑器创建和修改资源。不要用文本编辑器修改
.scene、.prefab、.meta,不要手工编写 UUID。
实验目标
验证三件事:
- Prefab 是模板资源,不是运行时实例。
- 多个实例拥有独立 Node/Component。
- 实例内部组件引用能被自动恢复。
实验步骤
- 在 Creator 2.4.x 编辑器中新建空场景
SerializationLab。 - 新建节点
ShopItem,下面创建Icon、Name、Price。 - 给 Icon 挂 Sprite,Name 和 Price 挂 Label。
- 给 ShopItem 挂
ShopItemView脚本。 - 在 Inspector 中把 Sprite 和两个 Label 拖入脚本属性。
- 在编辑器中把 ShopItem 拖到 Assets 面板,生成一个新的 Prefab。
- 新建
LabController,通过@property(cc.Prefab)绑定该 Prefab。 - 在
start中实例化两个节点并加入 Canvas。 - 分别修改两个实例的名称、位置和 Label 内容。
- 运行预览,观察日志和画面。
实验代码
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class LabController extends cc.Component {
@property(cc.Prefab)
shopItemPrefab: cc.Prefab = null;
start() {
const first = cc.instantiate(this.shopItemPrefab);
const second = cc.instantiate(this.shopItemPrefab);
first.name = 'ShopItem-A';
second.name = 'ShopItem-B';
first.x = -180;
second.x = 180;
const firstView = first.getComponent('ShopItemView') as any;
const secondView = second.getComponent('ShopItemView') as any;
firstView.nameLabel.string = 'Potion';
secondView.nameLabel.string = 'Sword';
this.node.addChild(first);
this.node.addChild(second);
cc.log('different node', first !== second);
cc.log('different component', firstView !== secondView);
cc.log('first refs', !!firstView.icon, !!firstView.nameLabel, !!firstView.priceLabel);
cc.log('second refs', !!secondView.icon, !!secondView.nameLabel, !!secondView.priceLabel);
}
}如果项目使用脚本类引用而不是字符串 getComponent,优先采用项目现有 TypeScript 导入方式。示例使用 any 只是让重点集中在对象关系,不代表推荐的业务类型写法。
预期结果
text
画面出现两个位置不同的 ShopItem
第一个显示 Potion
第二个显示 Sword
different node true
different component true
两个实例的三个引用均为 true观察点
- 修改 first 的 Label 不会改变 second 的 Label。
- 两个实例的 ShopItemView 不是同一个对象。
- Inspector 绑定的内部引用在两个实例内分别指向各自子节点。
- 如果两者使用同一 SpriteFrame,它们可以共享 Asset,而不共享 Sprite Component。
十二、实验失败时怎么排查
现象一:Prefab 属性为 null
排查:
text
LabController 是否挂到场景节点
→ Inspector 是否绑定了 Prefab
→ 属性类型是否为 cc.Prefab
→ 脚本是否编译成功现象二:ShopItemView 找不到
排查:
text
脚本是否挂在 Prefab 根节点
→ 类名和文件名是否符合项目规则
→ 控制台是否有脚本编译错误
→ Prefab 是否保存了最新修改现象三:nameLabel 为 null
排查:
text
Inspector 是否拖拽了正确 Label 组件
→ 拖入的是 Node 还是 Label Component
→ Prefab 是否 Apply / 保存
→ 运行时实例是否来自正确 Prefab现象四:修改一个实例,另一个也变化
先判断变化的是:
text
实例组件属性
还是共享 Asset 内容两个实例修改各自 Label.string 应相互独立;如果直接修改共享 SpriteFrame/Material/Texture 资源对象,则可能影响所有使用者。
十三、真实项目故障:Prefab 更新后线上 Missing Script
现象
编辑器内某些 Prefab 显示 Missing Script,实例化后业务组件不存在。
可能原因
- 脚本移动或重命名时资源导入没有完成。
- 脚本编译失败,类无法注册。
- 冲突解决时误删或重建
.meta,资源身份改变。 - 分支合并出现无关
.meta大面积重写。 - Prefab 保存时组件已经丢失。
错误修复
text
手工打开 .prefab 搜索并替换 UUID
手工复制另一个脚本的 .meta
修改 library/ 中生成的数据这些做法会进一步破坏引用关系。
正确排查
text
先修复脚本编译错误
→ 在 Creator 编辑器中确认资源导入完成
→ 检查 Git 中脚本和编辑器生成的 .meta 是否成对移动
→ 确认没有 UUID 冲突
→ 在 Inspector 中重新绑定真正丢失的组件
→ 保存 Prefab 并重新预览对于序列化资源,编辑器和 AssetDB 是事实入口,不应把内部文件格式当作稳定公开 API 手工维护。
十四、常见误区
误区一:Prefab 就是一段普通 JSON
即使底层文本看起来像结构化数据,它仍包含引擎对象类型、索引、UUID、内部引用和版本约定。不能按普通业务 JSON 随意编辑。
误区二:加载 Prefab 后已经得到可显示 Node
加载得到的是 Prefab Asset;还需要 cc.instantiate 创建 Node 实例,并接入场景树。
误区三:instantiate 会复制所有外部图片和纹理
它主要复制 Prefab 的对象图。SpriteFrame、Texture 等 Asset 通常由实例共享。
误区四:组件构造完成后 Inspector 引用一定立即可用
构造、反序列化赋值、引用恢复和生命周期属于不同阶段。业务初始化应在 Creator 支持的生命周期或显式方法中进行。
误区五:Prefab 内部引用可以使用内存地址保存
内存地址下次运行会变化,序列化必须使用可恢复的对象索引或资源身份。
误区六:对象创建完成就等于 start 已执行
是否进入生命周期还与场景树、activeInHierarchy 和当前调度阶段有关。
十五、练习
A. 概念题
- Scene 文件、SceneAsset 和运行时 Scene 有什么区别?
cc.Prefab和cc.instantiate(prefab)返回的对象有什么区别?- 为什么组件引用不能保存为内存地址?
- Prefab 的两个实例为什么能共享 Texture,却不能共享同一个 Label Component?
B. 应用题
有一个 Enemy.prefab:
text
Enemy
├── Body/Sprite
├── HpBar/ProgressBar
└── EnemyView
├── bodySprite → Body/Sprite
└── hpBar → HpBar/ProgressBar实例化 100 个 Enemy 后:
- 哪些对象通常有 100 份?
- 哪些资源可以只有一份并被共享?
- 如果其中一个 Enemy 修改
hpBar.progress,是否应该影响其他实例? - 如果直接修改共享 Material Asset,可能发生什么?
C. 故障诊断题
现象:Prefab 在编辑器里引用正常,但运行时 nameLabel 为 null。
请按优先级给出至少五步排查流程,不允许手改 Prefab 文件或 .meta。
十六、练习参考答案
A. 概念题答案
- Scene 文件是序列化描述;SceneAsset 是资源系统中的场景资源对象;运行时 Scene 是反序列化并由 Director 管理的对象树。
cc.Prefab是模板 Asset;cc.instantiate返回新创建的 Node/Component 实例图。- 内存地址只在当前进程当前时刻有效,无法跨保存、构建和下次运行恢复。
- Label Component 属于实例对象图,每个实例需要独立状态;Texture 是外部资源,多个实例可以只读共享以节省内存。
B. 应用题答案
- Enemy Node、子节点、Sprite Component、ProgressBar Component、EnemyView Component 通常各有 100 份。
- SpriteFrame、Texture、AnimationClip、Prefab Asset 等资源可以共享,具体取决于是否在运行时克隆或修改。
- 不应该。每个实例的 ProgressBar Component 是独立对象。
- 如果多个实例引用同一 Material Asset,修改共享材质状态可能影响所有使用者;需要判断是否应创建材质实例。
C. 故障诊断答案
推荐顺序:
text
1. 查看控制台是否有脚本编译或反序列化错误。
2. 确认当前实例来自预期 Prefab,而不是旧缓存或另一个同名资源。
3. 在 Creator Inspector 中打开 Prefab,确认属性绑定的是 Label Component。
4. 保存 Prefab,等待 AssetDB 导入完成后重新预览。
5. 检查代码是否在 onLoad 之前或构造阶段提前覆盖了 nameLabel。
6. 检查脚本类、属性名或类型是否曾重命名,导致旧序列化字段无法恢复。
7. 检查 Git 变更,确认脚本和编辑器生成的 .meta 没有被错误删除、复制或冲突覆盖。这个顺序先排编译和资源选择,再排绑定和代码覆盖,最后排资源身份问题,能够减少盲目重绑和破坏引用的风险。
十七、本课总结
本课最重要的数据流是:
text
编辑器对象图
↓ 序列化
Scene / Prefab 资源数据
↓ AssetDB / Loader
资源对象
↓ 反序列化或 instantiate
Node / Component 运行时对象图
↓ 引用恢复
完整实例
↓ 接入 active 场景树
生命周期和 Engine Loop最终记住:
text
资源文件不是运行时对象。
Prefab Asset 不是 Prefab 实例。
反序列化不仅创建对象,还要恢复对象之间的引用。
实例内部组件独立,外部 Asset 可以共享。
对象创建、节点激活和生命周期执行是不同阶段。十八、下一课预告
text
Lesson023|UUID、.meta、AssetDB 和属性引用是什么关系?下一课会回答:
text
资源移动后引用为什么还能存在?
.meta 里为什么需要 UUID?
为什么不能复制旧 .meta 给新资源?
为什么修改 assets 后必须等待 AssetDB 导入?