Files
cc-3-8-x-mcp/doc/nested-prefab-protocol.md
T
furao 14c5b00f14 Initial public release: cc-3-8-x-mcp
Cocos Creator 3.8.x MCP bridge extension with a built-in offline CLI.

Components:
- Editor extension: in-process MCP server exposing scene / asset-db /
  preview / local / editor-process-control tools
- stdio router: aggregates multiple editor instances on one machine,
  with shortName dedup
- offline CLI (cocos-mcp-cli): headless prefab read/write + a wrapper
  around the Cocos CLI build

Pure Node.js, zero third-party dependencies. Licensed under Apache-2.0.
2026-06-06 11:33:19 +08:00

11 KiB
Raw Blame History

CC3 Nested Prefab 组件引用协议

针对"主 prefab 里的脚本组件 @property 字段要引用嵌套 prefabstub 代理)内部节点/组件"这一场景,cc3 有专门的 cc.TargetOverrideInfo 协议。本文档给出离线工具直接写 prefab JSON 时必须遵循的格式,附 Cocos 引擎运行时的展开走法与踩过的坑。

样本来源:packages/module/game/merge/base/prefab/BottomView.prefab27 条 targetOverrides)、tools/step-3-script/bind-prefab-components.ts 的 resolveLocalIdChain + addTargetOverride


1. 问题场景

fgui 转 cc3 后,父 prefabBottomView.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 脚本,声明:

@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[] sourcesourceInfopropertyPathtargettargetInfo
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

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 逐层查)

function getTarget(localID, targetMap) {
  let targetIter = targetMap;
  for (let i = 0; i < localID.length; i++) {
    targetIter = targetIter[localID[i]];
  }
  return targetIter;
}

3.3 applyTargetOverrides

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.jsprefabUtilsgenerateTargetMap / applyTargetOverrides / expandPrefabInstanceNode)。


4. 产出正确格式的最小例子

主 prefabBottomView.prefab)根节点下挂 BottomView 脚本,字段 _btnStore 要指向 btnStore stub 里的 cc.Button

离线写入(简化伪代码,见 cli/src/api.js_execSetComponentRef 实际实现):

// 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 _btnSpeedcontent stub → btnSpeed(在 content 子 prefab 里又是 stub)→ GemBtn.prefab.cc.Button

{
  "__type__": "cc.TargetInfo",
  "localID": [
    "gFhDEJwJ9nzsN6Ckb087BQ",   // content 子 prefab (BottomViewContent) 里 btnSpeed stub 的 PrefabInstance.fileId
    "njVtqkHLesyiqmBwtic1eg"    // GemBtn.prefab 里 cc.Button 的 CompPrefabInfo.fileId
  ]
}

target 仍指向主 prefab 里最外层 stubcontent 节点)。运行时从 content PrefabInstance 的 targetMap 开始,targetMap[btnSpeed_instance.fileId][cc.Button_comp.fileId]cc.Button


5. 踩坑

5.1 stub 代理节点 _nameundefined,不要按 _name 遍历

fgui 转 cc3 的 nested stub 节点 JSON 里没有 _name 字段——名字靠 cc.PrefabInstance.propertyOverrides 运行时填(CCPropertyOverrideInfo.propertyPath=['_name'])。按 _name 遍历 _children 会对 stub 节点返回 undefined,走不下去。

正确做法:用 tools 侧 stage 2 产出的 prefab-node-paths.json cachePrefabBuilder 按 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-refrefType: "cc.Node" 即自动走这条路径(_getNestedNodeFileId),加载子 prefab JSON,找 _parent === null 的节点,读其 PrefabInfo.fileId。实际案例:SettingUI._role: NoderefNode: {"id": 33}, refType: "cc.Node"2026-05-07 验证)。

5.3 sourceInfo 什么时候必填

cc.TargetOverrideInfo.source 如果是主 prefab 根节点上直接挂的组件 → sourceInfo: null

源组件也在某个 stub 内部(典型:脚本组件本身被 mountedComponents 方式挂到 stub 上)→ sourceInfo 要填 cc.TargetInfolocalID 用跟 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 的 targetOverridesjq '.[1]._prefab | { id: .__id__ }' *.prefab → 找 PrefabInfo,看 targetOverrides 数组长度
  • 验 localID 是否正确:在子 prefab JSON 里 grep fileId,确认对应组件 __type__ 匹配
  • Cocos 编辑器看到 "Missing Component" 或字段空 → 多半是 localID 错,或 target 指向的 stub 节点 _prefab.instance 为 null(漏写 PrefabInstance