Skip to content

Stage03|Runtime Objects & Assets ​

Lesson025|Prefab 实例、属性覆盖与组件引用 ​

Tags: #Creator2.x #Stage03 #PrefabInstance #Override #Apply #ReferenceDifficulty: ⭐⭐⭐⭐☆


一、本课从一个“为什么没同步”开始 ​

项目里有一个通用按钮 Prefab:

text
CommonButton
├── Background/Sprite
├── Title/Label
└── CommonButtonView

场景里放了三个实例:

text
BuyButton:Title = 购买
SellButton:Title = 出售
CloseButton:Title = 关闭

美术修改 Prefab,把 Background 的颜色从蓝色改为绿色。结果:

  • BuyButton 变成绿色。
  • SellButton 仍然是蓝色。
  • CloseButton 的尺寸也没有同步。

这不是 Creator 随机失效,而是 Prefab 默认值和实例覆盖值之间的合并结果。

本课要回答:

Prefab 实例如何继承模板,又如何保留自己的差异?


二、本课目标 ​

完成本课后,你应该能够:

  1. 区分 Prefab 模板默认值、实例值和 Override。
  2. 解释为什么模板修改不一定覆盖所有实例属性。
  3. 理解 Apply、Revert 和 Unlink/解除关联一类操作的语义边界。
  4. 设计 Prefab 内部引用和外部业务数据的合理边界。
  5. 识别跨 Scene 引用、脚本字段重命名和 Prefab 冲突风险。
  6. 通过编辑器实验验证哪些属性跟随模板、哪些由实例保留。

三、Prefab 模板和实例是什么关系 ​

Prefab Asset 提供一套默认对象图:

text
CommonButton.prefab
├── size = 240 × 80
├── color = blue
├── Title.string = Button
└── CommonButtonView.title → Title/Label

场景中的 BuyButton 是这个模板的实例:

text
BuyButton Instance
├── 继承 size = 240 × 80
├── 继承 color = blue
├── 覆盖 Title.string = 购买
└── 继承组件引用

可以把最终值理解成:

text
实例最终值 = Prefab 默认值 + 实例 Override

这只是概念模型,不代表实际文件用简单对象合并实现。


四、什么是 Override ​

当你在场景中的 Prefab 实例上修改某个可序列化属性,编辑器需要记住:

text
这个实例不再使用模板的该属性值
而是使用实例自己的值

例如:

text
Prefab:Title.string = Button
BuyButton Override:Title.string = 购买
SellButton Override:Title.string = 出售

之后模板把默认文字改成“确认”:

text
没有覆盖的实例 → 可能跟随为 确认
已有文字 Override → 继续保留 购买 / 出售

Override 的目的就是允许“一套结构,多个实例差异”。


五、为什么模板修改没有同步 ​

出现不同步时按顺序判断:

text
属性是否在实例上被修改过?
→ 是否形成 Override?
→ 修改的是 Prefab Asset 还是某个实例?
→ 是否执行了 Apply 或保存?
→ 编辑器是否完成资源导入?
→ 实例是否仍保持 Prefab 关联?

不要立即删除实例重建。删除重建虽然可能恢复模板值,却会同时丢失合法的实例差异和场景引用。


六、Apply 的语义 ​

在 Prefab 实例上修改属性后,Apply 通常表达:

把当前实例的某些修改写回 Prefab 模板,让其他实例以后也能继承。

例如在 BuyButton 实例上把按钮高度从 80 改成 100,然后 Apply:

text
BuyButton Instance.height = 100
    ↓ Apply
CommonButton Prefab.height = 100
    ↓
其他没有 height Override 的实例同步为 100

风险是:BuyButton 的业务特例可能被误当成通用模板修改。

Apply 前要问:

text
这是所有按钮都需要的修改?
还是只有 BuyButton 的场景差异?

七、Revert 的语义 ​

Revert 通常表达:

放弃实例自己的覆盖,让该属性重新使用 Prefab 默认值。

text
SellButton.color Override = blue
Prefab.color = green
    ↓ Revert SellButton.color
SellButton.color = green

