From bf6ec93b9950d82a5dc2dc433460ea5961c9dc1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=AB=E7=84=B0=E5=BA=93=E6=8B=89?= Date: Sat, 7 Feb 2026 23:14:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20UI=20=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=88=9B=E5=BB=BA=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20UUID=20=E8=A7=A3=E6=9E=90=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=B0=86=E6=89=80=E6=9C=89=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=92=8C=E6=B3=A8=E9=87=8A=E6=9B=B4=E6=96=B0=E4=B8=BA=E7=AE=80?= =?UTF-8?q?=E4=BD=93=E4=B8=AD=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 +++++-- main.js | 89 +++++++++++++++++++++++---- scene-script.js | 155 +++++++++++++++++++++++++++++++++++++----------- 3 files changed, 214 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index af21e33..73a3868 100644 --- a/README.md +++ b/README.md @@ -123,15 +123,27 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j ### 7. create_node -- **描述**: 在当前场景中创建一个新节点。对于 Canvas/Label 类型,会自动添加对应组件。 +- **描述**: 在当前场景中创建一个新节点。 +- **重要提示**: + 1. 如果指定了 `parentId`,必须先通过 `get_scene_hierarchy` 确认该 UUID 对应的父节点仍然存在。 + 2. **预设类型差异**: + - `empty`: 纯空节点,无组件,不带贴图。 + - `sprite`: 自动添加 Sprite 组件,默认尺寸 100x100,并带有引擎默认贴图占位。 + - `button`: 自动添加 Sprite 和 Button 组件,默认尺寸 **150x50**,背景色设为深色以便看清文字,并带有默认贴图。 + - `label`: 自动添加 Label 组件,默认尺寸 120x40。 - **参数**: - `name`: 节点名称 - - `parentId`: 父节点 UUID (可选,不传则挂在场景根部) - - `type`: 节点预设类型(`empty`, `sprite`, `label`, `canvas`) + - `parentId`: 父节点 UUID (可选) + - `type`: 节点预设类型(`empty`, `sprite`, `label`, `button`) ### 8. manage_components -- **描述**: 管理节点组件。**重要最佳实践**:在执行 `add` 操作前,建议先通过 `get` 操作检查节点上是否已存在同类型的组件,以避免重复添加。 +- **描述**: 管理节点组件。 +- **重要最佳实践**: + 1. **引用验证**:操作前必须调用 `get_scene_hierarchy` 确认 `nodeId` 对应的节点真实存在(防止由于场景重置或节点删除导致的引用失效)。 + 2. 在执行 `add` 操作前,建议先通过 `get` 操作检查是否已存在同类组件。 + 3. 添加 `cc.Sprite` 后请务必设置其 `spriteFrame` 属性,否则节点将不显示。 + 4. 创建按钮时,请确保目标节点有正数尺寸(`width`/`height`)作为点击区域。 - **参数**: - `nodeId`: 节点 UUID - `action`: 操作类型(`add`, `remove`, `get`, `update`) diff --git a/main.js b/main.js index b96e1fc..86db7e6 100644 --- a/main.js +++ b/main.js @@ -14,7 +14,11 @@ let serverConfig = { active: false, }; -// 封装日志函数,同时发送给面板和编辑器控制台 +/** + * 封装日志函数,同时发送给面板、保存到内存并在编辑器控制台打印 + * @param {'info' | 'success' | 'warn' | 'error'} type 日志类型 + * @param {string} message 日志内容 + */ function addLog(type, message) { const logEntry = { time: new Date().toLocaleTimeString(), @@ -33,10 +37,18 @@ function addLog(type, message) { } } +/** + * 获取完整的日志内容(文本格式) + * @returns {string} 拼接后的日志字符串 + */ function getLogContent() { return logBuffer.map(entry => `[${entry.time}] [${entry.type}] ${entry.content}`).join('\n'); } +/** + * 生成新场景的 JSON 模板数据 + * @returns {string} 场景数据的 JSON 字符串 + */ const getNewSceneTemplate = () => { // 尝试获取 UUID 生成函数 let newId = ""; @@ -74,11 +86,15 @@ const getNewSceneTemplate = () => { return JSON.stringify(sceneData); }; +/** + * 获取所有支持的 MCP 工具列表定义 + * @returns {Array} 工具定义数组 + */ const getToolsList = () => { return [ { name: "get_selected_node", - description: "获取当前编辑器中选中的节点 ID", + description: "获取当前编辑器中选中的节点 ID。建议获取后立即调用 get_scene_hierarchy 确认该节点是否仍存在于当前场景中。", inputSchema: { type: "object", properties: {} }, }, { @@ -105,7 +121,7 @@ const getToolsList = () => { }, { name: "update_node_transform", - description: "修改节点的坐标、缩放或颜色", + description: "修改节点的坐标、缩放或颜色。执行前必须调用 get_scene_hierarchy 确保 node ID 有效。", inputSchema: { type: "object", properties: { @@ -158,7 +174,7 @@ const getToolsList = () => { }, { name: "create_node", - description: "在当前场景中创建一个新节点", + description: "在当前场景中创建一个新节点。重要提示:1. 如果指定 parentId,必须先调用 get_scene_hierarchy 确保该父节点真实存在。2. 类型说明:'sprite' (100x100 尺寸 + 默认贴图), 'button' (150x50 尺寸 + 深色底图 + Button组件), 'label' (120x40 尺寸 + Label组件), 'empty' (纯空节点)。", inputSchema: { type: "object", properties: { @@ -169,7 +185,7 @@ const getToolsList = () => { }, type: { type: "string", - enum: ["empty", "sprite", "label"], + enum: ["empty", "sprite", "label", "button"], description: "节点预设类型", }, }, @@ -178,7 +194,7 @@ const getToolsList = () => { }, { name: "manage_components", - description: "管理节点组件。重要提示:在执行 'add' 操作前,请务必先通过 'get' 操作检查节点上是否已存在同类型的组件,以避免重复添加导致逻辑异常。", + description: "管理节点组件。重要提示:1. 操作前必须调用 get_scene_hierarchy 确认 nodeId 对应的节点仍然存在。2. 添加前先用 'get' 检查是否已存在。3. 添加 cc.Sprite 后必须设置 spriteFrame 属性,否则节点不显示。4. 创建按钮时,请确保目标节点有足够的 width 和 height 作为点击区域。", inputSchema: { type: "object", properties: { @@ -486,6 +502,9 @@ const getToolsList = () => { module.exports = { "scene-script": "scene-script.js", + /** + * 插件加载时的回调 + */ load() { addLog("info", "MCP Bridge Plugin Loaded"); // 读取配置 @@ -501,15 +520,25 @@ module.exports = { }, 1000); } }, - // 获取配置文件的辅助函数 + /** + * 获取插件配置文件的辅助函数 + * @returns {Object} Editor.Profile 实例 + */ getProfile() { // 'local' 表示存储在项目本地(local/mcp-bridge.json) return Editor.Profile.load("profile://local/mcp-bridge.json", "mcp-bridge"); }, + /** + * 插件卸载时的回调 + */ unload() { this.stopServer(); }, + /** + * 启动 HTTP 服务器 + * @param {number} port 监听端口 + */ startServer(port) { if (mcpServer) this.stopServer(); @@ -711,6 +740,12 @@ module.exports = { } }, + /** + * 处理来自 HTTP 的 MCP 调用请求 + * @param {string} name 工具名称 + * @param {Object} args 工具参数 + * @param {Function} callback 完成回调 (err, result) + */ handleMcpCall(name, args, callback) { if (isSceneBusy && (name === "save_scene" || name === "create_node")) { return callback("编辑器正忙(正在处理场景),请稍候。"); @@ -790,6 +825,10 @@ module.exports = { break; 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; + } Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, callback); break; @@ -905,7 +944,11 @@ module.exports = { } }, - // 管理脚本文件 + /** + * 管理项目中的脚本文件 (TS/JS) + * @param {Object} args 参数 + * @param {Function} callback 完成回调 + */ manageScript(args, callback) { const { action, path: scriptPath, content } = args; @@ -1005,7 +1048,11 @@ export default class NewScript extends cc.Component { } }, - // 批处理执行 + /** + * 批量执行多个 MCP 工具操作 + * @param {Object} args 参数 (operations 数组) + * @param {Function} callback 完成回调 + */ batchExecute(args, callback) { const { operations } = args; const results = []; @@ -1027,7 +1074,11 @@ export default class NewScript extends cc.Component { }); }, - // 管理资源 + /** + * 通用的资源管理函数 (创建、删除、移动等) + * @param {Object} args 参数 + * @param {Function} callback 完成回调 + */ manageAsset(args, callback) { const { action, path, targetPath, content } = args; @@ -1095,7 +1146,11 @@ export default class NewScript extends cc.Component { } }, - // 场景管理 + /** + * 场景相关的资源管理 (创建、克隆场景等) + * @param {Object} args 参数 + * @param {Function} callback 完成回调 + */ sceneManagement(args, callback) { const { action, path, targetPath, name } = args; @@ -1262,7 +1317,11 @@ export default class NewScript extends cc.Component { } }, - // 管理编辑器 + /** + * 管理编辑器状态 (选中对象、刷新等) + * @param {Object} args 参数 + * @param {Function} callback 完成回调 + */ manageEditor(args, callback) { const { action, target, properties } = args; @@ -1415,7 +1474,11 @@ export default class NewScript extends cc.Component { }, - // 应用文本编辑 + /** + * 对文件应用一系列精确的文本编辑操作 + * @param {Object} args 参数 + * @param {Function} callback 完成回调 + */ applyTextEdits(args, callback) { const { filePath, edits } = args; diff --git a/scene-script.js b/scene-script.js index 2ee1f9b..da13c6d 100644 --- a/scene-script.js +++ b/scene-script.js @@ -1,11 +1,38 @@ "use strict"; +/** + * 更加健壮的节点查找函数,支持解压后的 UUID + * @param {string} id 节点的 UUID (支持 22 位压缩格式) + * @returns {cc.Node | null} 找到的节点对象或 null + */ +const findNode = (id) => { + if (!id) return null; + let node = cc.engine.getInstanceById(id); + if (!node && typeof Editor !== 'undefined' && Editor.Utils && Editor.Utils.UuidUtils) { + // 如果直接查不到,尝试对可能是压缩格式的 ID 进行解压后再次查找 + try { + const decompressed = Editor.Utils.UuidUtils.decompressUuid(id); + if (decompressed !== id) { + node = cc.engine.getInstanceById(decompressed); + } + } catch (e) { + // 忽略转换错误 + } + } + return node; +}; + module.exports = { + /** + * 修改节点的基础属性 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (id, path, value) + */ "set-property": function (event, args) { const { id, path, value } = args; // 1. 获取节点 - let node = cc.engine.getInstanceById(id); + let node = findNode(id); if (node) { // 2. 修改属性 @@ -34,6 +61,10 @@ module.exports = { } } }, + /** + * 获取当前场景的完整层级树 + * @param {Object} event IPC 事件对象 + */ "get-hierarchy": function (event) { const scene = cc.director.getScene(); @@ -66,11 +97,16 @@ module.exports = { if (event.reply) event.reply(null, hierarchy); }, + /** + * 批量更新节点的变换信息 (坐标、缩放、颜色) + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (id, x, y, scaleX, scaleY, color) + */ "update-node-transform": function (event, args) { const { id, x, y, scaleX, scaleY, color } = args; Editor.log(`[scene-script] update-node-transform called for ${id} with args: ${JSON.stringify(args)}`); - let node = cc.engine.getInstanceById(id); + let node = findNode(id); if (node) { Editor.log(`[scene-script] Node found: ${node.name}, Current Pos: (${node.x}, ${node.y})`); @@ -107,6 +143,11 @@ module.exports = { if (event.reply) event.reply(new Error("找不到节点")); } }, + /** + * 在场景中创建新节点 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (name, parentId, type) + */ "create-node": function (event, args) { const { name, parentId, type } = args; const scene = cc.director.getScene(); @@ -130,18 +171,57 @@ module.exports = { camNode.addComponent(cc.Camera); camNode.parent = newNode; } else if (type === "sprite") { - newNode = new cc.Node(name || "New Sprite"); - newNode.addComponent(cc.Sprite); + newNode = new cc.Node(name || "新建精灵节点"); + let sprite = newNode.addComponent(cc.Sprite); + // 设置为 CUSTOM 模式 + sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; + // 为精灵设置默认尺寸 + newNode.width = 100; + newNode.height = 100; + + // 加载引擎默认图做占位 + if (args.defaultSpriteUuid) { + cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { + if (!err && (asset instanceof cc.SpriteFrame || asset instanceof cc.Texture2D)) { + sprite.spriteFrame = asset instanceof cc.SpriteFrame ? asset : new cc.SpriteFrame(asset); + Editor.Ipc.sendToMain("scene:dirty"); + } + }); + } + } else if (type === "button") { + newNode = new cc.Node(name || "新建按钮节点"); + let sprite = newNode.addComponent(cc.Sprite); + newNode.addComponent(cc.Button); + + // 设置为 CUSTOM 模式并应用按钮专用尺寸 + sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; + newNode.width = 150; + newNode.height = 50; + + // 设置稍暗的背景颜色 (#A0A0A0),以便于看清其上的文字 + newNode.color = new cc.Color(160, 160, 160); + + // 加载引擎默认图 + if (args.defaultSpriteUuid) { + cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { + if (!err && (asset instanceof cc.SpriteFrame || asset instanceof cc.Texture2D)) { + sprite.spriteFrame = asset instanceof cc.SpriteFrame ? asset : new cc.SpriteFrame(asset); + Editor.Ipc.sendToMain("scene:dirty"); + } + }); + } } else if (type === "label") { - newNode = new cc.Node(name || "New Label"); + newNode = new cc.Node(name || "新建文本节点"); let l = newNode.addComponent(cc.Label); - l.string = "New Label"; + l.string = "新文本"; + newNode.width = 120; + newNode.height = 40; } else { newNode = new cc.Node(name || "New Node"); } // 设置层级 - let parent = parentId ? cc.engine.getInstanceById(parentId) : scene; + let parent = parentId ? findNode(parentId) : scene; if (parent) { newNode.parent = parent; @@ -157,12 +237,19 @@ module.exports = { }, 10); if (event.reply) event.reply(null, newNode.uuid); + } else { + if (event.reply) event.reply(new Error(`无法创建节点:找不到父节点 ${parentId}`)); } }, + /** + * 管理节点上的组件 (添加、移除、更新属性) + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (nodeId, action, componentType, componentId, properties) + */ "manage-components": function (event, args) { const { nodeId, action, componentType, componentId, properties } = args; - let node = cc.engine.getInstanceById(nodeId); + let node = findNode(nodeId); // 辅助函数:应用属性并智能解析 const applyProperties = (component, props) => { @@ -182,16 +269,8 @@ module.exports = { // 解析 Target Node if (item.target) { - let targetNode = null; - if (typeof item.target === 'string') { - targetNode = cc.engine.getInstanceById(item.target); - if (!targetNode && Editor.Utils && Editor.Utils.UuidUtils) { - try { - const decompressed = Editor.Utils.UuidUtils.decompressUuid(item.target); - targetNode = cc.engine.getInstanceById(decompressed); - } catch (e) { } - } - } else if (item.target instanceof cc.Node) { + let targetNode = findNode(item.target); + if (!targetNode && item.target instanceof cc.Node) { targetNode = item.target; } @@ -225,17 +304,7 @@ module.exports = { // 检查传入值是否是字符串 (可能是 UUID) 或 Node 对象 let targetNode = null; if (typeof value === 'string') { - targetNode = cc.engine.getInstanceById(value); - - // 针对压缩 UUID 的回退处理 - if (!targetNode && Editor.Utils && Editor.Utils.UuidUtils) { - try { - const decompressed = Editor.Utils.UuidUtils.decompressUuid(value); - if (decompressed !== value) { - targetNode = cc.engine.getInstanceById(decompressed); - } - } catch (e) { } - } + targetNode = findNode(value); if (targetNode) { Editor.log(`[scene-script] Resolved node: ${value} -> ${targetNode.name}`); @@ -565,6 +634,11 @@ module.exports = { }); }, + /** + * 根据特定条件在场景中搜索节点 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (conditions, recursive) + */ "find-gameobjects": function (event, args) { const { conditions, recursive = true } = args; const result = []; @@ -634,9 +708,14 @@ module.exports = { } }, + /** + * 删除指定的场景节点 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (uuid) + */ "delete-node": function (event, args) { const { uuid } = args; - const node = cc.engine.getInstanceById(uuid); + const node = findNode(uuid); if (node) { const parent = node.parent; node.destroy(); @@ -656,6 +735,11 @@ module.exports = { } }, + /** + * 管理高效的全场景特效 (粒子系统) + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (action, nodeId, properties, name, parentId) + */ "manage-vfx": function (event, args) { const { action, nodeId, properties, name, parentId } = args; const scene = cc.director.getScene(); @@ -743,7 +827,7 @@ module.exports = { } } else if (action === "update") { - let node = cc.engine.getInstanceById(nodeId); + let node = findNode(nodeId); if (node) { let particleSystem = node.getComponent(cc.ParticleSystem); if (!particleSystem) { @@ -761,7 +845,7 @@ module.exports = { } } else if (action === "get_info") { - let node = cc.engine.getInstanceById(nodeId); + let node = findNode(nodeId); if (node) { let ps = node.getComponent(cc.ParticleSystem); if (ps) { @@ -791,9 +875,14 @@ module.exports = { } }, + /** + * 控制节点的动画组件 (播放、暂停、停止等) + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (action, nodeId, clipName) + */ "manage-animation": function (event, args) { const { action, nodeId, clipName } = args; - const node = cc.engine.getInstanceById(nodeId); + const node = findNode(nodeId); if (!node) { if (event.reply) event.reply(new Error(`Node not found: ${nodeId}`));