Files
cc-3-8-x-mcp/doc/nested-prefab-protocol.md
T

217 lines
11 KiB
Markdown
Raw Normal View History

2026-06-06 11:33:19 +08:00
# CC3 Nested Prefab 组件引用协议
> 针对"主 prefab 里的脚本组件 @property 字段要引用嵌套 prefabstub 代理)内部节点/组件"这一场景,cc3 有专门的 `cc.TargetOverrideInfo` 协议。本文档给出离线工具直接写 prefab JSON 时必须遵循的格式,附 Cocos 引擎运行时的展开走法与踩过的坑。
>
> 样本来源:`packages/module/game/merge/base/prefab/BottomView.prefab`27 条 targetOverrides)、tools/step-3-script/bind-prefab-components.ts 的 `resolveLocalIdChain` + `addTargetOverride`。
---
## 1. 问题场景
fgui 转 cc3 后,父 prefab`BottomView.prefab`)里的 `btnStore` / `content` 节点是**嵌套 prefab 代理**stub),真正的 `cc.Button` / `cc.Label` 等组件在孙 prefab 里:
```
BottomView.prefab
├── touchArea (普通节点)
├── btnStore [stub, _prefab.instance ≠ null] → StoreBtn.prefab (内部有 cc.Button)
├── btnHome [stub] → HomeBtn.prefab
└── content [stub] → BottomViewContent.prefab
├── txtName (cc.Label)
├── btnSell (cc.Button)
└── btnSpeed [stub] → GemBtn.prefab (内部 cc.Button,两层 nested)
```
BottomView 根节点挂 BottomView.ts 脚本,声明:
```typescript
@property({ type: cc.Button }) private _btnStore: cc.Button; // 1 层
@property({ type: cc.Label }) private _txtName: cc.Label; // 1 层(在 content stub 内部)
@property({ type: cc.Button }) private _btnSpeed: cc.Button; // 2 层(content > btnSpeed
```
cc3 **不允许**在主 prefab JSON 里直接写 `comp._btnStore = { __id__: stubNodeId }`——运行时 stub 展开后,主 prefab 里的 stub 子节点全部被替换为子 prefab 内容,按 `__id__` 引用作废。必须通过 **`cc.TargetOverrideInfo`**,用 fileId 链定位目标。
---
## 2. 协议对象
三个关键 `__type__`
| 类型 | 位置 | 字段 |
|---|---|---|
| `cc.TargetOverrideInfo` | 宿主 prefab root `cc.PrefabInfo.targetOverrides[]` | `source``sourceInfo``propertyPath``target``targetInfo` |
| `cc.TargetInfo` | 被 `targetInfo` 引用 | `localID: string[]` |
| `cc.CompPrefabInfo` | 子 prefab 内每个组件的 `__prefab` 指向 | `fileId: string` |
### 2.1 `cc.TargetOverrideInfo` 字段
- **`source`**: `{ __id__: 主 prefab 里源组件的数组下标 }`,如 `BottomView` 脚本组件本身
- **`sourceInfo`**: 源组件也在 stub 内部时填 `cc.TargetInfo`;源在主 prefab 根组件时填 `null`
- **`propertyPath`**: `[fieldName]`,如 `["_btnStore"]`
- **`target`**: `{ __id__: 主 prefab 里 stub 代理节点的下标 }`
- **`targetInfo`**: `{ __id__: 关联的 cc.TargetInfo 下标 }`
### 2.2 `cc.TargetInfo.localID` — 多层 fileId 链
`localID` 是**字符串数组**,每一段对应 Cocos 运行时展开 stub 后的一层查找 key:
| 层数 | 场景 | localID 元素 |
|---|---|---|
| 1 | stub 展开后根组件匹配 | `[组件 cc.CompPrefabInfo.fileId]`(子 prefab 里该组件的 fileId |
| 1 | stub 展开后根节点引用 | `[节点 cc.PrefabInfo.fileId]` |
| 2 | stub 里还有孙 stub | `[孙 stub instance.fileId, 孙 prefab 内组件的 CompPrefabInfo.fileId]` |
| N | N-1 层孙代理 | 每过一层 PrefabInstance 边界压一段 fileId |
**每过一层 PrefabInstance 边界取的是 `instance.fileId`**(从 `cc.PrefabInstance.fileId` 读),不是代理节点自身 `PrefabInfo.fileId`,也不是子 prefab root 的 `PrefabInfo.fileId`
---
## 3. 引擎运行时走法(Cocos 3.8.x
### 3.1 `generateTargetMap`(为代理节点生成 targetMap
```js
function generateTargetMap(node, targetMap, isRoot) {
let curTargetMap = targetMap;
const prefabInstance = node.prefab?.instance;
if (!isRoot && prefabInstance) {
// 过 PrefabInstance 边界 → 新开子 mapkey = instance.fileId
targetMap[prefabInstance.fileId] = {};
curTargetMap = targetMap[prefabInstance.fileId];
}
const prefabInfo = node.prefab;
if (prefabInfo) curTargetMap[prefabInfo.fileId] = node;
for (const comp of node.components) {
if (comp.__prefab) curTargetMap[comp.__prefab.fileId] = comp;
}
for (const child of node.children) generateTargetMap(child, curTargetMap, false);
}
```
### 3.2 `getTarget`(按 localID 逐层查)
```js
function getTarget(localID, targetMap) {
let targetIter = targetMap;
for (let i = 0; i < localID.length; i++) {
targetIter = targetIter[localID[i]];
}
return targetIter;
}
```
### 3.3 `applyTargetOverrides`
```js
const targetAsNode = targetOverride.target; // 主 prefab 里 stub 节点
const targetInstance = targetAsNode.prefab.instance; // 该 stub 的 PrefabInstance
const target = getTarget(targetOverride.targetInfo.localID, targetInstance.targetMap);
// targetPropOwner 就是 source(或按 propertyPath 走到最末段的 owner
targetPropOwner[propertyName] = target; // 把真正的目标对象挂上去
```
引擎源文件(仅参考):`/Applications/Cocos/Creator/3.8.5/CocosCreator.app/.../3d/engine/bin/.editor/bundled/index.js``prefabUtils``generateTargetMap` / `applyTargetOverrides` / `expandPrefabInstanceNode`)。
---
## 4. 产出正确格式的最小例子
主 prefab`BottomView.prefab`)根节点下挂 `BottomView` 脚本,字段 `_btnStore` 要指向 `btnStore` stub 里的 `cc.Button`
离线写入(简化伪代码,见 `cli/src/api.js``_execSetComponentRef` 实际实现):
```js
// 1. 找子 prefab (StoreBtn.prefab) 里 cc.Button 的 __prefab.fileId
const btnFileId = findCompFileId(subPrefabJson, 'cc.Button'); // e.g. 'jJyfx2p9Mlc7QuNfNGOGMg'
// 2. push TargetInfo
const tiIdx = elements.length;
elements.push({ __type__: 'cc.TargetInfo', localID: [btnFileId] });
// 3. push TargetOverrideInfo
const toIdx = elements.length;
elements.push({
__type__: 'cc.TargetOverrideInfo',
source: { __id__: sourceCompIdx }, // BottomView 脚本组件
sourceInfo: null, // 源在主 prefab 根,不需要 sourceInfo
propertyPath: ['_btnStore'],
target: { __id__: stubNodeIdx }, // 主 prefab 里 btnStore 节点
targetInfo: { __id__: tiIdx },
});
// 4. 挂到主 prefab root cc.PrefabInfo.targetOverrides
rootPrefabInfo.targetOverrides = rootPrefabInfo.targetOverrides || [];
rootPrefabInfo.targetOverrides.push({ __id__: toIdx });
```
### 两层 nested 的 localID 示例
BottomView `_btnSpeed``content` stub → `btnSpeed`(在 content 子 prefab 里又是 stub)→ GemBtn.prefab.cc.Button
```json
{
"__type__": "cc.TargetInfo",
"localID": [
"gFhDEJwJ9nzsN6Ckb087BQ", // content 子 prefab (BottomViewContent) 里 btnSpeed stub 的 PrefabInstance.fileId
"njVtqkHLesyiqmBwtic1eg" // GemBtn.prefab 里 cc.Button 的 CompPrefabInfo.fileId
]
}
```
`target` 仍指向**主 prefab 里最外层 stub**`content` 节点)。运行时从 content PrefabInstance 的 targetMap 开始,`targetMap[btnSpeed_instance.fileId][cc.Button_comp.fileId]``cc.Button`
---
## 5. 踩坑
### 5.1 stub 代理节点 `_name` 为 `undefined`,不要按 `_name` 遍历
fgui 转 cc3 的 nested stub 节点 JSON 里没有 `_name` 字段——名字靠 `cc.PrefabInstance.propertyOverrides` 运行时填(`CCPropertyOverrideInfo.propertyPath=['_name']`)。按 `_name` 遍历 `_children` 会对 stub 节点返回 `undefined`,走不下去。
**正确做法**:用 tools 侧 stage 2 产出的 `prefab-node-paths.json` cache`PrefabBuilder` 按 fgui 原始对象名建的 path → idx 映射)。cli 场景没有该 cache 时,按 "stub 节点 `_prefab.instance` 非空" 识别 stub,然后 `parsePrefab` 加载子 prefab 按需查子结构。
### 5.2 localID 段的取法不一致
- 过 PrefabInstance 边界 → 读 `cc.PrefabInstance.fileId`(不是代理节点 `PrefabInfo.fileId`
- 到组件 → 读 `cc.CompPrefabInfo.fileId`
- 到节点 → 读 `cc.PrefabInfo.fileId`(即嵌套 prefab 根节点 `_parent === null` 对应 PrefabInfo 的 fileId
混用会让 `getTarget` 返回 undefined。
**cc.Node 型绑定(localID 取法)**:当 @property 字段声明是 `Node` 而非某个组件时,`localID = [嵌套 prefab 根节点的 cc.PrefabInfo.fileId]`。cli 的 `set-component-ref``refType: "cc.Node"` 即自动走这条路径(`_getNestedNodeFileId`),加载子 prefab JSON,找 `_parent === null` 的节点,读其 PrefabInfo.fileId。实际案例:`SettingUI._role: Node``refNode: {"id": 33}, refType: "cc.Node"`2026-05-07 验证)。
### 5.3 `sourceInfo` 什么时候必填
`cc.TargetOverrideInfo.source` 如果是主 prefab 根节点上直接挂的组件 → `sourceInfo: null`
源组件也在某个 stub 内部(典型:脚本组件本身被 mountedComponents 方式挂到 stub 上)→ `sourceInfo` 要填 `cc.TargetInfo``localID` 用跟 `targetInfo` 同样规则逐层查到源组件。cli 当前 `_execSetComponentRef` **暂不支持**源在 stub 的场景(抛错提示)。
### 5.4 fgui→cc3 的 "Empty 占位 + 运行时绑定组件"
fgui 里 `@inject(SpineView, "path", { res: xxx })` 这种运行时加载组件的字段,fgui 源文件里用通用 `Empty.xml` 占位。直接转 cc3 nested stub 会让目标节点是 Empty.prefab 代理,里面**没有** sp.Skeleton 组件,bind 挂不上。
tools 侧的修法:`step-2-prefab/src/converter/PrefabBuilder.ts` 检测到 `nodeTypeMap` 要求挂内建组件 + 孙 prefab 是 `Empty` → 退化为 inline 普通节点,sp.Skeleton 直接挂节点。
### 5.5 localID 数组只有 1 段 vs 多段
单层 nested(stub 展开后的根组件匹配)→ 1 段 fileId。多层 nested → N 段。cli 当前 `_resolveLocalIdChain` 实现只支持 1 层;多层由 tools/step-3-script/bind-prefab-components 的 `resolveLocalIdChain` 递归展开(见该文件的 fileId 链构造)。cli 要支持多层场景需要扩展,暂未实现。
---
## 6. 相关工具
| 操作 | 入口 |
|---|---|
| 给 @property 字段挂跨 nested 引用 | `set-component-ref` oprefNode 是 stub 自动走 targetOverrides |
| 改 stub 内部组件字段(cc.Label._string 等) | `set-nested-component-field` op |
| 批量跨 nested 挂载(数百字段) | tools/step-3-script/bind-prefab-components.tsstage 3 pipeline |
| 手工修单个 prefab 快速验证 | `set-component-ref` + dry-run 查 targetOverrides 数组 |
---
## 7. 调试
- 查当前 prefab 的 targetOverrides`jq '.[1]._prefab | { id: .__id__ }' *.prefab` → 找 PrefabInfo,看 `targetOverrides` 数组长度
- 验 localID 是否正确:在子 prefab JSON 里 grep fileId,确认对应组件 `__type__` 匹配
- Cocos 编辑器看到 "Missing Component" 或字段空 → 多半是 localID 错,或 target 指向的 stub 节点 `_prefab.instance` 为 null(漏写 PrefabInstance