Revert 不是从磁盘恢复整个项目,也不是撤销所有业务数据。它针对 Prefab 关联和实例覆盖关系。

操作名称和可用粒度可能因 Creator 2.x 小版本不同,应以当前编辑器 Inspector/Prefab 工具为准。


八、解除 Prefab 关联意味着什么 ​

如果一个实例与 Prefab 解除关联:

text
当前 Node / Component 仍可留在场景
但不再自动继承 Prefab 后续修改

这适合实例已经演变成独立对象的情况,但会失去模板统一维护优势。

不要把“同步不符合预期”一律用解除关联解决。否则项目里会出现大量外观相同、实际无法统一维护的孤立节点树。


九、组件引用为什么属于结构契约 ​

Prefab 脚本:

ts
@property(cc.Label)
titleLabel: cc.Label = null;

@property(cc.Sprite)
background: cc.Sprite = null;

这些引用表达组件对 Prefab 内部结构的依赖:

text
CommonButtonView
需要 Title/Label
需要 Background/Sprite

如果重构 Prefab 时删除 Title 节点,却不更新脚本绑定,运行时就会出现 null。

结构契约应满足:

  • 引用目标位于 Prefab 内部。
  • 类型明确。
  • 重命名和移动节点后在编辑器验证绑定。
  • 不在每帧使用 getChildByName 重新猜结构。

十、为什么 Prefab 不应依赖外部 Scene Node ​

假设 Prefab 内的脚本引用场景里的 GameManager Node:

text
Scene/GameManager
↑
Enemy.prefab/EnemyController.manager

这个引用难以成为可复用 Prefab 的稳定组成:

  • 在另一个 Scene 中目标不存在。
  • 动态 instantiate 时外部对象尚未创建。
  • Prefab 独立预览时引用失效。
  • 场景切换后引用可能过期。

更好的方式:

显式注入 ​

ts
const enemy = cc.instantiate(this.enemyPrefab);
const controller = enemy.getComponent(EnemyController);
controller.init(enemyData, this.gameService);

事件或服务查询 ​

适用于真正全局且生命周期明确的服务,但要避免隐藏依赖和无法测试的单例。

原则:

text
Prefab 内部结构引用 → Inspector 绑定
外部业务依赖 → 显式注入或明确服务边界

十一、序列化字段重命名的风险 ​

原脚本:

ts
@property(cc.Label)
titleLabel: cc.Label = null;

直接改名:

ts
@property(cc.Label)
captionLabel: cc.Label = null;

旧 Prefab 保存的是原字段对应的数据。字段重命名后,编辑器不一定自动知道两者是同一业务属性,可能表现为:

text
captionLabel = null
旧序列化数据残留或被忽略

安全迁移要考虑:

  • 当前 Creator 版本是否提供字段迁移/原名称支持。
  • 是否需要在编辑器中重新绑定。
  • 所有 Prefab/Scene 是否都已升级并保存。
  • 分支合并是否会把新旧序列化状态混合。

不要手工批量替换 .prefab 内部字段来绕开编辑器验证。


十二、运行时修改和编辑器 Override 不是一回事 ​

运行时代码:

ts
this.titleLabel.string = '购买成功';

这只是修改当前运行实例的内存状态,通常不会自动写回 Prefab Asset,也不会形成编辑器里可保存的场景 Override。

text
编辑器实例修改 → 可序列化并可能形成 Override
运行时实例修改 → 当前进程中的状态变化

不要指望运行时把用户操作“保存回 Prefab”。用户数据应写入业务存档、数据库或配置系统。


十三、贯穿案例:CommonButton 的正确设计 ​

Prefab 保存通用结构和视觉默认值 ​

text
尺寸
背景 Sprite
通用字体
点击组件
内部 Label/Sprite 引用
公共动画节点

场景实例保存少量展示差异 ​

text
BuyButton.title = 购买
SellButton.title = 出售
CloseButton.title = 关闭

运行时注入业务行为 ​

ts
buyButtonView.configure({
    title: '购买',
    onClick: () => this.buyItem(itemId),
});

不应该写入 Prefab 的内容 ​

text
当前玩家余额
当前商品 ID
当前网络请求
场景中的 ShopManager Node 引用
上一次点击状态

