1. AI 怎么知道节点的 UUID

1.1 AI 不可能凭空知道 UUID

每个 Cocos 场景中的节点都有一个唯一的 UUID(如 a5b3c2d1-e4f5-6789-abcd-ef0123456789),所有对节点的操作都需要这个 UUID。

但 AI 模型不可能事先知道这些 UUID,因为:

  • UUID 是 Cocos 引擎在创建节点时随机生成的
  • 每个项目、每个场景的 UUID 都不同
  • AI 模型没有读取 Cocos 编辑器内存的能力

所以 AI 的做法是:每次操作前先查询,再操作。

1.2 第一步:查询场景树

当用户说”把 Canvas 改成 200x100”时,AI 会先调用 get_scene_hierarchy 查询整个场景的节点树。

AI 发送的工具调用:

1
{ "name": "get_scene_hierarchy", "arguments": { "depth": 3 } }

scene-script 中的处理(src/scene-script.ts:70):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"get-hierarchy": function (event, args) {
const scene = cc.director.getScene(); // 拿到当前场景根节点

function dumpNodes(node, currentDepth) {
return {
name: node.name, // 节点名称,如 "Canvas"
uuid: node.uuid, // 节点 UUID ← AI 就是从这里拿到的
childrenCount: node.childrenCount,
children: [...] // 递归子节点
};
}

event.reply(null, dumpNodes(scene, 0));
}

返回给 AI 的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"name": "Scene",
"uuid": "67362fcd-xxxx",
"children": [
{
"name": "Canvas",
"uuid": "a5b3c2d1-e4f5-6789-abcd-ef0123456789",
"childrenCount": 3,
"components": ["Canvas", "Widget"],
"children": [
{ "name": "Background", "uuid": "b1c2d3e4-xxxx", "components": ["Sprite"] },
{ "name": "Title", "uuid": "c2d3e4f5-xxxx", "components": ["Label"] },
{ "name": "StartBtn", "uuid": "d3e4f5g6-xxxx", "components": ["Button"] }
]
},
{
"name": "Main Camera",
"uuid": "18g4J2xx-xxxx",
"components": ["Camera"]
}
]
}

AI 看到这个树,就知道了:Canvas 的 UUID 是 a5b3c2d1-e4f5-6789-abcd-ef0123456789

1.3 第二步:拿着 UUID 去操作

AI 拿到 UUID 后,发起第二次工具调用:

1
2
3
4
5
6
7
8
{
"name": "update_node_transform",
"arguments": {
"id": "a5b3c2d1-e4f5-6789-abcd-ef0123456789",
"width": 200,
"height": 100
}
}

scene-script 收到后,通过 cc.engine.getInstanceById(id) 找到节点对象,直接修改属性:

1
2
node.width = 200;
node.height = 100;

完整时序:

1
2
3
4
5
6
7
8
9
10
11
12
13
用户: "把 Canvas 改成 200x100"


AI 思考: 我不知道 Canvas 的 UUID,先查

├─ 第一次调用: get_scene_hierarchy
│ → 返回场景树 → AI 从中找到 Canvas 的 UUID

├─ 第二次调用: update_node_transform(id: "a5b3c2d1-xxxx", width: 200, height: 100)
│ → scene-script 通过 UUID 找到节点 → 修改 width/height → 成功


AI 回复: "已将 Canvas 尺寸修改为 200x100"

1.4 工具描述中强制约束了这个流程

src/tools/ToolRegistry.ts 中,写操作工具的 description 里嵌入了安全守则:

1
2
3
4
{
name: "update_node_transform",
description: "【AI 安全守则】: 1. 执行任何写操作前必须先通过 get_scene_hierarchy 验证主体存在..."
}

这段描述会被注入到 AI 模型的上下文中,模型会遵循这个规则——先查再改,不盲猜 UUID


2. 创建预制体的完整流程

2.1 AI 的操作顺序

用户说:”把场景里的 Player 节点保存为预制体”

AI 会分两步操作:

1
2
① get_scene_hierarchy → 拿到 Player 节点的 UUID
② create_prefab(nodeId: "xxx", prefabName: "Player")

create_prefab 内部经过三个阶段完成。

2.2 阶段一:scene-script 序列化节点

对应源码: src/scene-script.ts:1548

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"create-prefab": function (event, args) {
const { nodeId, nodeName } = args;

// 1. 通过 UUID 找到场景中的节点对象
const node = findNode(nodeId);

// 2. 保存原始状态
const originalParent = node.parent;
const originalName = node.name;

// 3. 临时把节点从场景树上摘下来
// 原因:Editor.serialize 会从场景根开始序列化整棵树
// 摘下来后,它只会序列化这个节点及其子树
node.parent = null;
if (nodeName) node.name = nodeName;

// 4. 调用 Cocos 编辑器的序列化 API
const serializedStr = Editor.serialize(node);

// 5. 序列化完成后立刻恢复节点(用户无感知)
node.parent = originalParent;
node.name = originalName;
}

