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, uuid: node.uuid, 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;
const node = findNode(nodeId);
const originalParent = node.parent; const originalName = node.name;
node.parent = null; if (nodeName) node.name = nodeName;
const serializedStr = Editor.serialize(node);
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._createPrefabViaSceneScript(src/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) { callSceneScript("mcp-bridge", "create-prefab", { nodeId, nodeName }, (err, serializedData) => {
AssetPatcher.safeCreateAsset(prefabUrl, serializedData, callback, (doneCreate) => {
const prefabFspath = Editor.assetdb.urlToFspath(prefabUrl); ToolDispatcher.fixPrefabRootFileId(prefabFspath);
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": 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; }
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) { const node = new cc.Node(args.name);
if (args.parentId) { const parent = findNode(args.parentId); node.parent = parent; } else { node.parent = cc.director.getScene(); }
switch (args.type) { case "sprite": const sprite = node.addComponent(cc.Sprite); node.width = 100; node.height = 100; 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; }
if (args.layout) { const widget = node.addComponent(cc.Widget); }
Editor.Ipc.sendToMain("scene:dirty"); Editor.Ipc.sendToAll("scene:node-changed", ...);
event.reply(null, node.uuid); }
|
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") |