mirror of
https://github.com/HappyLifeOk/cc-3-8-x-mcp.git
synced 2026-06-10 09:46:47 +00:00
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.
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
# CC 3.8.x AnimationClip (.anim) 文件结构速查表
|
||||
|
||||
> 目标读者:读写 .anim 文件的 AI / 工具开发者。
|
||||
> .anim 和 .prefab 都是「JSON 数组 + `__id__` 交叉引用」的同构格式,parse/write 复用 [prefab-schema.md](./prefab-schema.md) 里描述的规则;本文只补 .anim 独有的对象类型与字段。
|
||||
> 样本来源:`assets/packages/module/game/merge/effect/component/prefab/Skwjquchuquchu.anim`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 整体结构
|
||||
|
||||
```
|
||||
objects[0] = cc.AnimationClip ← 文件头(引用 _tracks / _embeddedPlayers / _additiveSettings)
|
||||
objects[1..N-3] = Tracks + TrackPaths + Channels + Curves + HierarchyPath/ComponentPath
|
||||
objects[N-2] = cc.AnimationClipAdditiveSettings
|
||||
objects[N-1] = (可选)EmbeddedPlayer / EmbeddedAnimationClipPlayable
|
||||
```
|
||||
|
||||
依赖链:
|
||||
|
||||
```
|
||||
AnimationClip
|
||||
└─ _tracks[] → Track
|
||||
├─ _binding.path → TrackPath
|
||||
│ └─ _paths[] → HierarchyPath / ComponentPath / string(propName)
|
||||
└─ _channel / _channels → Channel
|
||||
└─ _curve → RealCurve / ObjectCurve
|
||||
└─ _times[] / _values[](对齐等长)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Track 类型与字段(**最容易踩的坑**)
|
||||
|
||||
| `__type__` | 通道数 | 字段名 | 额外字段 | 典型属性 |
|
||||
|---|---|---|---|---|
|
||||
| `cc.animation.RealTrack` | 1 | **`_channel`**(单数) | — | `opacity`, `active`, 单个标量属性 |
|
||||
| `cc.animation.ObjectTrack` | 1 | **`_channel`**(单数) | — | `spriteFrame`, 资产引用 |
|
||||
| `cc.animation.VectorTrack` | 2/3/4 | **`_channels`**(复数数组) | `_nComponents` | `position`, `scale`, `eulerAngles` |
|
||||
| `cc.animation.ColorTrack` | 4 | **`_channels`**(复数数组) | — | `color`(r/g/b/a) |
|
||||
|
||||
### ⚠️ 历史踩过的坑
|
||||
|
||||
**给 `RealTrack` 写 `_channels: [ref(idx)]`(复数数组)而不是 `_channel: ref(idx)`(单数)。**
|
||||
|
||||
CC3 编辑器按 schema 验证 .anim 文件,RealTrack / ObjectTrack 的通道字段必须是 `_channel`(单数、单对象)。字段名写错会:
|
||||
|
||||
- **编辑器里看不到这条轨道的关键帧**(属性列表不显示)
|
||||
- **运行时不播**(track 被忽略)
|
||||
- **文件能写进去、也不报错**(CC3 对未知/缺失字段静默忽略)
|
||||
|
||||
排查路径:打开 .anim 看节点 UIOpacity.opacity / spriteFrame 这类单值属性没显示时,第一件事是检查 `_channel` vs `_channels`。
|
||||
|
||||
**正确写法请用 [cli/src/anim-primitives.js](../cli/src/anim-primitives.js) 的 `makeRealTrack` / `makeObjectTrack` / `makeVectorTrack` / `makeColorTrack` 工厂函数**,绑定了正确的字段名,编译期避免拼错。
|
||||
|
||||
---
|
||||
|
||||
## 3. 曲线与关键帧
|
||||
|
||||
### cc.RealCurve(浮点曲线)
|
||||
|
||||
```json
|
||||
{
|
||||
"__type__": "cc.RealCurve",
|
||||
"_times": [0, 0.1, 0.4666...],
|
||||
"_values": [{RealKeyframeValue}, ...],
|
||||
"preExtrapolation": 1,
|
||||
"postExtrapolation": 1
|
||||
}
|
||||
```
|
||||
|
||||
- `_times` 递增秒数,和 `_values` 等长
|
||||
- `preExtrapolation` / `postExtrapolation`:`0=LINEAR / 1=CLAMP / 2=REPEAT / 3=PINGPONG`,最常用 `1`
|
||||
|
||||
### cc.RealKeyframeValue
|
||||
|
||||
```json
|
||||
{
|
||||
"__type__": "cc.RealKeyframeValue",
|
||||
"interpolationMode": 1,
|
||||
"tangentWeightMode": 0,
|
||||
"value": 255,
|
||||
"rightTangent": 0, "rightTangentWeight": 1,
|
||||
"leftTangent": 0, "leftTangentWeight": 1,
|
||||
"easingMethod": 0,
|
||||
"__editorExtras__": null
|
||||
}
|
||||
```
|
||||
|
||||
- `interpolationMode`:**`0=LINEAR / 1=CONSTANT / 2=CUBIC`**
|
||||
- LINEAR:两关键帧之间平滑线性过渡(视觉上看到淡入淡出)
|
||||
- CONSTANT:保持当前值到下一帧瞬变(FGUI 非 tween item 语义)
|
||||
- CUBIC:带缓动曲线(`easingMethod` 配 `cc.EasingMethod` 枚举)
|
||||
|
||||
### cc.ObjectCurve(对象引用曲线,如 spriteFrame 序列)
|
||||
|
||||
和 RealCurve 结构一样,`_values` 换成 `{ __uuid__, __expectedType__ }` 资产引用。
|
||||
|
||||
---
|
||||
|
||||
## 4. TrackPath 路径寻址
|
||||
|
||||
TrackPath 定位"哪个节点 → 哪个组件 → 哪个属性":
|
||||
|
||||
```json
|
||||
{
|
||||
"__type__": "cc.animation.TrackPath",
|
||||
"_paths": [
|
||||
{ "__id__": hierIdx }, // HierarchyPath: { path: "n4" }
|
||||
{ "__id__": compIdx }, // ComponentPath: { component: "cc.UIOpacity" }
|
||||
"opacity" // 属性名字符串
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- **根节点自身**(`path === ""`):省略 HierarchyPath,直接 `[ComponentPath, propName]` 或 `[propName]`(Node 本身的属性如 position)
|
||||
- **Node 本身属性**(`position` / `scale` / `eulerAngles` / `active`):不需要 ComponentPath,`[HierarchyPath, propName]`
|
||||
- **组件属性**(`UIOpacity.opacity` / `Sprite.color` / `Sprite.spriteFrame`):需要 ComponentPath,`[HierarchyPath, ComponentPath, propName]`
|
||||
|
||||
---
|
||||
|
||||
## 5. EmbeddedPlayer(movieclip 子动画触发)
|
||||
|
||||
FGUI 的 movieclip 节点在 CC3 里用「主 clip 挂 EmbeddedPlayer + 目标节点自己的 cc.Animation 播子 clip」这种双层结构表达。
|
||||
|
||||
```
|
||||
主 AnimationClip
|
||||
└─ _embeddedPlayers[] → cc.animation.EmbeddedPlayer
|
||||
├─ begin, end ← 主 clip 时间线上的起止秒
|
||||
└─ playable → EmbeddedAnimationClipPlayable
|
||||
├─ path = "n4" ← 目标节点(从主 clip 挂载节点算相对路径)
|
||||
└─ clip = UUID ← 子 clip 资产 UUID
|
||||
```
|
||||
|
||||
### ⚠️ 子 clip 的关键帧时序
|
||||
|
||||
子 clip 的 `t=0` **必须对应 `EmbeddedPlayer.begin` 那一刻**,不能把主 clip 的绝对时间当子 clip 的时间来写。
|
||||
|
||||
举例:FGUI 在 frame 3(0.1s)触发 movieclip 播放,子 clip 20 帧。
|
||||
|
||||
- ❌ 错:子 clip keyframes `[t=0, t=0.1, t=0.133, ...]`(t=0 是静态占位、t=0.1 才是播放命令)→ EmbeddedPlayer begin=0.1 时,子 clip 从自己的 t=0 开始,frame 0 会显示 0.1 秒才切 frame 1
|
||||
- ✅ 对:子 clip keyframes `[t=0, t=0.033, t=0.067, ...]`(t=0 直接是第一帧),sub-clip `_duration = transitionDuration - begin`
|
||||
|
||||
### ⚠️ 主 clip / 子 clip 职责划分
|
||||
|
||||
**主 clip 做所有"业务逻辑"动画**(fade in/out、rotation、scale、position 等),**子 clip 只做 spriteFrame 逐帧切换**。原因:
|
||||
|
||||
- 节点的 cc.Animation 播子 clip 时,子 clip 的 TrackPath 写空 HierarchyPath 直接驱动节点本身;主 clip 通过 HierarchyPath 也能驱动该节点其他属性
|
||||
- 两边同时驱动同一属性会产生混合(blending)冲突
|
||||
- 把业务轨道留在主 clip,子 clip 只管图片切换,最干净
|
||||
|
||||
---
|
||||
|
||||
## 6. 初始值关键帧
|
||||
|
||||
**非 tween 轨道的首个关键帧若不在 `t=0`,必须额外补一帧 `t=0` 表示初始值**。否则 `preExtrapolation=CLAMP` 会把首帧值倒推到 `-∞`,产生视觉异常。
|
||||
|
||||
典型例子:Rotation 只在 `t=0.1` 设 `Z=269°`,要在 `t=0` 补 `Z=0`,否则 clip 一开始节点就已经是旋转 269°。
|
||||
|
||||
Alpha 通常 FGUI 自己会写 `time=0, value=0`,所以 opacity 轨道天然有初始帧;Rotation/Scale 常常缺首帧,需要转换器主动补。
|
||||
|
||||
---
|
||||
|
||||
## 7. 用 anim-primitives 构建完整 .anim
|
||||
|
||||
最小示例:给 "n4" 节点 `cc.UIOpacity.opacity` 加一条 `[0→0, 0.1→255, 0.467→0]` 的 CONSTANT 轨道。
|
||||
|
||||
```js
|
||||
const { parsePrefab, writePrefab, anim, ref } = require('./cli/src');
|
||||
const {
|
||||
makeHierarchyPath, makeComponentPath, makeTrackPath,
|
||||
makeRealKeyframe, makeRealCurve, makeChannel, makeRealTrack,
|
||||
makeAdditiveSettings, makeAnimationClip,
|
||||
InterpolationMode, hashString,
|
||||
} = anim;
|
||||
|
||||
const objects = [];
|
||||
objects.push(null); // [0] 占位 AnimationClip
|
||||
|
||||
const hierIdx = objects.length; objects.push(makeHierarchyPath('n4')); // [1]
|
||||
const compIdx = objects.length; objects.push(makeComponentPath('cc.UIOpacity')); // [2]
|
||||
const pathIdx = objects.length;
|
||||
objects.push(makeTrackPath([ref(hierIdx), ref(compIdx), 'opacity'])); // [3]
|
||||
|
||||
const curveIdx = objects.length;
|
||||
objects.push(makeRealCurve({
|
||||
times: [0, 0.1, 0.4667],
|
||||
values: [
|
||||
makeRealKeyframe({ value: 0, interpolationMode: InterpolationMode.CONSTANT }),
|
||||
makeRealKeyframe({ value: 255, interpolationMode: InterpolationMode.CONSTANT }),
|
||||
makeRealKeyframe({ value: 0, interpolationMode: InterpolationMode.CONSTANT }),
|
||||
],
|
||||
})); // [4]
|
||||
|
||||
const chIdx = objects.length; objects.push(makeChannel(curveIdx)); // [5]
|
||||
const trackIdx = objects.length; objects.push(makeRealTrack(pathIdx, chIdx)); // [6]
|
||||
|
||||
const additiveIdx = objects.length; objects.push(makeAdditiveSettings()); // [7]
|
||||
|
||||
objects[0] = makeAnimationClip({
|
||||
name: 'demo',
|
||||
sample: 30,
|
||||
duration: 0.7333,
|
||||
hash: hashString('demo'),
|
||||
trackIndices: [trackIdx],
|
||||
additiveIdx,
|
||||
});
|
||||
|
||||
require('fs').writeFileSync('demo.anim', JSON.stringify(objects, null, 2));
|
||||
```
|
||||
|
||||
**关键点**:`makeRealTrack` 自动生成 `_channel`(单数)字段,保证编辑器能识别;改用 `makeVectorTrack` / `makeColorTrack` 自动生成 `_channels`(复数数组)+ 必要的 `_nComponents`。
|
||||
+795
@@ -0,0 +1,795 @@
|
||||
# cocos-mcp-cli — CC3 Prefab 离线读写工具
|
||||
|
||||
> 单一真相文档。覆盖:定位、命令、op 全表、配方、已知坑、源码导航。
|
||||
>
|
||||
> 给 AI agent 也给人类开发者。改 `.prefab` / `.anim` 文件前必读。
|
||||
|
||||
---
|
||||
|
||||
## 1. 定位与硬规则
|
||||
|
||||
**`.prefab` 文件必须用本 CLI 操作,禁止 `Read + Edit` 工具直接编辑。**
|
||||
|
||||
prefab 是 JSON 数组,`__id__` 是数组下标,字符串替换会破坏所有引用关系。唯一例外:纯文本字面量替换(如改一个类名字符串)。
|
||||
|
||||
**适用场景**:
|
||||
|
||||
- 修改节点字段(`_active` / `_lpos` / `_name` / 子节点顺序)
|
||||
- 修改组件字段(`cc.Label._string` / `cc.Sprite._spriteFrame` / `cc.UITransform` 锚点尺寸 / 自定义脚本字段)
|
||||
- 给脚本组件 `@property` 挂节点 / 组件引用(含嵌套 prefab 内)
|
||||
- 增删克隆节点 / 加组件 / 合并重复组件
|
||||
- 跨多个 prefab 跑同一组 ops(`--glob`)
|
||||
- 比较两个 prefab(`diff` 子命令)
|
||||
- 操作 `.anim` 文件(`anim` 子命令,与 prefab 同格式)
|
||||
|
||||
**不适用 / 已知限制**:
|
||||
|
||||
- 多层嵌套 stub(stub 内还有 stub):CLI 支持 `refSubNode` 字符串数组路径,但更深层场景仍建议走 tools pipeline(`tools/step-3-script/bind-prefab-components.ts`)
|
||||
- 改 `.anim` 文件里的 AnimationClip / Track / Curve 结构:用 `cli/src/anim-primitives.js` 在脚本中处理,不通过 op
|
||||
- 脚本组件本身在 stub 内挂载(`mountedComponents`)的 @property 绑定:CLI 会抛错
|
||||
|
||||
---
|
||||
|
||||
## 2. 入口与路径约定
|
||||
|
||||
```bash
|
||||
# 必须用 bin/cocos-mcp-cli.js(src/index.js 是 re-export 无 CLI 入口;src/cli/main.js 不自调用)
|
||||
cd <你的 Cocos 项目根目录>
|
||||
node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js <command> [args]
|
||||
```
|
||||
|
||||
prefab 路径相对 `forest/` 项目根,不带 `forest/` 前缀:
|
||||
|
||||
```
|
||||
assets/packages/common/setting/ui/SettingUI.prefab
|
||||
assets/packages/module/sign/prefab/SignUI.prefab
|
||||
```
|
||||
|
||||
零依赖,无需 `npm install`。可选全局链接:
|
||||
|
||||
```bash
|
||||
cd extensions/cc-3-8-x-mcp/cli
|
||||
npm link
|
||||
cocos-mcp-cli <command>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 标准工作流
|
||||
|
||||
```bash
|
||||
# 1. 查节点树,确认目标 ID 和 isStub 信息
|
||||
node bin/cocos-mcp-cli.js query <prefab> --selector tree
|
||||
|
||||
# 2. 写 ops.json(见 §6 Op 全表)
|
||||
|
||||
# 3. 干跑预览改动(不写盘)
|
||||
node bin/cocos-mcp-cli.js batch <prefab> ops.json --dry-run
|
||||
|
||||
# 4. 落盘
|
||||
node bin/cocos-mcp-cli.js batch <prefab> ops.json
|
||||
|
||||
# 5. 类型检查
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
成功输出 `{"changed": true, "opsApplied": N, "nodesAffected": [...]}`。任一 op 失败整体不落盘(原子性)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 命令完整参考
|
||||
|
||||
```
|
||||
cocos-mcp-cli query <prefab> [--selector tree|node|find|field]
|
||||
[--name X] [--type cc.Label]
|
||||
[--comp cc.UITransform] [--field _anchorPoint]
|
||||
[--with-comps]
|
||||
cocos-mcp-cli set <prefab> <nodeName> <field> <value>
|
||||
cocos-mcp-cli batch <prefab> <ops.json> [--project-root <path>] [--dry-run]
|
||||
cocos-mcp-cli batch <ops.json> --glob <pattern> [--project-root <path>] [--dry-run]
|
||||
cocos-mcp-cli anim <subcommand> <file> [args] # subcommand: query | batch
|
||||
cocos-mcp-cli diff <prefabA> <prefabB> # 字段级 diff,输出与 dry-run 同格式
|
||||
cocos-mcp-cli create-prefab <out> [--name N] [--width W] [--height H]
|
||||
[--add-spine <skel-uuid>] [--dry-run]
|
||||
cocos-mcp-cli extract-prefab <src> <out> --node <selector> [--name X] [--dry-run]
|
||||
cocos-mcp-cli compact-prefab <prefab> [--dry-run]
|
||||
```
|
||||
|
||||
### query
|
||||
|
||||
| selector | 说明 | 必带 flag |
|
||||
|---|---|---|
|
||||
| `tree`(默认) | 精简节点树(`id` / `name` / `isStub` / `componentTypes` / `children`) | — |
|
||||
| `node` | 单节点详情(含 raw + overrides) | `--name <节点名>` |
|
||||
| `find` | 按 `__type__` 列所有匹配 element 的 id | `--type <类型>` |
|
||||
| `field` | 单组件单字段值(输出原始 JSON,方便 `jq` 管道) | `--name --comp --field` |
|
||||
| `overrides` | 列 stub 节点当前所有 propertyOverrides + 关联 root targetOverrides | `--id N` / `--path A/B/C` / `--name <name>` |
|
||||
|
||||
`--with-comps`:`tree` / `node` 下展开节点的所有组件字段(输出 `components: [{type, id, fields}]`)。不带这个 flag 只输出 `componentTypes` 类型名列表。
|
||||
|
||||
`overrides` 输出每条 override 的落点 `target.kind`:
|
||||
- `stub-node-field` — stub 节点自身字段(_lpos / _name / _lscale 等)
|
||||
- `nested-component` — 嵌套 prefab 内某组件字段(带 `componentType` + `ownerNodeName`)
|
||||
- `nested-node` — 嵌套 prefab 内某子节点字段
|
||||
- `unknown` — 嵌套 prefab 加载失败或 fileId 不在嵌套索引内
|
||||
|
||||
调 stub 字段对不上时先跑 `overrides` 看当前已写入哪些,再用 `reset-overrides` op 清单条或一键回滚。
|
||||
|
||||
### set(单字段快捷写入)
|
||||
|
||||
支持的 `field`:`active` / `label.text` / `position.x` / `position.y` / `position.z`。
|
||||
|
||||
复杂操作请用 `batch`。
|
||||
|
||||
### batch
|
||||
|
||||
- `--project-root <path>`:当 `<prefab>` 放在项目目录外(如 `/tmp/`)时必须显式传入含 `assets/ + package.json` 的项目根。否则 className → 压缩 classId 查表失败时会抛错(避免写入 className 字符串导致 cocos MissingScript)。同样,刚新建 .ts 但 cocos 编辑器尚未生成 .ts.meta 时也会抛错——等 .meta 出来再跑。
|
||||
- `--dry-run`:跑完不写盘,输出 `{ changed: false, dryRun: true, diff: [...] }`,`diff` 是字段级差异 `{ "a.b.c": [old, new] }`
|
||||
- `--glob <pattern>`:第一个位置参数当 ops.json,对所有匹配 pattern 的 prefab 跑同一组 ops。pattern 支持 `**` / `*` / `?`,相对 cwd。每个文件独立执行,单文件失败不阻断后续。**先用 `--dry-run` 确认匹配范围**,再去掉落盘
|
||||
|
||||
```bash
|
||||
# glob 示例
|
||||
node bin/cocos-mcp-cli.js batch /tmp/ops.json \
|
||||
--glob "assets/packages/module/**/ui/*.prefab" --dry-run
|
||||
```
|
||||
|
||||
### anim
|
||||
|
||||
`.anim` 与 `.prefab` 同为 JSON 数组 + `__id__` 引用格式,复用 `editPrefab`。op 主要面向 cc.Node 树。改 AnimationClip / Track / Curve 结构请用 `cli/src/anim-primitives.js` 在脚本中处理。
|
||||
|
||||
```bash
|
||||
node bin/cocos-mcp-cli.js anim query my.anim --selector tree
|
||||
node bin/cocos-mcp-cli.js anim batch my.anim ops.json --dry-run
|
||||
```
|
||||
|
||||
### diff
|
||||
|
||||
字段级 diff,输出与 dry-run 同格式:
|
||||
|
||||
```bash
|
||||
node bin/cocos-mcp-cli.js diff old.prefab new.prefab
|
||||
```
|
||||
|
||||
适用:CI 验证转换工具产物 / 对照历史版本 / review 自动 diff。
|
||||
|
||||
### ops schema 预校验
|
||||
|
||||
`editPrefab` 跑前一次性扫所有 op,发现拼错(`comp` → `componentType` 友好提示)/ 缺必填字段 / 未知 op 类型,一次性报齐,不会跑到一半才发现。
|
||||
|
||||
### create-prefab
|
||||
|
||||
从零创建一个新 prefab + 配套 `.prefab.meta`。`output-path` 不带 `.prefab` 后缀会自动补全。
|
||||
|
||||
`uuid` / `fileId` 走 `deterministicUUID` / `deterministicFileId` 以 `create-prefab:<name>` 为种子推导:**同名 prefab 每次生成相同 UUID**,可重入。
|
||||
|
||||
| flag | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `--name <N>` | 取 basename 去 `.prefab` | prefab 内部 `_name` + `meta.userData.syncNodeName` |
|
||||
| `--width <W>` | 普通 750 / spine 100 | UITransform 宽 |
|
||||
| `--height <H>` | 普通 200 / spine 100 | UITransform 高 |
|
||||
| `--add-spine <skel-uuid>` | — | 在 root 节点多挂 `sp.Skeleton` 组件,`_skeletonData.__uuid__` 指向给定的 `.skel` 资产 UUID;条目数 5 → 7 |
|
||||
| `--dry-run` | — | 不写盘,把 prefab + meta JSON 输出到 stdout |
|
||||
|
||||
不带 `--add-spine`(5 条目):
|
||||
```
|
||||
0 cc.Prefab
|
||||
1 cc.Node (root)
|
||||
2 cc.UITransform
|
||||
3 cc.CompPrefabInfo (UITransform)
|
||||
4 cc.PrefabInfo (root)
|
||||
```
|
||||
|
||||
带 `--add-spine`(7 条目):
|
||||
```
|
||||
0 cc.Prefab
|
||||
1 cc.Node (root, _components: [2, 4])
|
||||
2 cc.UITransform
|
||||
3 cc.CompPrefabInfo (UITransform)
|
||||
4 sp.Skeleton
|
||||
5 cc.CompPrefabInfo (sp.Skeleton)
|
||||
6 cc.PrefabInfo (root)
|
||||
```
|
||||
|
||||
**批量生成 spine prefab**(外层 shell 循环,CLI 自身不扫目录):
|
||||
|
||||
```bash
|
||||
for meta in assets/res/<group>/<xxxN>/*.skel.meta; do
|
||||
name=$(basename "$meta" .skel.meta)
|
||||
uuid=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['uuid'])" "$meta")
|
||||
node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js create-prefab \
|
||||
"assets/packages/<group>/<xxxN>/prefab/${name}.prefab" \
|
||||
--add-spine "$uuid"
|
||||
done
|
||||
```
|
||||
|
||||
已存在 prefab 加 spine 组件用 `add-component`(不走本命令):
|
||||
```json
|
||||
{"op": "add-component", "node": "root", "componentType": "sp.Skeleton",
|
||||
"props": {"_skeletonData": {"__uuid__": "<skel-uuid>", "__expectedType__": "sp.SkeletonData"}}}
|
||||
```
|
||||
|
||||
### extract-prefab
|
||||
|
||||
从源 prefab 里抠出一个子节点的完整闭包,写成新独立 prefab。
|
||||
|
||||
| flag | 说明 |
|
||||
|---|---|
|
||||
| `<src>` | 源 prefab 路径 |
|
||||
| `<out>` | 输出新 prefab 路径 |
|
||||
| `--node <selector>` | 节点选择器(同 batch 三种:节点名 / `{"id":N}` / `{"path":"A/B"}`) |
|
||||
| `--name <X>` | 新 prefab 根节点 `_name`(可选,默认沿用源节点名) |
|
||||
| `--dry-run` | 不写盘,把新 prefab + meta JSON 输出到 stdout |
|
||||
|
||||
闭包收集规则(`cli/src/cli/extract-cmd.js`):
|
||||
- 从 srcNode 开始递归走所有字段,遇 `{__id__: N}` 把 N 加入闭包队列
|
||||
- 跳过 `_parent`(反向引用会把父链/兄弟拖进闭包,破坏「只提子树」语义)
|
||||
- 闭包内元素按原 idx 升序拷贝到新数组(srcNode 永远是 idx 1)
|
||||
|
||||
输出语义:
|
||||
- 新 root: `_parent = null`,`_name = newName`
|
||||
- 新 root 的 PrefabInfo: `root → {__id__: 1}`, `asset → {__id__: 0}`, 清掉 `instance / targetOverrides / nestedPrefabInstanceRoots`(这些字段在源里是相对宿主的,独立 prefab 不需要)
|
||||
- meta uuid: 走 `deterministicUUID(extract-prefab:<outPath>:<newName>:uuid)`,**同 src+out+name 每次同 uuid**
|
||||
|
||||
例:从 HomeBottom 提取 btnTask 为独立 BottomEntry.prefab
|
||||
```bash
|
||||
node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js \
|
||||
extract-prefab \
|
||||
assets/packages/.../HomeBottom.prefab \
|
||||
assets/packages/module/task/prefab/BottomEntry.prefab \
|
||||
--node "btnTask" \
|
||||
--name "BottomEntry"
|
||||
```
|
||||
|
||||
### compact-prefab
|
||||
|
||||
清 prefab `data` 数组里所有 `null` 槽位 + 重映射所有 `__id__` 引用。
|
||||
|
||||
**为啥需要这个 op**:Cocos editor 反序列化是「宽容模式」(遇 null 跳过);但 Cocos build worker 是「严格模式」,scan 整个 data 数组撞 null 就崩 `TypeError: Cannot read properties of undefined (reading '__type__')`。早期手工生成的 prefab(比如 extract-prefab 上线前用 Read+Edit 改 elements[i] = null 这种历史操作)会留下 null 槽位;GUI 能打开 + 运行时正常,但 build 跑不通。
|
||||
|
||||
算法(`cli/src/cli/compact-cmd.js`):
|
||||
- 跟 extract-cmd line 105-132 同款(紧凑 push + remap),但不剔除任何东西
|
||||
- 收集所有 null 索引 → 构造 oldIdx → newIdx 映射(newIdx = oldIdx - 前面被删 null 数量)→ `data.filter(el => el !== null)` → 递归 `_remapIds`
|
||||
|
||||
| flag | 说明 |
|
||||
|---|---|
|
||||
| `<prefab>` | prefab 路径 |
|
||||
| `--dry-run` | 不写盘,输出统计 + dangling 引用警告 |
|
||||
|
||||
输出格式:
|
||||
```
|
||||
<prefab> → <oldLen> → <newLen> (清掉 N 个 null) [dry-run]
|
||||
原 null 索引: 24,25
|
||||
⚠ K 个 __id__ 引用原本指向 null 槽位(已置 null):(如有)
|
||||
[0].xxxRef → __id__:24
|
||||
```
|
||||
|
||||
**dangling 引用警告**:如果 prefab 里某个 `__id__:N` 指向了被删的 null 槽位("软删 + 引用残留"场景),compact 会把它置为 `__id__: null` 并打印警告。一般情况下 null 槽位都是孤儿(没人引用),dangling = 0;如果 > 0 需要人工排查那条引用是否本来就该解开。
|
||||
|
||||
例:清两个历史 null prefab
|
||||
```bash
|
||||
node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js compact-prefab \
|
||||
assets/packages/module/task/prefab/TaskRewardItem.prefab --dry-run
|
||||
# 输出: 29 → 27 (清掉 2 个 null)
|
||||
|
||||
# 批量扫整个项目(shell 循环 + dry-run)
|
||||
for f in $(find assets -name "*.prefab"); do
|
||||
node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js compact-prefab "$f" --dry-run 2>&1 | grep -v "无 null"
|
||||
done
|
||||
```
|
||||
|
||||
适用范围:只清 data 数组**顶层**的 null。Cocos build 还可能崩在「子节点字段里 null 引用」(如 `node._children[i] = null` 这种深层损坏),那种不是 compact-prefab 能修的,要靠 Cocos GUI 重新保存或 query+set 单点修复。
|
||||
|
||||
---
|
||||
|
||||
## 5. 节点定位三种形式
|
||||
|
||||
`node` / `parent` / `target` / `source` / `refNode` 都通用:
|
||||
|
||||
```js
|
||||
"itemList" // 名字(首个匹配)
|
||||
{ "id": 65 } // __id__(stub 节点必须用这个,见下方踩坑)
|
||||
{ "path": "Canvas/Main/itemList" } // DOM-like 路径,从根逐级下钻;同名节点多时用这个
|
||||
```
|
||||
|
||||
### 踩坑:stub 节点不能用字符串名定位
|
||||
|
||||
stub 节点(嵌套 prefab 实例)在 prefab JSON 里 `_name = ""`(空字符串),真实显示名挂在 `PrefabInstance.propertyOverrides` 里。**按字符串名查找会失败**:
|
||||
|
||||
```bash
|
||||
# ❌ Inspector 里看到的是 "board",但底层 _name=""
|
||||
{"op": "set-label-text", "node": "board", "text": "游戏说明", "labelNode": "title"}
|
||||
# → Error: 找不到节点 "board"
|
||||
|
||||
# ✅ 用 query --tree 先查 id,然后用 __id__ 定位
|
||||
{"op": "set-label-text", "node": {"id": 2}, "text": "游戏说明", "labelNode": "title"}
|
||||
```
|
||||
|
||||
适用于所有 op 的所有节点引用字段(`node` / `parent` / `target` 等),不限于 set-label-text。
|
||||
|
||||
---
|
||||
|
||||
## 6. Op 全表
|
||||
|
||||
26 个 op,按场景分组。`node` / `parent` / `source` / `target` 均支持上面三种定位形式。
|
||||
|
||||
### 6.1 节点字段
|
||||
|
||||
| op | 参数 | 说明 |
|
||||
|---|---|---|
|
||||
| `set-active` | `node`, `active: bool` | 切换节点显隐 |
|
||||
| `rename-node` | `node`, `name` | 改 `_name`。stub 走 propertyOverrides |
|
||||
| `set-position` | `node`, `x`, `y`, `z?` | 设置本地位置(绝对值) |
|
||||
| `adjust-position` | `node`, `dx?`, `dy?`, `dz?` | `_lpos` 相对偏移,免去先 query 取原值;任一轴缺省视为 0 |
|
||||
| `set-node-color` | `node`, `r?`, `g?`, `b?`, `a?` | 改节点 `_color` 分量(0-255),至少提供一个分量。不支持 stub |
|
||||
| `reorder-children` | `node`, `order` | 调整子节点顺序,影响 UI 渲染层级。`order` 是子节点名或 `{id:N}` 数组,**必须包含全部子节点** |
|
||||
|
||||
### 6.2 通用组件字段
|
||||
|
||||
| op | 参数 | 说明 |
|
||||
|---|---|---|
|
||||
| `set-component-field` | `node`, `componentType`, `property`, `value` | **普通节点**改任意组件任意字段。`property` 接字符串(顶层)或字符串数组(嵌套路径,如 `["_color","r"]`)。改 cc.Vec2/Vec3/Size 时 `value` 必须带 `__type__` |
|
||||
| `set-component-enabled` | `node`, `componentType`, `enabled`, `subNode?` | 改组件 `_enabled`,stub 走 propertyOverrides |
|
||||
| `set-nested-component-field` | `node`(stub), `componentType`, `property`, `value`, `subNode?` | **stub 节点**改内部任意组件的任意字段。`property` 支持字符串或嵌套路径数组。SpriteFrame 等资源 `value` 自备 `{__uuid__,__expectedType__}` |
|
||||
| `reset-overrides` | `node`(stub), `property?`, `componentType?`, `subNode?`, `all?` | 清 stub 已写入的 propertyOverrides。`all: true` 清空整个数组(一键回滚到嵌套默认);`property` 单条匹配——无 componentType = 节点字段(_lpos / _name 等),有 componentType = 嵌套内组件字段。幂等 |
|
||||
| `ensure-meta` | `path` | 给 `.ts` / `.json` 文件创建 `.meta`(v4 uuid + 按扩展名选模板)。`path` 可绝对或相对项目根。已存在时幂等。**典型用法**:新建脚本 → `ensure-meta` → `add-component` 同 batch 内联动,免等 cocos 编辑器异步 import。dry-run 时不写盘。同 batch 内自动 invalidate classid-resolver cache,让后续 op 能查到新 className |
|
||||
|
||||
### 6.3 cc.UITransform 便捷
|
||||
|
||||
| op | 参数 | 说明 |
|
||||
|---|---|---|
|
||||
| `set-anchor` | `node`, `x?`, `y?`, `compensatePosition?` | `_anchorPoint` 便捷写法。`compensatePosition: true` 时按 anchor 差值 × size 自动补偿 `_lpos`,保持节点视觉位置不变。**stub 节点支持**:自动走 propertyOverrides 改嵌套 UITransform;compensate 时 `oldA/size` 从嵌套 prefab 默认值读 |
|
||||
| `set-size` | `node`, `width?`, `height?` | `_contentSize` 便捷写法。**stub 节点支持**:自动走 propertyOverrides 改嵌套 UITransform |
|
||||
|
||||
### 6.4 cc.* 引擎组件多字段快捷 op
|
||||
|
||||
不含 stub 支持。改 stub 内同字段请用 `set-nested-component-field`。
|
||||
|
||||
| op | 参数 | 关键 enum |
|
||||
|---|---|---|
|
||||
| `set-label` | `node`, `text?`, `fontSize?`, `lineHeight?`, `overflow?`, `horizontalAlign?`, `verticalAlign?`, `bold?`, `italic?`, `underline?`, `enableWrapText?` | `overflow`: 0=NONE 1=CLAMP 2=SHRINK 3=RESIZE_HEIGHT 4=TRUNCATE |
|
||||
| `set-label-text` | `node`, `text`, `labelNode?` | 改 `_string`,stub 节点走 propertyOverrides;`labelNode` 指定嵌套 prefab 内持有 Label 的子节点名 |
|
||||
| `set-richtext` | `node`, `text?`, `maxWidth?`, `fontSize?`, `lineHeight?` | 支持 BBCode 标签 |
|
||||
| `set-sprite` | `node`, `sizeMode?`, `type?`, `grayscale?`, `trim?` | `type`: 0=SIMPLE 1=SLICED 2=TILED 3=FILLED 4=MESH。**换图用 `set-sprite-frame`** |
|
||||
| `set-sprite-frame` | `node`, `uuid: string`, `spriteNode?` | 替换 SpriteFrame uuid,stub 走 propertyOverrides |
|
||||
| `set-button` | `node`, `interactable?`, `transition?`, `zoomScale?`, `duration?` | `transition`: 0=NONE 1=COLOR 2=SPRITE 3=SCALE |
|
||||
| `set-editbox` | `node`, `inputMode?`, `maxLength?`, `placeholder?`, `string?`, `inputFlag?`, `fontSize?` | `inputMode`: 0=ANY 1=EMAIL 2=NUMERIC 3=PHONE 4=URL 5=DECIMAL 6=SINGLE_LINE |
|
||||
| `set-layout` | `node`, `type?`, `resizeMode?`, `paddingLeft?`, `paddingRight?`, `paddingTop?`, `paddingBottom?`, `spacingX?`, `spacingY?`, `startAxis?`, `constraint?`, `constraintNum?`, `affectedByScale?` | `type`: 0=NONE 1=HORIZONTAL 2=VERTICAL 3=GRID |
|
||||
|
||||
### 6.5 节点结构
|
||||
|
||||
| op | 参数 | 说明 |
|
||||
|---|---|---|
|
||||
| `add-node` | `parent`, `node: {name, lpos?, active?, components?, width?, height?, anchor?}` | 新增 cc.Node;parent 是 stub 时走 mountedChildren。`components: ["UITransform"]` 自动建 UITransform |
|
||||
| `remove-node` | `target` | 从父节点移除引用;元素本身保留(orphan),保持其他 `__id__` 稳定 |
|
||||
| `sync-nested-roots` | (无) | 重建根 `PrefabInfo.nestedPrefabInstanceRoots`,剔除「删了一半」残留的悬空嵌套实例根(节点 `_parent` 已移除但根登记残留 → 残留嵌套 prefab 的 asset 仍被当依赖加载,运行时 404)。只重写该数组,不删 elements、不动其他 `__id__`、不产生 null 槽;被孤立的残留对象成为不可达 orphan。复用 remove-node 内部同名逻辑 |
|
||||
| `clone-node` | `source`, `parent`, `name` | 深拷贝整棵子树,分配新 `__id__` + 新 fileId |
|
||||
|
||||
#### add-node 的 node 字段
|
||||
|
||||
| 字段 | 类型 | 默认 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `name` | string | 必填 | 节点名 |
|
||||
| `lpos` | [x,y,z] | [0,0,0] | 本地位置 |
|
||||
| `active` | bool | true | 是否激活 |
|
||||
| `components` | string[] | [] | `["UITransform"]` 自动创建 cc.UITransform |
|
||||
| `width` / `height` | number | 100 | UITransform 尺寸(仅 components 含时生效) |
|
||||
| `anchor` | [x,y] | [0.5,0.5] | UITransform 锚点 |
|
||||
|
||||
### 6.6 组件 / 引用
|
||||
|
||||
| op | 参数 | 说明 |
|
||||
|---|---|---|
|
||||
| `add-component` | `node`, `componentType`, `props?` | 在 `_components` 新挂一个组件 + 配套 CompPrefabInfo。`componentType` 支持 @ccclass 名(`"GMUI"`)、压缩 classId(`"a57b6RRA21B5I70mCpu1pBP"`)、引擎类(`"cc.Button"`) |
|
||||
| `remove-component` | `node`, `componentType` | 从普通节点 `_components` 移除指定组件引用。组件元素与其 CompPrefabInfo 作为 orphan 保留在数组里,保持其他 `__id__` 稳定(与 `remove-node` 同策略)。**不支持 stub**——嵌套 prefab 的组件归子 prefab 拥有,外层只能用 `set-component-enabled` 禁用 |
|
||||
| `set-component-ref` | `node`, `componentType`, `property`, `refNode`, `refType?`, `refSubNode?` | 给脚本组件 `@property` 挂节点 / 组件引用。详见 §6.7 |
|
||||
| `dedupe-component` | `node?` | 合并同节点上同语义但重复挂载的组件条目(cli 写 className + 编辑器 reimport 写压缩 classId 形成两份的场景)。按规范化 classId 分组,留字段非空数最多的为 keeper,losers 字段并入后删除并重映射 `__id__`。`node` 缺省扫整 prefab |
|
||||
|
||||
### 6.7 set-component-ref 完整字段
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `op` | string | ✓ | `"set-component-ref"` |
|
||||
| `node` | name / `{id}` / `{path}` | ✓ | 挂脚本的节点(持有 @property 字段) |
|
||||
| `componentType` | string | ✓ | 脚本组件类型,支持 @ccclass 名、压缩 classId、`cc.Button` 等 |
|
||||
| `property` | string | ✓ | @property 字段名(如 `"_role"`) |
|
||||
| `refNode` | name / `{id}` / `{path}` | ✓ | 要绑定的目标节点。**stub 必须用 `{id:N}`**(_name 为 null) |
|
||||
| `refType` | string | 可选 | 目标类型。省略 = 取 refNode 第一个非引擎组件;`"cc.Node"` = 绑节点本身(localID = 嵌套 prefab 根节点 PrefabInfo.fileId) |
|
||||
| `refSubNode` | string \| string[] | 可选 | stub 内部子节点定位。**字符串**指定单层 stub 的子节点名;**字符串数组**走多层嵌套(每层一段名字,最后一段配合 refType 决定终点) |
|
||||
|
||||
**常见拼错**:`"comp"` → `componentType`,`"ref"` → `refNode`。schema 校验会友好提示。
|
||||
|
||||
`refNode` 是 stub 时自动走 `cc.TargetOverrideInfo` 协议(详见 [`nested-prefab-protocol.md`](./nested-prefab-protocol.md))。
|
||||
|
||||
### 6.8 按场景速查
|
||||
|
||||
| 场景 | op |
|
||||
|---|---|
|
||||
| 改节点 _active / _name / _lpos | `set-active` / `rename-node` / `set-position`(普通+stub 通用) |
|
||||
| _lpos 相对偏移 | `adjust-position` |
|
||||
| cc.UITransform 锚点 + 自动补偿 lpos | `set-anchor`(带 `compensatePosition: true`) |
|
||||
| cc.UITransform 尺寸 | `set-size` |
|
||||
| 改普通节点任意组件字段(含嵌套路径) | `set-component-field` |
|
||||
| 改 stub 内任意组件字段 | `set-nested-component-field` |
|
||||
| 启用/禁用某组件 | `set-component-enabled` |
|
||||
| 改子节点渲染顺序 | `reorder-children` |
|
||||
| 一次改一批节点(按组件类型 / 名前缀 / 正则) | `bulk-set` |
|
||||
| 改 cc.Label / Sprite / Button / EditBox / Layout / RichText 多字段 | `set-label` / `set-sprite` / `set-button` / `set-editbox` / `set-layout` / `set-richtext` |
|
||||
| 改节点 _color | `set-node-color` |
|
||||
| 给脚本 @property 挂引用(节点 / 组件 / 单层 stub / 多层 stub) | `set-component-ref`(多层用 `refSubNode: ["A","B"]`) |
|
||||
| 加 / 删 / 复制节点 | `add-node` / `remove-node` / `clone-node` |
|
||||
| 加 / 删组件 | `add-component` / `remove-component` |
|
||||
| 合并重复组件 | `dedupe-component` |
|
||||
| 跨多 prefab 跑同一组 ops | `batch --glob` |
|
||||
| 比较两个 prefab | `diff` 子命令 |
|
||||
| 操作 .anim 文件 | `anim query` / `anim batch` |
|
||||
|
||||
---
|
||||
|
||||
## 7. Op 配方(按场景示例)
|
||||
|
||||
### 7.1 绑定 @property 到普通节点上的组件
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "set-component-ref",
|
||||
"node": "SettingUI",
|
||||
"componentType": "SettingUI",
|
||||
"property": "_btnClose",
|
||||
"refNode": "btnClose",
|
||||
"refType": "cc.Button"
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 绑定 @property 到嵌套 prefab(stub)内部的组件
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "set-component-ref",
|
||||
"node": "SettingUI",
|
||||
"componentType": "SettingUI",
|
||||
"property": "_someLabel",
|
||||
"refNode": {"id": 33},
|
||||
"refType": "cc.Label"
|
||||
}
|
||||
```
|
||||
|
||||
`refNode` 必须用 `{"id": N}`(stub `_name` 为 null)。CLI 自动在 `cc.PrefabInfo.targetOverrides` 写 `cc.TargetOverrideInfo`。
|
||||
|
||||
### 7.3 绑定 @property 到 stub 根节点本身(cc.Node 类型)
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "set-component-ref",
|
||||
"node": "SettingUI",
|
||||
"componentType": "SettingUI",
|
||||
"property": "_role",
|
||||
"refNode": {"id": 33},
|
||||
"refType": "cc.Node"
|
||||
}
|
||||
```
|
||||
|
||||
`refType: "cc.Node"` 表示绑定节点本身。CLI 找嵌套 prefab 根节点(`_parent === null`)的 `cc.PrefabInfo.fileId`,生成 `localID: [fileId]`。
|
||||
|
||||
### 7.4 多层嵌套 stub @property 挂载
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "set-component-ref",
|
||||
"node": "Main",
|
||||
"componentType": "MainUI",
|
||||
"property": "_innerLabel",
|
||||
"refNode": {"id": 12},
|
||||
"refType": "cc.Label",
|
||||
"refSubNode": ["B", "C"]
|
||||
}
|
||||
```
|
||||
|
||||
主 prefab stub → 第一层嵌套内的 stub `B` → 第二层嵌套内的 `C` 节点上的 `cc.Label`。每跨一层 PrefabInstance 边界 push 一个 fileId 到 localID 链。1 层时仍可写字符串 `"B"`,向后兼容。
|
||||
|
||||
### 7.5 改 stub 内部某组件的字段
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "set-nested-component-field",
|
||||
"node": {"id": 33},
|
||||
"componentType": "cc.Label",
|
||||
"property": "_string",
|
||||
"value": "新文字"
|
||||
}
|
||||
```
|
||||
|
||||
### 7.6 改普通节点 cc.UITransform 锚点 + 保持视觉位置不变
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "set-anchor",
|
||||
"node": "itemList",
|
||||
"y": 1,
|
||||
"compensatePosition": true
|
||||
}
|
||||
```
|
||||
|
||||
内部按 `lpos.y += height * (newAnchorY - oldAnchorY)` 自动调整位置。
|
||||
|
||||
### 7.7 改普通节点的组件字段(嵌套路径)
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "set-component-field",
|
||||
"node": "label",
|
||||
"componentType": "cc.Label",
|
||||
"property": ["_color", "r"],
|
||||
"value": 255
|
||||
}
|
||||
```
|
||||
|
||||
`property` 接字符串(顶层)或字符串数组(嵌套路径,逐级下钻)。中间路径不是对象会报错,不会自动建中间结构。
|
||||
|
||||
### 7.8 bulk-set:批量改一类节点
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "bulk-set",
|
||||
"selector": { "byComponent": "cc.Label" },
|
||||
"target": "component:cc.Label",
|
||||
"property": "_isItalic",
|
||||
"value": true
|
||||
}
|
||||
```
|
||||
|
||||
`selector` 可组合(AND):
|
||||
|
||||
- `{ byComponent: "cc.X" }`:节点上挂指定组件
|
||||
- `{ byNamePrefix: "btn" }`:`_name` 前缀匹配
|
||||
- `{ byNameRegex: "^icon_\\d+$" }`:正则
|
||||
|
||||
`target`:`"node"`(改节点字段)或 `"component:<Type>"`(改节点上某组件字段)。匹配 0 个不算错。**bulk-set 不处理 stub 节点**(避免代码路径混用)。
|
||||
|
||||
### 7.9 reorder-children
|
||||
|
||||
```json
|
||||
{"op": "reorder-children", "node": "list", "order": ["item3", "item1", "item2"]}
|
||||
```
|
||||
|
||||
`order` 必须包含全部子节点(数量校验),元素是 `string`(_name)或 `{id:N}`。
|
||||
|
||||
### 7.10 path 选择器(同名节点多 / id 不稳)
|
||||
|
||||
```json
|
||||
{"op": "set-active", "node": {"path": "Canvas/Main/itemList"}, "active": false}
|
||||
```
|
||||
|
||||
从根节点逐级按 `_name` 匹配 `_children`,遇 stub 不下钻。根节点名匹配第一段时可省略。同名段会取首个匹配。
|
||||
|
||||
### 7.11 dry-run 预览
|
||||
|
||||
```bash
|
||||
node bin/cocos-mcp-cli.js batch <prefab> ops.json --dry-run
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```json
|
||||
{
|
||||
"changed": false,
|
||||
"opsApplied": 2,
|
||||
"nodesAffected": ["itemList"],
|
||||
"dryRun": true,
|
||||
"diff": [
|
||||
{ "id": 65, "type": "cc.Node", "name": "itemList",
|
||||
"changes": { "_lpos.y": [-28, 412] } },
|
||||
{ "id": 66, "type": "cc.UITransform", "name": "",
|
||||
"changes": { "_anchorPoint.y": [0.5, 1] } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`diff` 是字段级,路径用点分(嵌套对象会展平)。
|
||||
|
||||
### 7.12 query 单字段值(脚本管道)
|
||||
|
||||
```bash
|
||||
node bin/cocos-mcp-cli.js query <prefab> --selector field \
|
||||
--name itemList --comp cc.UITransform --field _anchorPoint
|
||||
# → {"__type__": "cc.Vec2", "x": 0.5, "y": 1}
|
||||
```
|
||||
|
||||
### 7.13 query 树带组件字段
|
||||
|
||||
```bash
|
||||
node bin/cocos-mcp-cli.js query <prefab> --selector tree --with-comps
|
||||
```
|
||||
|
||||
每个节点附 `components: [{type, id, fields}]`,`fields` 过滤掉系统字段(`__type__` / `node` / `_enabled` / `__prefab` 等)后的业务字段。
|
||||
|
||||
---
|
||||
|
||||
## 8. 已知坑
|
||||
|
||||
### 坑 1:stub 节点 `_name` 为 null
|
||||
|
||||
prefab JSON 里 stub 节点的 `_name` 是 `null`(不是字符串)。名字来自 `cc.PrefabInstance.propertyOverrides`,运行时才填。
|
||||
|
||||
**做法**:先 `query --selector tree` 拿 `id`,然后用 `{"id": N}` 定位。
|
||||
|
||||
### 坑 2:入口文件错
|
||||
|
||||
- `bin/cocos-mcp-cli.js` ✓
|
||||
- `src/index.js` ✗(只是 re-export,无 CLI 入口)
|
||||
- `src/cli/main.js` ✗(导出 `main()` 但不自调用)
|
||||
|
||||
### 坑 3:localID 链多层嵌套
|
||||
|
||||
CLI 的 `resolveLocalIdChain`(`src/editor/nested.js`)支持 `refSubNode` 字符串数组路径走多层。但每层都假定能按节点名定位 stub;不支持靠组件类型在中间层定位。极端深嵌套场景仍建议走 tools pipeline(`step-3-script/bind-prefab-components.ts`)。
|
||||
|
||||
### 坑 4:root 节点检测依赖 `_parent === null`
|
||||
|
||||
`getNestedNodeFileId` 通过 `el._parent === null` 判断嵌套 prefab 的根节点。不是靠 `cc.PrefabInfo.asset.__uuid__`——嵌套 prefab JSON 里所有节点的 PrefabInfo.asset 都是 `{__id__: 0}`(in-file 引用),`__uuid__` 字段不存在。
|
||||
|
||||
### 坑 5:`sourceInfo` 为 null vs cc.TargetInfo
|
||||
|
||||
- `source`(挂 @property 的脚本组件)在主 prefab 根节点上 → `sourceInfo: null`
|
||||
- 脚本组件本身在某个 stub 内(`mountedComponents`)→ `sourceInfo` 要填 `cc.TargetInfo`。CLI 当前不支持,会抛错
|
||||
|
||||
### 坑 6:set-component-field vs set-nested-component-field 不通用
|
||||
|
||||
- **普通节点**改组件字段 → `set-component-field`(直改 elements 数组)
|
||||
- **stub 节点**改组件字段 → `set-nested-component-field`(写 PrefabInstance.propertyOverrides + 嵌套 prefab fileId)
|
||||
|
||||
用错时 CLI 抛明确错误:「节点 X 是 stub 代理,请用 set-nested-component-field」。判别 stub 看 `query --selector tree` 输出的 `isStub` 字段。
|
||||
|
||||
### 坑 7:cc.Vec2/Vec3/Size 写入必须带 `__type__`
|
||||
|
||||
`set-component-field` 改这类字段时 `value` 必须形如 `{"__type__": "cc.Vec2", "x": 0.5, "y": 1}`,缺 `__type__` 会被 Cocos 反序列化为普通对象,运行时 `getAnchorPoint()` 等 API 拿不到正确值。
|
||||
|
||||
`set-anchor` / `set-size` op 内部已带 `__type__`,无需自己处理。
|
||||
|
||||
### 坑 8:bulk-set 跳过 stub 节点
|
||||
|
||||
`bulk-set` 实现不处理 stub。要批量改 stub 内字段,先 `query --selector find` 拿 stub id 列表,再逐个 `set-nested-component-field`。
|
||||
|
||||
### 坑 9:reorder-children.order 必须含全部子节点
|
||||
|
||||
不允许只列要前置的几个、剩下的自动补尾。order 长度 ≠ _children 长度直接抛错。避免「你以为剩下的会按原序,但 CLI 默认丢掉了」这种隐式行为。
|
||||
|
||||
### 坑 10:path 选择器同名段必须消歧
|
||||
|
||||
`{path: "A/B/C"}` 走每段时如果 `A._children` 下有 ≥2 个同名 B,CLI 直接抛错并列出候选 `__id__`,**不静默取首个**。同名场景请用 `{id:N}`,或组合"父用 path、当前层用 id"。
|
||||
|
||||
### 坑 11:schema 类型校验(已加)
|
||||
|
||||
schema 校验既检查"字段拼写 + 必填存在"也检查字段类型。`width: "100"` 这种类型错跑前直接报,不会进 handler 才崩。复杂值(`value` / `props` / `selector` / `refSubNode`)走 `any`,仍由 handler 报场景错。
|
||||
|
||||
### 坑 12:className → 压缩 classId 自动规范化
|
||||
|
||||
Cocos 编辑器反序列化时 `__type__` 可填 @ccclass 名(`"GMUI"`)或压缩 classId(`"a57b6RRA21B5I70mCpu1pBP"`),但**保存 prefab 时会 round-trip 为后者**。如果 TS 脚本注册前触发 reimport,@property refs 会被丢,导致出现「字符串版 + 压缩版」两份组件。
|
||||
|
||||
CLI 两端防护:
|
||||
|
||||
1. **写入前**:`add-component` / `set-component-ref` 的 `componentType` / `refType` 自动扫 `assets/scripts` 反查 `.ts.meta` uuid,转 23 字符压缩 classId 写入。引擎类(`cc.*`/`sp.*`/`dragonBones.*`)和已压缩格式原样透传
|
||||
2. **写入后兜底**:用 `dedupe-component` op 合并已经被 round-trip 过的 prefab
|
||||
|
||||
### 坑 13:stub-node-field override 的 localID 用错 fileId
|
||||
|
||||
`set-position` / `rename-node` / `adjust-position` / `set-active` 等 op 对 stub 节点(嵌套 prefab 代理)写入字段时,走 `setOverrideProperty`,产物形如:
|
||||
|
||||
```json
|
||||
{ "__type__": "cc.TargetInfo", "localID": ["<某 fileId>"] }
|
||||
{ "__type__": "CCPropertyOverrideInfo", "targetInfo": { ... }, "propertyPath": ["_lpos"], "value": ... }
|
||||
```
|
||||
|
||||
**正确的 localID 是「嵌套 prefab 内部根节点 PrefabInfo.fileId」**,不是「外层主 prefab 里 stub 自己的 PrefabInfo.fileId」。Cocos 运行时 `generateTargetMap` 按嵌套 prefab 内 fileId 建 targetMap,外层 stub fileId 在 map 里查不到,override 静默失效。
|
||||
|
||||
早期 fgui→cc3 转出的 prefab 设计上让这两个 fileId 一致(PrefabBuilder 复用同一 fileId),所以本工具早期版本里用 stubFileId 巧合工作;手编 prefab 或重新设计的标杆 prefab(如 `common-new/button/btnClose.prefab`)两个 fileId 一般不同,必须读嵌套 prefab JSON 拿真实根 fileId。
|
||||
|
||||
CLI 行为(2026-05-20 修,`cli/src/overrides.js`):
|
||||
|
||||
1. **写入前**:`setOverrideProperty` 通过 `prefabInfo.asset.__uuid__` + `resolveUuidToPath` 加载嵌套 prefab,找根节点(`_parent === null`)的 `PrefabInfo.fileId` 作为 localID。**解析失败抛错**(不再 fallback 到 stubFileId),调用方需保证嵌套 prefab 可用。
|
||||
2. **历史脏数据自动矫正(一次性迁移)**:识别旧版 cli 写入的 `stubFileId` 形式条目,命中同 propertyPath 时把 localID 改写成真值。仓库里现存的 fgui→cc3 转出 prefab 跑一次新 cli 就会逐步收敛到真值,无需手工迁移。
|
||||
3. **`listOverrides` / `reset-overrides`**:只识别真值 localID(已迁移完的状态)。如果手工保留旧 stubFileId 条目不跑 cli 矫正,list/reset 会忽略它们。
|
||||
|
||||
诊断方法:在主 prefab JSON 里 grep `CCPropertyOverrideInfo` 找到目标条目,看 `targetInfo.localID[0]` 是否等于「嵌套 prefab 内根节点的 PrefabInfo.fileId」(在嵌套 prefab JSON 里直接 grep 根节点的 `fileId` 字段,或用 `query --selector overrides --id <stubId>` 看 cli 报告)。
|
||||
|
||||
### 坑 14:rootTargetOverrides 单字段 override 必须排在数组字段 override 之前
|
||||
|
||||
Cocos 加载 prefab 时遍历 `cc.PrefabInfo.targetOverrides` 数组应用 override。**实测 cocos 3.8.x 行为**:若数组前面有数组字段 override(`propertyPath = ["_items", N]`),数组后面的单字段 override(`propertyPath = ["_btnClose"]`)会被静默跳过,运行时 `ui._btnClose === null`。
|
||||
|
||||
最小可复现:TurnUI 有 14 个 `_items[N]` override,新 `set-component-ref _btnClose` 追加到数组末尾 → cocos 加载后 `ui._btnClose` 为 null。把这条 `_btnClose` 用 Python 移到数组首位 → 立即正常。对照实验同样的文件 mtime + reimport 流程,仅位置差异就触发不同结果,确认是顺序问题不是 reimport 时机。
|
||||
|
||||
CLI 防护(2026-05-20 修,`cli/src/editor/nested.js` `addRootTargetOverride`):
|
||||
|
||||
1. **新条目按 `propertyPath.length` 分插**:
|
||||
- `length === 1`(单字段,如 `["_btnClose"]`):`splice` 到第一个数组字段 override 之前
|
||||
- `length > 1`(数组字段,如 `["_items", 0]`):`push` 到末尾
|
||||
2. 已有的单字段 override 之间相对顺序不变(每次插入都在最后一个单字段之后、第一个数组字段之前)
|
||||
3. 历史脏数据需手工迁移(Python `pop(idx) + insert(0, ref)` 或者 `splice(arrayBoundary, 0, ref)`)
|
||||
|
||||
诊断方法:用 `query --selector tree` 拿 prefab 整体结构,然后看 root `cc.PrefabInfo.targetOverrides` 数组里第一个 `propertyPath.length > 1` 条目的位置——之前的所有条目(包括目标单字段)能正常加载,之后的单字段都会被跳过。
|
||||
|
||||
---
|
||||
|
||||
## 9. 验证 targetOverrides 已写入
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import json
|
||||
data = json.load(open('assets/packages/common/setting/ui/SettingUI.prefab'))
|
||||
for el in data:
|
||||
if isinstance(el, dict) and el.get('__type__') == 'cc.PrefabInfo' and el.get('rootUuid'):
|
||||
print('targetOverrides count:', len(el.get('targetOverrides', [])))
|
||||
break
|
||||
"
|
||||
```
|
||||
|
||||
或 jq:
|
||||
|
||||
```bash
|
||||
jq '[.[] | select(.__type__ == "cc.TargetOverrideInfo")] | length' \
|
||||
assets/packages/common/setting/ui/SettingUI.prefab
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 协议参考
|
||||
|
||||
- `cc.TargetOverrideInfo` 协议细节:[`nested-prefab-protocol.md`](./nested-prefab-protocol.md)
|
||||
- prefab JSON 结构速查:[`prefab-schema.md`](./prefab-schema.md)
|
||||
- offline vs 编辑器路径决策:[`prefab-direct-edit.md`](./prefab-direct-edit.md)
|
||||
- `.anim` 文件结构:[`anim-schema.md`](./anim-schema.md)
|
||||
|
||||
多层嵌套 localID 链的 tools pipeline 实现:`tools/step-3-script/bind-prefab-components.ts` 的 `resolveLocalIdChain`。
|
||||
|
||||
---
|
||||
|
||||
## 11. 源码导航
|
||||
|
||||
```
|
||||
extensions/cc-3-8-x-mcp/cli/
|
||||
├── bin/cocos-mcp-cli.js # CLI 入口(require src/cli/main.js)
|
||||
└── src/
|
||||
├── index.js # 公开 API re-export(parsePrefab / writePrefab / editPrefab / queryPrefab / ...)
|
||||
├── parse.js / write.js # JSON 数组 + __id__ 引用格式的读写
|
||||
├── id.js # deterministic fileId / classId 压缩
|
||||
├── primitives.js # cc.Node / cc.PrefabInfo / cc.UITransform 等节点对象构造原语
|
||||
├── overrides.js # cc.PrefabInstance.propertyOverrides 读写
|
||||
├── classid-resolver.js # @ccclass 名 → 压缩 classId(扫 assets/scripts/*.ts.meta)
|
||||
├── uuid-resolver.js # uuid → 磁盘路径(扫 assets/**/*.prefab)
|
||||
├── anim-primitives.js # .anim 文件构造原语
|
||||
│
|
||||
├── editor/ # editPrefab 主入口 + op handler
|
||||
│ ├── index.js # editPrefab + OP_HANDLERS 注册表
|
||||
│ ├── helpers.js # resolveNode(name/id/path)/ findComponent / isStub / normalizeComponentType
|
||||
│ ├── nested.js # stub / 嵌套 prefab fileId 协议(多层支持)
|
||||
│ ├── id-utils.js # fileId 分配 / 子树断开 / __id__ 重映射
|
||||
│ ├── diff.js # 字段级 diff(dry-run + diff 子命令共用)
|
||||
│ ├── op-schema.js # ops 跑前 schema 校验
|
||||
│ └── ops/ # 26 个 op handler,一文件一个
|
||||
│
|
||||
├── query/ # 只读查询
|
||||
│ ├── index.js / tree.js / node.js / find.js / field.js
|
||||
│ └── comp-fields.js
|
||||
│
|
||||
└── cli/ # 命令行子命令分发
|
||||
├── main.js / flags.js / help.js
|
||||
├── query-cmd.js / set-cmd.js
|
||||
├── batch-cmd.js # 含 --glob 实现
|
||||
├── anim-cmd.js
|
||||
└── diff-cmd.js
|
||||
```
|
||||
|
||||
### 加新 op 流程
|
||||
|
||||
1. `src/editor/ops/<new-op>.js` 写 handler,导出 `execXxx`
|
||||
2. `src/editor/index.js` 在 `OP_HANDLERS` 加一行 + import
|
||||
3. `src/editor/op-schema.js` 在 `SCHEMAS` 登记必填 / 可选字段
|
||||
4. 本文档 §6(op 全表)+ §6.8(按场景速查)补一行
|
||||
5. `test/api.test.js` 加测试用例
|
||||
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
# 6 个测试文件 116 个用例
|
||||
for f in extensions/cc-3-8-x-mcp/cli/test/*.test.js; do
|
||||
node --test "$f" 2>&1 | grep -E "^# (pass|fail)"
|
||||
done
|
||||
```
|
||||
@@ -0,0 +1,216 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,114 @@
|
||||
# Prefab 直改流程指南
|
||||
|
||||
> 目标读者:使用 Claude Code / MCP agent 操作 CC3.8.x prefab 的开发者。
|
||||
> 本文聚焦**决策和流程**:什么时候用哪条路径、改完要做什么、有哪些已知坑。
|
||||
> prefab 文件结构细节见 [prefab-schema.md](./prefab-schema.md),不在此重复。
|
||||
|
||||
---
|
||||
|
||||
## 为什么绕过 Cocos 编辑器直改 prefab 文件
|
||||
|
||||
Cocos Creator 编辑器将 prefab 序列化为 JSON 文件存储在磁盘上。传统工作流需要打开 GUI → 在场景树里手动操作 → 保存,这在以下场景无法工作:
|
||||
|
||||
- **无头环境**:CI / MCP agent / 自动化脚本,没有显示器或不方便启动编辑器。
|
||||
- **批量修改**:几十个 prefab 同步改文字、布局参数,手动操作成本极高。
|
||||
- **可重复性**:工具链幂等产物(A/B 变体、多语言注入),需要确定性结果而非 GUI 点击。
|
||||
|
||||
直改文件可以做到零 GUI 依赖、秒级完成、可 diff 审计。代价是需要精确理解 prefab 格式,错误写入可能让编辑器报解析错误或静默破坏数据。
|
||||
|
||||
---
|
||||
|
||||
## 整体架构
|
||||
|
||||
```
|
||||
Claude Code / MCP agent
|
||||
│
|
||||
▼
|
||||
router.js(cc-3-8-x-mcp)
|
||||
│
|
||||
├── offline tools(via cli 模块,纯文件 I/O)
|
||||
│ ├── prefab_query → cli/src/parse.js + query.js
|
||||
│ ├── prefab_edit → cli/src/write.js + overrides.js
|
||||
│ └── prefab_batch → cli/src/batch.js
|
||||
│
|
||||
└── editor tools(via HTTP → Cocos 扩展进程)
|
||||
├── asset_reimport / asset_refresh
|
||||
├── scene_query / scene_set_property
|
||||
├── preview_screenshot
|
||||
└── editor_eval
|
||||
```
|
||||
|
||||
**offline tools**:读写本地 `.prefab` 文件,不需要编辑器在线。适合批量、自动化场景。
|
||||
|
||||
**editor tools**:通过 HTTP 与运行中的 Cocos Creator 编辑器通信(扩展监听固定端口)。可以操作运行时场景、触发资源重新导入、截图等。需要编辑器已启动且扩展已加载。
|
||||
|
||||
---
|
||||
|
||||
## 决策表:用 offline 还是 editor?
|
||||
|
||||
| 操作 | 推荐路径 | 原因 |
|
||||
|---|---|---|
|
||||
| 改普通节点 position / active | **offline**(prefab_edit) | 直接改文件字段,秒级完成 |
|
||||
| 改普通节点 Label 文字 | **offline**(prefab_edit) | 同上 |
|
||||
| 改普通节点 SpriteFrame | **offline**(prefab_batch,op: set-sprite-frame) | `set` 子命令不支持,必须走 batch |
|
||||
| 改 stub 节点普通字段(position / active) | **offline**(prefab_edit) | 工具自动路由到 propertyOverrides |
|
||||
| 改 stub 节点组件字段(Label._string / Sprite._spriteFrame) | **当前不支持** | 需跨 prefab 读取组件 fileId,未实现;必须在编辑器里手改 |
|
||||
| 查节点树结构 | **offline**(prefab_query)更快 | 纯文件解析,无需编辑器在线;但看不到运行时动态状态 |
|
||||
| 查运行时节点状态(动画、数据绑定后) | **editor**(scene_query) | 运行时才有真实数据 |
|
||||
| 触发资源重新导入 | **editor**(asset_reimport) | 必须让编辑器重建 UUID 索引 |
|
||||
| 预览截图 | **editor**(preview_screenshot) | 只有编辑器能渲染 |
|
||||
| 在预览中执行 JS | **editor**(editor_eval) | 需要运行时上下文 |
|
||||
| 改运行时场景节点(非 prefab 文件) | **editor**(scene_set_property) | 场景节点不存在离线文件可直改 |
|
||||
|
||||
---
|
||||
|
||||
## offline 改完后的操作流程
|
||||
|
||||
直接改 `.prefab` 文件后,编辑器需要重新加载才能感知变化。有两种方式:
|
||||
|
||||
### 方式 A:仅重新导入资产
|
||||
|
||||
```
|
||||
editor tool: asset_reimport
|
||||
参数: { "path": "assets/packages/game/home/ui/HomeUI.prefab" }
|
||||
```
|
||||
|
||||
让编辑器重建该 prefab 的内部索引(UUID 映射、子资产列表)。适合只改了 prefab 内容、不涉及其他资产变动的情况。
|
||||
|
||||
### 方式 B:整链路刷新(改动较大时)
|
||||
|
||||
```
|
||||
editor tool: preview_refresh_and_reload
|
||||
```
|
||||
|
||||
触发编辑器重新加载所有修改过的资产并刷新预览。改动多个文件时用此方式,避免缓存不一致。
|
||||
|
||||
**重要**:offline 改完不做 reimport,编辑器下次保存 prefab 时可能用旧的序列化数据覆盖你的修改。
|
||||
|
||||
---
|
||||
|
||||
## 踩坑清单
|
||||
|
||||
### 坑 1:stub 节点直改字段永远无效
|
||||
|
||||
嵌套 prefab 的 stub 节点在父 prefab 中通常只有五个字段(`__type__`、`_objFlags`、`_parent`、`_prefab`、`__editorExtras__`),其余属性全在 `cc.PrefabInstance.propertyOverrides` 里。直接改 stub 节点的 `_lpos` 或 `_active` 字段,编辑器加载时会用 propertyOverrides 覆盖回去,修改静默丢失。
|
||||
|
||||
→ offline 工具已自动处理:检测到 stub 节点时自动路由到 propertyOverrides 写入。
|
||||
→ 手写 ops.json 绕过高层 API 时,必须自己判断 `instance !== null` 再决定写哪里。
|
||||
→ 详见 [prefab-schema.md § 4](./prefab-schema.md)。
|
||||
|
||||
### 坑 2:nestedPrefabInstanceRoots 不同步导致静默数据损坏
|
||||
|
||||
根节点 `cc.PrefabInfo.nestedPrefabInstanceRoots` 是 CC3 编辑器识别所有嵌套 prefab stub 的总索引。新增嵌套实例时如果忘记把 stub 节点引用加进这个数组,编辑器不会报错,但在下次保存 prefab 时会把嵌套引用关系覆盖掉,造成子组件引用丢失。
|
||||
|
||||
→ offline 工具的 `editPrefab` 路径已自动维护该列表。
|
||||
→ 手动拼 op 绕过高层 API 时必须自己维护。
|
||||
→ 详见 [prefab-schema.md § 4 坑二](./prefab-schema.md) 和 cli/README.md 地雷二。
|
||||
|
||||
### 坑 3:stub 节点组件字段当前不支持离线写入
|
||||
|
||||
覆写 stub 节点的组件字段(如 `cc.Label._string`、`cc.Sprite._spriteFrame`)需要跨 prefab 文件读取该组件的 `cc.CompPrefabInfo.fileId`,当前 offline 工具未实现该能力。调用时会抛 `unsupported` 错误并不落盘。
|
||||
|
||||
**当前 workaround**:在 Cocos 编辑器里手动改这类属性,或通过 `editor tool: scene_set_property` 在运行时改(重启后失效)。后续版本将在 overrides.js 中实现跨 prefab fileId 查找。
|
||||
|
||||
→ 详见 cli/README.md 地雷一。
|
||||
@@ -0,0 +1,358 @@
|
||||
# CC 3.8.x Prefab 文件结构速查表
|
||||
|
||||
> 目标读者:写 cocos-mcp-cli 直接读写 .prefab 文件的 AI / 工具开发者。
|
||||
> 不是 Cocos 官方文档的复读;只讲这个仓库里 prefab 实际长什么样、改它要注意什么。
|
||||
> 样本来源:HomeUI.prefab / MergeUI.prefab / PassBar2.prefab / LoadingUI.prefab。
|
||||
|
||||
---
|
||||
|
||||
## 1. 文件总体结构:顶层数组 + `__id__` 交叉引用
|
||||
|
||||
### 例子
|
||||
|
||||
```json
|
||||
[
|
||||
{ "__type__": "cc.Prefab", "_name": "PassBar2", "data": { "__id__": 1 } },
|
||||
{ "__type__": "cc.Node", "_name": "PassBar2", "_children": [{"__id__": 2}], "_components": [{"__id__": 16}], "_prefab": {"__id__": 22} },
|
||||
{ "__type__": "cc.Node", "_name": "bar", "_parent": {"__id__": 1}, ... },
|
||||
...
|
||||
{ "__type__": "cc.UITransform", "node": {"__id__": 2}, ... },
|
||||
{ "__type__": "cc.CompPrefabInfo", "fileId": "xGXA9SOZ9EEId1SlOOZ8ww" },
|
||||
...
|
||||
{ "__type__": "cc.PrefabInfo", "root": {"__id__": 1}, "asset": {"__id__": 0}, "fileId": "VvRjmVLAOqp/Xn7PjxOSlg", "instance": null, "nestedPrefabInstanceRoots": null }
|
||||
]
|
||||
```
|
||||
|
||||
### 说明
|
||||
|
||||
整个文件是一个 **JSON 数组**,每个元素称为一个"对象(object)"。对象之间的引用全部用 `{ "__id__": N }` 表达,N 是该数组下标(0-based)。没有任何嵌套层级——所有节点、组件、Prefab 元信息都平铺在同一层数组里。
|
||||
|
||||
- 下标 0:永远是 `cc.Prefab` 资产描述对象,`data` 字段指向根 Node 的 `__id__`。
|
||||
- 下标 1:通常是根 `cc.Node`(`_parent: null`)。
|
||||
- 其余下标:子 Node、组件、PrefabInfo、CompPrefabInfo、CCPropertyOverrideInfo 等,顺序大致遵循"先父后子、先节点后组件"但并非强约束。
|
||||
|
||||
### 坑
|
||||
|
||||
**读一个 `__id__` 就等于对数组做随机访问,绝对不要假设某类型在固定下标。** 必须先从 `cc.Prefab.data.__id__` 找根节点,再沿 `_children`、`_components`、`_prefab` 递归遍历。
|
||||
|
||||
---
|
||||
|
||||
## 2. 常见 `__type__` 清单与关键字段
|
||||
|
||||
| `__type__` | 职责 | 关键字段 |
|
||||
|---|---|---|
|
||||
| `cc.Prefab` | 文件根描述 | `_name`, `data.__id__`(→ 根 Node), `optimizationPolicy`, `persistent` |
|
||||
| `cc.Node` | 场景节点 | `_name`, `_parent`, `_children[]`, `_components[]`, `_prefab`, `_lpos`, `_lrot`, `_lscale`, `_euler`, `_layer`, `_active`, `_objFlags` |
|
||||
| `cc.PrefabInfo` | 节点的 prefab 元信息 | `root`, `asset`, `fileId`, `instance`(→ PrefabInstance 或 null), `targetOverrides`, `nestedPrefabInstanceRoots` |
|
||||
| `cc.CompPrefabInfo` | 组件的 prefab 元信息 | `fileId`(该组件在 prefab 内的唯一 ID) |
|
||||
| `cc.PrefabInstance` | 嵌套 prefab 实例描述 | `fileId`, `prefabRootNode.__id__`(恒为 1,即被嵌套 prefab 的根), `mountedChildren[]`, `mountedComponents[]`, `propertyOverrides[]`, `removedComponents[]` |
|
||||
| `cc.TargetInfo` | 覆写目标定位 | `localID: [fileId字符串]`(对应被嵌套节点/组件的 fileId) |
|
||||
| `CCPropertyOverrideInfo` | 单条属性覆写 | `targetInfo.__id__`, `propertyPath: string[]`, `value` |
|
||||
| `cc.UITransform` | 尺寸/锚点 | `node.__id__`, `_contentSize`, `_anchorPoint`, `__prefab.__id__`(→ CompPrefabInfo) |
|
||||
| `cc.Sprite` | 图片渲染 | `node.__id__`, `_spriteFrame.__uuid__`(格式 `uuid@f9941`), `_type`(0=SIMPLE/3=FILLED), `_color`, `_isTrimmedMode` |
|
||||
| `cc.Label` | 文字 | `node.__id__`, `_string`, `_fontSize`, `_horizontalAlign`, `_overflow`, `_font.__uuid__`, `_enableOutline`, `_outlineColor`, `_outlineWidth` |
|
||||
| `cc.Widget` | 布局对齐 | `node.__id__`, `_alignFlags`(位掩码), `_left/_right/_top/_bottom`, `_isAbsLeft` 等, `_alignMode` |
|
||||
| `cc.Button` | 按钮 | `node.__id__`, `_interactable`, `_transition`(0=NONE/1=COLOR/2=SPRITE/3=SCALE), `_normalColor`, `_zoomScale`, `_target`, `clickEvents[]` |
|
||||
| `cc.ProgressBar` | 进度条 | `node.__id__`, `_barSprite.__id__`(→ 子节点上的 cc.Sprite 组件), `_mode`(0=H/1=V/2=FILLED), `_totalLength`, `_progress` |
|
||||
| `cc.ScrollView` | 滚动视图 | `node.__id__`, `horizontal`, `vertical`, `elastic`, `inertia`, `_content`(→ 内容节点) |
|
||||
| `cc.Layout` | 自动布局 | `node.__id__`, `_layoutType`(0=NONE/1=H/2=V/3=GRID), `_resizeMode`, `_paddingLeft/_Right/_Top/_Bottom`, `_spacingX/_Y` |
|
||||
| `cc.Mask` | 遮罩 | `node.__id__`, `_type`(0=RECT/1=ELLIPSE/2=GRAPHICS_STENCIL) |
|
||||
| `cc.ProgressBar` | 进度条 | 同上 |
|
||||
| `cc.RichText` | 富文本 | `node.__id__`, `_string`(支持 BBCode), `_fontSize`, `_maxWidth` |
|
||||
|
||||
自定义组件(如 `fca1cfQuOlKb5w9Ll8YoEt8`):`__type__` 是 UUID 压缩串而非 `cc.xxx`,见第 6 节。
|
||||
|
||||
---
|
||||
|
||||
## 3. Node 的核心字段详解
|
||||
|
||||
### 例子(PassBar2.prefab 根节点)
|
||||
|
||||
```json
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "PassBar2",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": null,
|
||||
"_children": [{"__id__": 2}, {"__id__": 10}],
|
||||
"_active": true,
|
||||
"_components": [{"__id__": 16}, {"__id__": 18}, {"__id__": 20}],
|
||||
"_prefab": {"__id__": 22},
|
||||
"_lpos": {"__type__": "cc.Vec3", "x": 0, "y": 0, "z": 0},
|
||||
"_lrot": {"__type__": "cc.Quat", "x": 0, "y": 0, "z": 0, "w": 1},
|
||||
"_lscale": {"__type__": "cc.Vec3", "x": 1, "y": 1, "z": 1},
|
||||
"_mobility": 0,
|
||||
"_layer": 33554432,
|
||||
"_euler": {"__type__": "cc.Vec3", "x": 0, "y": 0, "z": 0},
|
||||
"_id": ""
|
||||
}
|
||||
```
|
||||
|
||||
### 说明
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `_parent` | 父节点引用(`{__id__: N}` 或 `null`) |
|
||||
| `_children` | 子节点引用数组(顺序即渲染顺序,后面的在上层) |
|
||||
| `_components` | 该节点挂载的组件引用数组 |
|
||||
| `_prefab` | 指向 `cc.PrefabInfo` 对象(每个节点都有) |
|
||||
| `_lpos` | 本地位置,cc.Vec3(x/y/z) |
|
||||
| `_lrot` | 本地旋转,四元数 cc.Quat(x/y/z/w),w=1 表示无旋转 |
|
||||
| `_lscale` | 本地缩放,cc.Vec3,默认 (1,1,1) |
|
||||
| `_euler` | 对应 `_lrot` 的欧拉角(单位°),改旋转时两者必须同步 |
|
||||
| `_layer` | 渲染层,UI 节点固定为 `33554432`(= 1 << 25) |
|
||||
| `_active` | 是否显示(false = 隐藏,不等于 opacity=0) |
|
||||
| `_mobility` | 0=STATIC,1=STATIONARY,2=MOBILE,UI 通常 0 |
|
||||
| `_id` | 运行时 id(prefab 内节点均为 `""`,场景里才有值) |
|
||||
|
||||
### 坑
|
||||
|
||||
**`_lrot` 和 `_euler` 必须同时改**,否则编辑器打开后会用 `_lrot` 覆盖 `_euler` 或反之,产生意外旋转。即使只需要改 2D 旋转,也要同步写两处:
|
||||
|
||||
```json
|
||||
"_lrot": {"__type__": "cc.Quat", "x": 0, "y": 0, "z": 0.7071, "w": 0.7071},
|
||||
"_euler": {"__type__": "cc.Vec3", "x": 0, "y": 0, "z": 90}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. PrefabInstance 和 propertyOverrides:嵌套 prefab 改属性必须走这里
|
||||
|
||||
这是整个 prefab 格式中最反直觉的部分。当一个 prefab 引用另一个 prefab(嵌套)时,**引用方的节点树里没有子组件的字段**,改哪里都没用——属性全在 `cc.PrefabInstance.propertyOverrides` 里。
|
||||
|
||||
### 例子(HomeUI.prefab 引用 taskEntry 子 prefab)
|
||||
|
||||
```json
|
||||
// 嵌套 prefab 的 stub 节点(几乎空的)
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_objFlags": 0,
|
||||
"_parent": {"__id__": 1},
|
||||
"_prefab": {"__id__": 11},
|
||||
"__editorExtras__": {}
|
||||
}
|
||||
|
||||
// 该 stub 节点的 PrefabInfo(标识它是哪个 prefab 的实例)
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {"__id__": 10},
|
||||
"asset": {"__uuid__": "36cca336-1f01-4c37-8ff4-9effb9279c44", "__expectedType__": "cc.Prefab"},
|
||||
"fileId": "as0LdMaKxSWSLxrZB9u9KA",
|
||||
"instance": {"__id__": 12},
|
||||
"targetOverrides": null
|
||||
}
|
||||
|
||||
// PrefabInstance(propertyOverrides 在这里)
|
||||
{
|
||||
"__type__": "cc.PrefabInstance",
|
||||
"fileId": "yNEx5g/jVxdN7FE/+ittNA",
|
||||
"prefabRootNode": {"__id__": 1},
|
||||
"mountedChildren": [],
|
||||
"mountedComponents": [],
|
||||
"propertyOverrides": [{"__id__": 14}, {"__id__": 15}, ...],
|
||||
"removedComponents": []
|
||||
}
|
||||
|
||||
// TargetInfo:定位要改哪个节点/组件(用 localID 匹配 fileId)
|
||||
{
|
||||
"__type__": "cc.TargetInfo",
|
||||
"localID": ["as0LdMaKxSWSLxrZB9u9KA"]
|
||||
}
|
||||
|
||||
// CCPropertyOverrideInfo:实际的属性值
|
||||
{
|
||||
"__type__": "CCPropertyOverrideInfo",
|
||||
"targetInfo": {"__id__": 13},
|
||||
"propertyPath": ["_lpos"],
|
||||
"value": {"__type__": "cc.Vec3", "x": -272, "y": 53, "z": 0}
|
||||
}
|
||||
```
|
||||
|
||||
### 说明
|
||||
|
||||
引用链:stub Node → PrefabInfo(含 fileId + instance 引用)→ PrefabInstance → propertyOverrides[] → CCPropertyOverrideInfo(含 targetInfo)→ TargetInfo(localID = 被嵌套 prefab 内目标节点的 PrefabInfo.fileId)。
|
||||
|
||||
**localID 的值**来自被嵌套 prefab 文件内对应节点的 `cc.PrefabInfo.fileId`,两边必须完全一致。根节点的 localID 即 stub 节点自身 `cc.PrefabInfo.fileId`。
|
||||
|
||||
覆写组件属性时,TargetInfo.localID 填的是**该组件的 cc.CompPrefabInfo.fileId**(不是节点的 PrefabInfo.fileId)。
|
||||
|
||||
### 坑
|
||||
|
||||
**直接修改 stub 节点的字段(如 `_lpos`)对嵌套 prefab 完全无效**。编辑器加载时会用 propertyOverrides 覆盖回去。必须找到对应的 CCPropertyOverrideInfo 修改 value,或新增一条覆写记录。
|
||||
|
||||
---
|
||||
|
||||
## 5. MountedChildren / MountedComponents:动态挂载节点
|
||||
|
||||
`cc.PrefabInstance` 的 `mountedChildren` 和 `mountedComponents` 用于在嵌套 prefab 实例上**额外追加**节点或组件(原 prefab 定义里没有的)。
|
||||
|
||||
本仓库实际样本中这两个字段均为 `[]`(空数组)。如需 AI 工具新增动态挂载,需要:
|
||||
|
||||
1. 把新节点对象 push 进 prefab 数组,分配新的下标。
|
||||
2. 在 `mountedChildren` 里加入 `{__id__: 新节点下标}`。
|
||||
3. 新节点的 `_parent` 必须指向该 stub 节点的下标。
|
||||
4. 新节点也需要一个 `cc.PrefabInfo`(`instance: null`,`asset` 指向当前 prefab `{__id__: 0}`)。
|
||||
|
||||
该特性复杂且编辑器兼容性未经充分验证,**非必要不要动**。
|
||||
|
||||
---
|
||||
|
||||
## 5.5 TargetOverrides:主 prefab 脚本 @property 跨嵌套 prefab 挂载
|
||||
|
||||
`cc.PrefabInfo.targetOverrides`(注意是主 prefab **根**节点的 PrefabInfo,不是 stub 节点的)用于:主 prefab 里的某个脚本组件有 `@property` 字段要引用**嵌套 prefab 内部**的节点/组件。
|
||||
|
||||
最常见场景:fgui 转 cc3 后,父 prefab 里的按钮 / 容器节点都是 nested stub,真正的 `cc.Button` / `cc.Label` 在子 prefab 里,`BottomView.ts` 等脚本的 `@property` 要指向这些目标时,直接 `comp._btnStore = ref(stubId)` 会让 Cocos 加载时拿到 stub 代理而非真实组件。必须走 `cc.TargetOverrideInfo` + `cc.TargetInfo.localID`(fileId 数组),引擎 `applyTargetOverrides` 从代理节点的 targetInstance.targetMap 逐层查目标对象再挂回源字段。
|
||||
|
||||
协议细节、localID 多层语义、踩坑与离线工具接入见:[`nested-prefab-protocol.md`](./nested-prefab-protocol.md)。
|
||||
|
||||
cli 入口:`set-component-ref` op(refNode 是 stub 自动走 targetOverrides);批量场景由 `tools/step-3-script/bind-prefab-components.ts` 覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 6. fileId 和 UUID 的生成规律
|
||||
|
||||
### fileId(节点/组件在 prefab 内的唯一 ID)
|
||||
|
||||
格式:**base64 编码的 16 字节随机数**,去掉末尾 `=` 号,22~24 个字符,例如 `xGXA9SOZ9EEId1SlOOZ8ww`。
|
||||
|
||||
工具链的确定性生成方式(见 `tools/fgui2cc3/src/utils/DeterministicId.ts`):
|
||||
|
||||
```typescript
|
||||
export function deterministicFileId(seed: string): string {
|
||||
const hash = createHash('sha256').update(seed).digest();
|
||||
return hash.subarray(0, 16).toString('base64').replace(/=+$/, '');
|
||||
}
|
||||
```
|
||||
|
||||
种子格式:`${baseSeed}#fid#${counter++}`,相同种子产生相同 fileId,保证转换产物幂等。
|
||||
|
||||
**手工写 prefab 时**,可以用任意不重复的 base64 字符串,只要 prefab 内不冲突。重复 fileId 会导致编辑器覆写时定位错误。
|
||||
|
||||
### UUID(资产引用)
|
||||
|
||||
引用外部资产(图片/字体/其他 prefab)时用标准 UUID v4,例如 `36cca336-1f01-4c37-8ff4-9effb9279c44`。
|
||||
|
||||
SpriteFrame 的 uuid 格式是 `资产uuid@f9941`(`@f9941` 是固定后缀,指向 SpriteFrame 子资产)。
|
||||
|
||||
自定义组件脚本的 `__type__` 不是类名,而是**压缩 UUID**(PrefabBuilder.compressUuid):取 UUID hex 的前 5 字符 + 后 27 字符 base64 编码的前 18 字符,共 23 字符,例如 `fca1cfQuOlKb5w9Ll8YoEt8`。反过来查类名需要在 `.meta` 文件里找。
|
||||
|
||||
---
|
||||
|
||||
## 7. .meta 文件结构
|
||||
|
||||
### 例子(HomeUI.prefab.meta)
|
||||
|
||||
```json
|
||||
{
|
||||
"ver": "1.1.50",
|
||||
"importer": "prefab",
|
||||
"imported": true,
|
||||
"uuid": "f3bd038c-fb1d-4abc-9a13-dcbbb422458c",
|
||||
"files": [".json"],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"syncNodeName": "HomeUI"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 说明
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `uuid` | 该 prefab 的资产 UUID,其他 prefab 用 `__uuid__` 引用它时用此值 |
|
||||
| `importer` | 固定 `"prefab"` |
|
||||
| `ver` | 导入器版本,不要手改 |
|
||||
| `subMetas` | prefab 没有子资产,永远是 `{}` |
|
||||
| `userData.syncNodeName` | 编辑器同步节点名,改 prefab 根节点名后需同步更新 |
|
||||
|
||||
### 什么时候需要改 meta
|
||||
|
||||
通常**不需要动 meta**。以下是例外情况:
|
||||
|
||||
- 新建 prefab 文件(需要同时创建 .meta,uuid 必须唯一,否则编辑器报冲突)。
|
||||
- 改了 prefab 根节点的 `_name`(`userData.syncNodeName` 跟着改,否则编辑器显示名不对,不影响功能)。
|
||||
- **绝对不要改 uuid**——其他 prefab 通过 uuid 交叉引用,改了会让所有引用失效。
|
||||
|
||||
---
|
||||
|
||||
## 8. 写回时的陷阱
|
||||
|
||||
### 缩进
|
||||
|
||||
本仓库 prefab 统一使用 **2 空格缩进**。写回时:
|
||||
|
||||
```javascript
|
||||
const newRaw = JSON.stringify(arr, null, 2) + '\n';
|
||||
fs.writeFileSync(path, newRaw, 'utf8');
|
||||
```
|
||||
|
||||
末尾必须有一个换行符(`\n`),否则 git diff 会多出"no newline at end of file"警告,且编辑器可能重新格式化。
|
||||
|
||||
### key 顺序
|
||||
|
||||
`JSON.stringify` 的 key 顺序是插入顺序。CC3 编辑器在保存时有自己的 key 顺序(如 `_name` 在 `_objFlags` 前,`__editorExtras__` 紧随其后),但**编辑器加载并不依赖 key 顺序**,所以顺序不同不影响运行,但会产生较大 diff 噪声。
|
||||
|
||||
如果用工具批量修改后想减少 diff,应在读取时保留原始 key 顺序(不要 `JSON.parse` 再 `JSON.stringify`,而是用精确字符串替换或保留对象引用顺序)。
|
||||
|
||||
### 数组空洞
|
||||
|
||||
prefab 数组的某些下标可能在构建过程中被"占位后回填"(工具链内部用 `push(null)` 占位,再 `set(idx, obj)` 回填)。写回前必须确认数组中**没有 `null` 元素**(除非是故意的空槽——本仓库中不存在这种情况)。`JSON.stringify` 会将 `null` 序列化为 `null`,编辑器加载时会报引用错误。
|
||||
|
||||
### 编码
|
||||
|
||||
prefab 文件是 UTF-8,无 BOM。行尾是 LF(`\n`),不是 CRLF。在 Windows 环境写回时要特别注意(git 配置 `core.autocrlf` 可能干扰)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 安全改 vs 危险改
|
||||
|
||||
### 安全改(可直接修改,无连锁影响)
|
||||
|
||||
| 改动 | 字段 | 注意 |
|
||||
|---|---|---|
|
||||
| 移动节点位置 | `cc.Node._lpos` | 非嵌套节点直接改;嵌套节点改对应 CCPropertyOverrideInfo.value |
|
||||
| 改节点名称 | `cc.Node._name` | 非嵌套节点直接改;嵌套节点改对应 CCPropertyOverrideInfo |
|
||||
| 显隐节点 | `cc.Node._active` | 同上 |
|
||||
| 改文字内容 | `cc.Label._string` 或 `cc.RichText._string` | 直接改或通过 propertyOverride |
|
||||
| 改颜色 | `cc.Sprite._color` / `cc.Label._color` | 直接改 |
|
||||
| 改透明度 | `cc.UIOpacity._opacity` | 直接改(如有该组件) |
|
||||
| 改尺寸 | `cc.UITransform._contentSize` | 注意 Widget 约束可能覆盖 |
|
||||
| 改字号 | `cc.Label._fontSize` | 直接改 |
|
||||
| 改进度 | `cc.ProgressBar._progress` | 直接改 |
|
||||
| 改 SpriteFrame | `cc.Sprite._spriteFrame.__uuid__` | 必须用 `资产uuid@f9941` 格式 |
|
||||
|
||||
### 危险改(有连锁影响,必须同时处理多处)
|
||||
|
||||
| 改动 | 必须同步处理的地方 | 风险 |
|
||||
|---|---|---|
|
||||
| **新增普通节点** | 1. push Node 对象;2. 在父 Node._children 加引用;3. 新节点需要 cc.PrefabInfo(instance:null,asset:0,fileId 唯一);4. 根节点 PrefabInfo.nestedPrefabInstanceRoots 不变(普通节点不加) | 漏掉任一步编辑器会报错 |
|
||||
| **删除节点** | 1. 从父 _children 移除引用;2. 递归清理所有子节点及其组件;3. 检查是否有其他组件引用该节点(如 cc.Button._target)| 悬空 __id__ 引用会导致加载崩溃 |
|
||||
| **新增嵌套 prefab 实例** | 1. stub Node;2. cc.PrefabInfo(含 instance 引用);3. cc.PrefabInstance;4. cc.TargetInfo;5. 若干 CCPropertyOverrideInfo;6. 父节点 _children;7. 根节点 PrefabInfo.nestedPrefabInstanceRoots 加入该 stub 节点引用 | 漏掉 nestedPrefabInstanceRoots 编辑器不识别为嵌套 prefab |
|
||||
| **修改嵌套 prefab 属性** | 不改 stub 节点字段,改对应 CCPropertyOverrideInfo.value | 改 stub 节点本身的字段运行时完全无效 |
|
||||
| **新增组件** | 1. push 组件对象;2. push 对应 cc.CompPrefabInfo(fileId 唯一);3. 组件对象含 `__prefab: {__id__: CompPrefabInfo下标}`;4. 节点 _components 加引用 | 缺 CompPrefabInfo 编辑器加载报错 |
|
||||
| **改根节点名** | 同步改 .meta 的 userData.syncNodeName | 功能正常但编辑器显示名混乱 |
|
||||
| **数组覆写(如 editorPages)** | 先写 `{propertyPath: ['editorPages', 'length'], value: N}` 再逐元素写 `{propertyPath: ['editorPages', '0'], value: {__id__: X}}` | 漏掉 length 覆写 CC3 不会截断原数组 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 快速定位某节点的操作流程
|
||||
|
||||
1. `JSON.parse` 整个 prefab 数组为 `objects[]`。
|
||||
2. 从 `objects[0]`(cc.Prefab)的 `data.__id__` 取根节点下标(通常是 1)。
|
||||
3. 递归 `_children` 找目标节点(按 `_name` 或路径匹配)。
|
||||
4. 判断目标节点的 `_prefab.__id__` 对应的 PrefabInfo:
|
||||
- `instance === null` → 普通节点,直接改字段。
|
||||
- `instance !== null` → 嵌套 prefab stub,改 `cc.PrefabInstance.propertyOverrides` 里对应的 CCPropertyOverrideInfo.value。
|
||||
5. 序列化:`JSON.stringify(objects, null, 2) + '\n'`,写回文件。
|
||||
|
||||
---
|
||||
|
||||
## 附:本仓库 prefab 的两个最反直觉的坑
|
||||
|
||||
**坑一:嵌套 prefab 的 stub 节点没有 `_name`、`_lpos` 等字段,直接改无效。**
|
||||
stub 节点通常只有 `__type__`、`_objFlags`、`_parent`、`_prefab`、`__editorExtras__` 五个字段。所有可见属性(名称、位置、缩放、可见性)全部在 propertyOverrides 里,改 stub 本身什么都不会发生。第一次踩这个坑的人通常会以为"写入成功了但编辑器没变"。
|
||||
|
||||
**坑二:`nestedPrefabInstanceRoots` 只在根节点的 PrefabInfo 上,且必须列出所有嵌套 stub 节点。**
|
||||
这个字段是 CC3 编辑器识别"哪些节点是嵌套 prefab"的总索引。新增嵌套实例时如果忘记把 stub 节点加进这个数组,编辑器会把它当成普通节点处理,保存时会把原来的嵌套引用关系覆盖掉,造成静默数据损坏(没有报错,但再次打开 prefab 时子组件引用丢失)。
|
||||
Reference in New Issue
Block a user