为什么要临时摘下节点?

1
2
3
4
5
6
7
8
9
10
不摘下:
Editor.serialize(node)
→ 序列化整个场景树(包含所有无关节点)
→ 生成的预制体会包含整个场景的数据(错误)

摘下后:
node.parent = null;
Editor.serialize(node)
→ 只序列化 node 自身及其子节点
→ 生成干净的预制体数据(正确)

2.3 阶段二:场景格式转预制体格式

对应源码: src/scene-script.ts:1582-1676

Editor.serialize 输出的是场景格式,需要转换成预制体格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
场景格式 (Editor.serialize 输出):
┌───────────────────────────────────┐
│ [0] cc.Node (根节点) │
│ [1] cc.Scene │ ← 预制体不需要这个
│ [2] cc.Node (子节点) │
│ [3] cc.Sprite (组件) │
│ [4] cc.Button (组件) │
└───────────────────────────────────┘

↓ 转换 ↓

预制体格式 (最终 .prefab 文件):
┌───────────────────────────────────┐
│ [0] cc.Prefab (包装器) │ ← 新增
│ [1] cc.Node (根节点) │
│ [2] cc.Node (子节点) │
│ [3] cc.Sprite (组件) │
│ [4] cc.Button (组件) │
│ [5] cc.PrefabInfo (根节点信息) │ ← 新增
│ [6] cc.PrefabInfo (子节点信息) │ ← 新增
└───────────────────────────────────┘

转换过程中做了四件事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 删除 cc.Scene 对象
→ 预制体不是场景,不需要 Scene 节点

2. 在数组开头插入 cc.Prefab 包装器
→ 告诉 Cocos 引擎"这是一个预制体"
→ 包含 data 字段指向根节点: { __id__: 1 }

3. 给每个 cc.Node 生成 cc.PrefabInfo
→ 记录节点在预制体中的 fileId(22 位随机标识)
→ 标记哪个是根节点(sync: 2)、哪些是子节点(sync: 0)

4. 重新计算所有 __id__ 引用索引
→ 因为删了 Scene、加了 Prefab,数组索引全变了
→ 所有 { __id__: N } 引用都需要重新映射

cc.Prefab 包装器长这样:

1
2
3
4
5
6
7
8
9
10
{
"__type__": "cc.Prefab",
"_name": "",
"_objFlags": 0,
"_native": "",
"data": { "__id__": 1 },
"optimizationPolicy": 0,
"asyncLoadAssets": false,
"readonly": false
}

cc.PrefabInfo 长这样:

1
2
3
4
5
6
7
{
"__type__": "cc.PrefabInfo",
"root": { "__id__": 1 },
"asset": { "__id__": 0 },
"fileId": "abCDeFgHiJkLmNoPqRsT+/",
"sync": false
}

2.4 阶段三:主进程写入文件

序列化数据从 scene-script 返回到主进程后,进入 ToolDispatcher._createPrefabViaSceneScriptsrc/tools/ToolDispatcher.ts:773):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static _createPrefabViaSceneScript(nodeId, prefabUrl, nodeName, callback) {
// 1. 调用 scene-script 序列化节点(阶段一 + 阶段二)
callSceneScript("mcp-bridge", "create-prefab", { nodeId, nodeName }, (err, serializedData) => {

// 2. 写入磁盘
// prefabUrl: "db://assets/Player.prefab"
// → 转为物理路径: "/项目目录/assets/Player.prefab"
// → 把 JSON 字符串写入文件
AssetPatcher.safeCreateAsset(prefabUrl, serializedData, callback, (doneCreate) => {

// 3. 修复根节点 fileId(用 crypto 生成更安全的随机 ID)
const prefabFspath = Editor.assetdb.urlToFspath(prefabUrl);
ToolDispatcher.fixPrefabRootFileId(prefabFspath);

// 4. 通知 Cocos 资源库刷新
// 让编辑器的资源管理器里出现这个新的 .prefab 文件
doneCreate(null, "预制体已创建: db://assets/Player.prefab");
});
});
}

最终在磁盘上生成的文件:

1
/项目目录/assets/Player.prefab

文件内容就是阶段二转换后的 JSON 数组。


3. 创建节点的完整流程

用户说:”在 Canvas 下创建一个叫 StartButton 的按钮”

3.1 主进程预处理

对应源码: src/tools/ToolDispatcher.ts:414