这样模板、实例差异和运行时数据各有明确边界。


十四、编辑器验证实验 ​

所有 Prefab 修改都通过 Creator 2.4.x 编辑器完成。不要手工修改 .prefab、.scene 或 .meta。

实验目标 ​

验证:

  1. 未覆盖属性会跟随模板。
  2. 已覆盖属性保留实例值。
  3. Revert 可以重新使用模板值。
  4. Apply 会改变模板并影响其他未覆盖实例。

准备 ​

  1. 创建 CommonButton 节点,尺寸设为 240×80。
  2. 添加 Background/Sprite 和 Title/Label。
  3. Title 默认文字设为 Button。
  4. 保存为 CommonButton.prefab。
  5. 在 PrefabOverrideLab 场景中放入三个实例。
  6. 把三个实例的 Title 分别改为购买、出售、关闭。
  7. 只把 SellButton 的 Background 颜色改为蓝色,形成颜色差异。

实验 A:修改模板默认颜色 ​

  1. 打开 Prefab 编辑模式。
  2. 把 Background 默认颜色改为绿色。
  3. 保存 Prefab。
  4. 回到场景观察三个实例。

预期观察:

text
没有颜色 Override 的实例跟随模板变绿
SellButton 如果保存了颜色 Override,则继续保留蓝色
三个实例自己的 Title 文本继续保留

实验 B:Revert ​

  1. 在 SellButton 上找到被覆盖的颜色属性。
  2. 使用当前 Creator 版本提供的 Revert/恢复 Prefab 值操作。
  3. 观察颜色。

预期:SellButton 重新使用 Prefab 的绿色。

实验 C:Apply ​

  1. 在 BuyButton 实例上把高度改为 100。
  2. 明确确认这是要推广到所有按钮的通用修改。
  3. 使用 Apply 把高度写回 Prefab。
  4. 观察其他没有高度 Override 的实例。

预期:它们同步到高度 100。

实验 D:运行时修改 ​

在脚本中运行:

ts
start() {
    this.buyTitle.string = '运行时文字';
}

停止预览后观察场景和 Prefab。

预期:运行时修改不会自动写回 Prefab 默认值。


十五、实验失败时怎么排查 ​

所有实例都不跟随模板 ​

text
实例是否仍与 Prefab 关联
→ 修改是否保存到正确 Prefab Asset
→ 属性是否都已经形成 Override
→ AssetDB 是否完成更新

Apply 后不该改变的实例也变化 ​

说明修改实际上被写成了模板默认值,而那些实例没有对应 Override。应重新判断这是不是通用修改;必要时恢复模板并在特定实例上设置差异。

组件引用变成 null ​

text
Prefab 内部目标节点/组件是否被删除
→ 属性类型或字段名是否改变
→ Inspector 是否仍绑定
→ 脚本是否编译成功
→ Prefab 是否保存

十六、真实项目故障:Apply 了活动页特例 ​

现象 ​

活动页为了做春节样式,把一个 CommonButton 实例的字体、颜色、动画和尺寸全部调整,然后点击 Apply。第二天全项目几十个按钮样式改变。

根因 ​

把“单个业务页面特例”写回了“全局通用模板”。

恢复思路 ​

text
通过版本控制确认 Prefab Asset 的实际修改
→ 恢复通用模板到正确状态
→ 在活动页面实例保留 Override
或创建明确的新变体 Prefab
→ 检查其他实例是否存在合法 Override

设计改进 ​

如果差异已经涉及结构、动画和主题,不要在一个实例上堆大量 Override。可以建立:

text
CommonButton 基础 Prefab
SpringButton 独立变体 Prefab
运行时主题配置

具体采用继承、嵌套或独立 Prefab 要根据 Creator 2.x 能力和团队工作流决定,不能只追求少建一个资源。


十七、版本控制和协作 ​

Prefab 是序列化资源,多个开发者同时修改同一 Prefab 容易产生冲突。

