# CC3 Nested Prefab 组件引用协议 > 针对"主 prefab 里的脚本组件 @property 字段要引用嵌套 prefab(stub 代理)内部节点/组件"这一场景,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 边界 → 新开子 map,key = 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` op(refNode 是 stub 自动走 targetOverrides) | | 改 stub 内部组件字段(cc.Label._string 等) | `set-nested-component-field` op | | 批量跨 nested 挂载(数百字段) | tools/step-3-script/bind-prefab-components.ts(stage 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)