1
2
3
4
5
6
7
8
9
10
11
12
case "create_node":
// 如果创建 sprite 或 button 类型,需要一个默认贴图
// 先在主进程中通过 Editor.assetdb 查到默认贴图的 UUID
if (args.type === "sprite" || args.type === "button") {
const splashUuid = Editor.assetdb.urlToUuid(
"db://internal/image/default_sprite_splash.png/default_sprite_splash"
);
args.defaultSpriteUuid = splashUuid; // 附加到参数中传给 scene-script
}

// 交给 scene-script 执行实际创建
callSceneScript("mcp-bridge", "create-node", args, callback);

为什么要预处理?

1
2
3
4
5
6
7
scene-script 运行在渲染进程中
→ 没有 Editor.assetdb API
→ 无法查询资源 UUID

主进程有 Editor.assetdb
→ 先查好默认贴图的 UUID
→ 通过参数传给 scene-script

3.2 scene-script 执行创建

在 scene-script 中(简化版逻辑):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
"create-node": function(event, args) {
// 1. 创建空节点
const node = new cc.Node(args.name); // new cc.Node("StartButton")

// 2. 挂到父节点上
if (args.parentId) {
const parent = findNode(args.parentId); // 通过 UUID 找到 Canvas
node.parent = parent;
} else {
node.parent = cc.director.getScene(); // 没指定父节点就挂到场景根
}

// 3. 根据类型添加组件和设置属性
switch (args.type) {
case "sprite":
const sprite = node.addComponent(cc.Sprite);
node.width = 100;
node.height = 100;
// 设置默认贴图(使用主进程传来的 UUID)
break;

case "button":
const btnSprite = node.addComponent(cc.Sprite);
node.addComponent(cc.Button);
node.width = 150;
node.height = 50;
// 设置按钮底图
break;

case "label":
const label = node.addComponent(cc.Label);
node.width = 120;
node.height = 40;
label.string = args.name;
break;

case "empty":
default:
// 纯空节点,不添加任何组件
break;
}

// 4. 如果指定了布局(layout),自动添加 Widget 组件进行适配
if (args.layout) {
const widget = node.addComponent(cc.Widget);
// 根据 layout 值(center/top/bottom/full 等)设置对齐方式
}

// 5. 通知编辑器场景已修改
Editor.Ipc.sendToMain("scene:dirty"); // 标记需要保存
Editor.Ipc.sendToAll("scene:node-changed", ...); // 刷新层级面板

// 6. 返回新节点的 UUID
event.reply(null, node.uuid); // 返回如 "f7g8h9i0-xxxx"
}

AI 拿到返回的 UUID 后,就可以继续对这个新节点做后续操作(改大小、加组件等)。


4. 总结

AI 怎么找到节点 ID?

1
2
3
4
5
6
7
8
9
10
不是"猜"的,是"先查再用"的两步走:

第一步: get_scene_hierarchy
→ cc.director.getScene() 遍历场景树
→ 返回每个节点的 name + uuid
→ AI 从返回数据中匹配用户说的节点名

第二步: 拿着 UUID 调用具体工具
→ scene-script 中通过 cc.engine.getInstanceById(uuid) 找到节点对象
→ 直接修改对象属性(node.width = 200)

创建预制体的流程?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
create_prefab 三阶段:

阶段一 (scene-script):
findNode(uuid) 找到节点
→ 临时摘下 (node.parent = null)
→ Editor.serialize(node) 序列化
→ 恢复节点

阶段二 (scene-script):
场景格式 → 预制体格式
→ 删 cc.Scene
→ 加 cc.Prefab 包装器
→ 加 cc.PrefabInfo
→ 重算索引引用

阶段三 (主进程):
→ 写入 .prefab 文件到磁盘
→ 修复 fileId
→ 通知资源库刷新

创建节点的流程?

1
2
3
4
5
6
7
8
9
10
11
12
13
create_node 两阶段:

主进程预处理:
→ 查询默认贴图 UUID(scene-script 里查不了)
→ 附加到参数中传给 scene-script

scene-script 执行:
→ new cc.Node(name)
→ node.parent = findNode(parentId)
→ 根据 type 添加组件 (Sprite/Button/Label)
→ 根据 layout 添加 Widget 适配
→ 通知编辑器刷新
→ 返回新节点 UUID

核心原理

MCP 只是翻译层,真正干活的是 Cocos 引擎自己的 API:

操作 实际调用的 Cocos API
查找节点 cc.engine.getInstanceById(uuid)
获取场景 cc.director.getScene()
创建节点 new cc.Node(name)
挂载节点 node.parent = parentNode
添加组件 node.addComponent(cc.Sprite)
修改属性 node.width = 200
序列化 Editor.serialize(node)
写入文件 fs.writeFileSync(path, data)
资源查询 Editor.assetdb.urlToUuid(url)
通知编辑器 Editor.Ipc.sendToMain("scene:dirty")