建议:

  • 大改前明确资源负责人。
  • 一个提交只处理明确的 Prefab 变更。
  • 不混入大量无关 .meta 重写。
  • 冲突解决后必须用 Creator 打开并检查实例。
  • 等待 AssetDB 导入完成再预览和提交。
  • 检查没有 library/、temp/、local/、build/ 进入提交。

不要只依据文本冲突标记“能合并”,还要验证对象结构和引用语义。


十八、常见误区 ​

误区一:Prefab 修改一定覆盖所有实例 ​

实例 Override 会保留自己的值。

误区二:不同步说明 Prefab 坏了 ​

先检查 Override、关联状态、保存和 AssetDB。

误区三:Apply 只是保存当前实例 ​

Apply 可能把实例差异写回模板,影响其他实例。

误区四:Revert 会删除实例 ​

它主要恢复 Prefab 默认关系,不等于销毁 Node。

误区五:运行时修改会写回 Prefab ​

运行时状态和编辑器序列化状态是不同系统。

误区六:Prefab 引用场景外部 Node 更方便 ​

它会破坏复用边界和生命周期,应使用显式依赖注入或服务边界。

误区七:大量 Override 没有维护成本 ​

Override 越多,模板升级行为越难预测;结构性差异应考虑独立变体。


十九、练习 ​

A. 概念题 ​

  1. Prefab 默认值和实例 Override 如何共同决定最终值?
  2. Apply 和 Revert 的方向分别是什么?
  3. 为什么运行时修改不会自动写回 Prefab?
  4. Prefab 内部组件引用和外部业务依赖应该如何处理?

B. 设计题 ​

有一个通用商品卡片 Prefab,三个页面分别需要:

text
商城:价格、购买按钮
仓库:数量、使用按钮
奖励预览:掉落概率、无按钮

请判断哪些内容适合放通用 Prefab,哪些适合实例 Override,哪些差异已经值得创建独立变体。

C. 故障诊断题 ​

模板把按钮宽度从 200 改成 260 后,场景里 20 个实例中只有 12 个同步。请给出排查流程,不允许通过删除重建全部实例解决。


二十、练习参考答案 ​

A. 概念题答案 ​

  1. 没有覆盖的属性使用 Prefab 默认值,有 Override 的属性使用实例值。
  2. Apply 从实例把修改写回模板;Revert 从模板恢复实例属性。
  3. 运行时修改只改变当前进程对象,不属于编辑器保存流程。
  4. Prefab 内部 Node/Component 用 Inspector 稳定绑定;外部业务服务和数据通过显式 init/configure 或清晰的服务接口注入。

B. 设计题答案 ​

通用 Prefab 可以保存:

text
卡片基本尺寸、背景、图标、标题和基础排版

实例或运行时配置可以保存:

text
具体标题、价格、数量、概率、按钮文字

如果三个页面的节点结构和交互差异持续扩大,例如一个有价格区、一个有数量区、一个没有按钮并有概率布局,可以使用共享基础组件加三个明确变体,而不是给一个 Prefab 堆大量 active/Override 条件。

C. 故障诊断答案 ​

text
1. 确认修改保存到了正确 Prefab Asset。
2. 检查未同步的 8 个实例是否仍保持 Prefab 关联。
3. 查看 width 是否在这些实例上形成 Override。
4. 判断 Override 是合法页面差异还是历史误操作。
5. 对应合法差异继续保留;误操作使用 Revert 恢复模板值。
6. 检查嵌套父节点、Widget/Layout 是否在运行时再次改变宽度。
7. 等待 AssetDB 完成并预览验证。

删除重建会丢失实例业务配置,无法解释真正原因。


二十一、本课总结 ​

text
Prefab 提供默认结构和属性。
实例通过 Override 保存自己的差异。
Apply 把实例修改推向模板。
Revert 让实例重新使用模板值。
Prefab 内部引用属于结构契约。
外部业务依赖应该显式注入。
运行时状态不会自动写回编辑器资源。

二十二、下一课预告 ​

text
Lesson026|场景切换、常驻节点与对象生命周期

下一课会把对象放入更长的时间线:

text
当前 Scene
→ loadScene
→ 旧对象退出
→ 常驻节点保留
→ 新 Scene 激活
→ 全局服务和事件如何清理