外观
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 实例如何继承模板,又如何保留自己的差异?
二、本课目标
完成本课后,你应该能够:
- 区分 Prefab 模板默认值、实例值和 Override。
- 解释为什么模板修改不一定覆盖所有实例属性。
- 理解 Apply、Revert 和 Unlink/解除关联一类操作的语义边界。
- 设计 Prefab 内部引用和外部业务数据的合理边界。
- 识别跨 Scene 引用、脚本字段重命名和 Prefab 冲突风险。
- 通过编辑器实验验证哪些属性跟随模板、哪些由实例保留。
三、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 = greenRevert 不是从磁盘恢复整个项目,也不是撤销所有业务数据。它针对 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。
实验目标
验证:
- 未覆盖属性会跟随模板。
- 已覆盖属性保留实例值。
- Revert 可以重新使用模板值。
- Apply 会改变模板并影响其他未覆盖实例。
准备
- 创建
CommonButton节点,尺寸设为240×80。 - 添加 Background/Sprite 和 Title/Label。
- Title 默认文字设为
Button。 - 保存为
CommonButton.prefab。 - 在
PrefabOverrideLab场景中放入三个实例。 - 把三个实例的 Title 分别改为购买、出售、关闭。
- 只把 SellButton 的 Background 颜色改为蓝色,形成颜色差异。
实验 A:修改模板默认颜色
- 打开 Prefab 编辑模式。
- 把 Background 默认颜色改为绿色。
- 保存 Prefab。
- 回到场景观察三个实例。
预期观察:
text
没有颜色 Override 的实例跟随模板变绿
SellButton 如果保存了颜色 Override,则继续保留蓝色
三个实例自己的 Title 文本继续保留实验 B:Revert
- 在 SellButton 上找到被覆盖的颜色属性。
- 使用当前 Creator 版本提供的 Revert/恢复 Prefab 值操作。
- 观察颜色。
预期:SellButton 重新使用 Prefab 的绿色。
实验 C:Apply
- 在 BuyButton 实例上把高度改为 100。
- 明确确认这是要推广到所有按钮的通用修改。
- 使用 Apply 把高度写回 Prefab。
- 观察其他没有高度 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. 概念题
- Prefab 默认值和实例 Override 如何共同决定最终值?
- Apply 和 Revert 的方向分别是什么?
- 为什么运行时修改不会自动写回 Prefab?
- Prefab 内部组件引用和外部业务依赖应该如何处理?
B. 设计题
有一个通用商品卡片 Prefab,三个页面分别需要:
text
商城:价格、购买按钮
仓库:数量、使用按钮
奖励预览:掉落概率、无按钮请判断哪些内容适合放通用 Prefab,哪些适合实例 Override,哪些差异已经值得创建独立变体。
C. 故障诊断题
模板把按钮宽度从 200 改成 260 后,场景里 20 个实例中只有 12 个同步。请给出排查流程,不允许通过删除重建全部实例解决。
二十、练习参考答案
A. 概念题答案
- 没有覆盖的属性使用 Prefab 默认值,有 Override 的属性使用实例值。
- Apply 从实例把修改写回模板;Revert 从模板恢复实例属性。
- 运行时修改只改变当前进程对象,不属于编辑器保存流程。
- 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 激活
→ 全局服务和事件如何清理