Files
cc-3-8-x-mcp/doc/nested-prefab-protocol.md
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

217 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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