Agent+MCP接管FairyGUI UI拼装:从40分钟到5分钟的自动化实践
发布时间:2026/9/25 21:33:55来源:尧图网络
1. 为什么我决定让 Agent 接管 FairyGUI 的 UI 拼装工作做 Unity 项目的人都有一个共识UI 拼装这件事技术含量不高但极其消耗时间。尤其是用 FairyGUI 的项目美术在编辑器里出好组件程序要一个个把组件拖到场景里、绑定脚本、设置适配参数、处理层级关系。一个中等复杂度的界面从拿到设计稿到能在真机上跑通两三个小时是常态。如果遇到设计稿频繁改版那更是噩梦——美术改一个按钮位置程序要重新对一遍坐标和锚点。我所在的项目组用的是 FairyGUI 作为 UI 中间件配合 Unity 做移动端游戏。团队规模不大前后端加美术一共十来个人但 UI 迭代频率很高平均每周有 3 到 5 个界面需要新增或修改。过去半年我一直在琢磨怎么把这块重复劳动压缩掉。试过写编辑器脚本批量生成也试过用模板代码自动绑定但都停留在“半自动”阶段——脚本能帮你生成代码骨架但组件之间的层级关系、特殊布局逻辑、动态加载时机还是得手动调。直到我开始认真研究 Agent 和 MCP 这套东西思路才真正打开。简单说Agent 负责理解意图和做决策MCP 负责提供标准化的工具接口两者结合就能让 AI 真正“动手”操作 FairyGUI 编辑器而不是只给你一段建议代码。这篇文章就是我这段时间折腾下来的完整记录包括整体设计思路、MCP 工具怎么封装、Agent 提示词怎么写、实际跑起来会遇到哪些坑。如果你也在用 FairyGUI 做 Unity 项目并且对 Agent 开发或者 MCP 协议感兴趣这篇内容应该能帮你省下不少试错时间。2. 整体方案设计与核心思路拆解2.1 为什么是 Agent MCP而不是纯脚本或纯 AI 对话先说清楚一个前提FairyGUI 的编辑器本身是带完整 API 的你可以用 C# 写编辑器扩展来操作它。理论上写一套脚本把所有 UI 拼装逻辑自动化是可行的。但问题在于UI 拼装的“规则”很难用固定脚本穷举。比如同样是“把标题放在顶部居中”有的界面标题是独立组件有的是文本控件有的需要跟随安全区适配有的要动态换字体。你写 if-else 写到天亮也覆盖不全。纯 AI 对话的方式我也试过。把 FairyGUI 的 API 文档丢给模型让它生成操作代码。结果就是模型生成的代码经常调错 API 签名或者对组件路径的理解和实际工程对不上。因为它没有“眼睛”看不到当前编辑器的真实状态只能靠猜。Agent MCP 的组合恰好补上了这两个短板。MCP 协议的本质是把 FairyGUI 编辑器的操作能力封装成一组标准工具比如“获取当前包列表”“创建组件”“设置控件属性”“建立父子关系”等等。Agent 则负责根据自然语言指令决定调用哪些工具、按什么顺序调用、传什么参数。Agent 有“手”了能真正操作编辑器MCP 有“规范”了工具调用不会跑偏。提示MCP 是 Model Context Protocol 的缩写你可以把它理解成一套“AI 和外部工具之间的 USB 接口标准”。只要工具按这个标准封装任何支持 MCP 的 Agent 都能直接调用不需要为每个 Agent 单独适配。2.2 方案选型为什么用 Cursor 作为 Agent 宿主Agent 的宿主环境我选了 Cursor。原因有几个第一Cursor 对 MCP 的支持比较成熟配置方式简单在设置里加一个 MCP Server 的地址就行第二Cursor 本身是代码编辑器Agent 在操作 FairyGUI 的同时还能直接读写项目里的 C# 脚本方便做代码和 UI 的联动第三团队里大部分人已经在用 Cursor 做日常开发学习成本低。当然你也可以用其他支持 MCP 的 Agent 框架比如自己基于开源 Agent 框架搭一个。但如果你只是想快速验证效果Cursor 是最省事的入口。它的中文设置也很简单在设置里搜“language”选中文即可不影响 MCP 功能。2.3 整体架构三层结构整个方案我拆成三层交互层Cursor 里的 Agent 对话窗口。你用自然语言描述 UI 需求比如“帮我创建一个登录界面包含账号输入框、密码输入框、登录按钮和忘记密码链接”。决策层Agent 根据你的描述结合当前 FairyGUI 工程的状态规划出一系列工具调用。比如先查包列表再创建组件再逐个添加控件并设置属性。执行层MCP Server 接收工具调用请求转换成 FairyGUI 编辑器的 C# API 调用实际修改工程文件。这三层之间通过 MCP 协议通信Agent 不需要知道 FairyGUI 的具体 API 细节只需要知道“有哪些工具可用、每个工具干什么”。MCP Server 也不需要理解自然语言只需要把标准化的工具调用执行到位。2.4 关键设计决策工具粒度怎么定这是整个方案里最需要斟酌的地方。工具粒度太粗比如只提供一个“创建完整界面”的工具那 Agent 的灵活性就没了等于把脚本逻辑硬编码进 MCP Server。工具粒度太细比如每个属性设置都单独一个工具那 Agent 调用次数会爆炸一个简单界面要调几百次工具效率极低。我的做法是按“操作单元”来划分工具而不是按“API 方法”。具体来说分成四类工具工具类别代表工具说明查询类list_packages、get_component_tree获取工程当前状态Agent 决策的依据创建类create_component、add_control创建新组件或向组件添加控件属性类set_control_props、set_relation批量设置控件属性支持一次传多个键值对布局类set_position、set_size、align_controls处理位置、尺寸、对齐等布局操作这样划分的好处是Agent 一次工具调用可以完成一组相关属性的设置既不会太碎也不会太粗。比如set_control_props可以一次传入{x: 100, y: 200, width: 300, height: 50, text: 登录}减少往返次数。3. MCP Server 的核心实现与实操要点3.1 环境准备FairyGUI 编辑器扩展的基础MCP Server 本身是一个独立进程它需要和 FairyGUI 编辑器通信。最直接的方式是把 MCP Server 做成 FairyGUI 的编辑器扩展这样它可以直接调用 FairyGUI 的 C# API不需要走额外的 IPC 通道。具体做法是在 Unity 项目的Editor目录下创建一个 FairyGUI 编辑器扩展脚本在OnEnable里启动一个本地 HTTP 服务或者 WebSocket 服务监听来自 Cursor 的 MCP 请求。FairyGUI 的编辑器 API 主要在FairyEditor命名空间下常用的类包括FairyEditor.App、FairyEditor.Project、FairyEditor.PackageItem等。注意FairyGUI 编辑器扩展的 API 和运行时 API 是两套东西。运行时用的GComponent、GButton这些在编辑器扩展里不能用编辑器里操作的是PackageItem和GComponent的编辑器版本。这一点我踩过坑一开始照着运行时文档写编译都过不了。3.2 MCP 协议的基本结构MCP 协议的核心是 JSON-RPC 风格的请求响应。一个典型的工具调用请求长这样{ jsonrpc: 2.0, method: tools/call, params: { name: create_component, arguments: { packageName: Login, componentName: LoginPanel, width: 750, height: 1334 } }, id: 1 }MCP Server 收到后解析参数调用 FairyGUI 编辑器 API 创建组件然后返回结果{ jsonrpc: 2.0, result: { content: [ { type: text, text: Component LoginPanel created in package Login, size 750x1334 } ] }, id: 1 }Agent 拿到返回结果后继续规划下一步。整个对话过程就是不断重复“Agent 决策 - 工具调用 - 返回结果 - Agent 再决策”这个循环。3.3 核心工具的实现细节以create_component为例实现步骤大致如下根据packageName找到对应的 FairyGUI 包。如果包不存在返回错误信息让 Agent 知道。在包内创建一个新的组件项设置名称和初始尺寸。调用App.project.Save()保存工程确保后续操作能读到最新状态。返回创建成功的组件路径格式如ui://Login/LoginPanel。再以add_control为例它需要处理多种控件类型public string AddControl(string componentPath, string controlType, string controlName, JObject props) { var item App.project.GetItemByURL(componentPath); var component item as PackageItem; if (component null) return Component not found; GComponent editorComponent component.GetEditorComponent(); GObject newControl null; switch (controlType.ToLower()) { case text: newControl editorComponent.AddChildGTextField(); break; case button: newControl editorComponent.AddChildGButton(); break; case image: newControl editorComponent.AddChildGImage(); break; case list: newControl editorComponent.AddChildGList(); break; default: return $Unsupported control type: {controlType}; } newControl.name controlName; ApplyProps(newControl, props); App.project.Save(); return $Control {controlName} added to {componentPath}; }这里有个关键点每次操作后都要保存工程。FairyGUI 编辑器的某些 API 在未保存状态下行为不一致比如新建的组件如果不保存后续通过 URL 查找可能找不到。这个坑我调了大半天才定位到。3.4 属性设置的批量处理set_control_props是调用最频繁的工具它的实现要兼顾灵活性和效率。我设计成接收一个 JSON 对象里面可以包含任意属性键值对public string SetControlProps(string componentPath, string controlName, JObject props) { var control FindControl(componentPath, controlName); if (control null) return Control not found; foreach (var prop in props) { switch (prop.Key.ToLower()) { case x: control.x prop.Value.Valuefloat(); break; case y: control.y prop.Value.Valuefloat(); break; case width: control.width prop.Value.Valuefloat(); break; case height: control.height prop.Value.Valuefloat(); break; case text: if (control is GTextField tf) tf.text prop.Value.Valuestring(); else if (control is GButton btn) btn.title prop.Value.Valuestring(); break; case visible: control.visible prop.Value.Valuebool(); break; case alpha: control.alpha prop.Value.Valuefloat(); break; case fontsize: if (control is GTextField tf2) tf2.textFormat.size prop.Value.Valueint(); break; case color: if (control is GTextField tf3) tf3.textFormat.color ParseColor(prop.Value.Valuestring()); break; default: // 未知属性记录日志不中断流程 Debug.LogWarning($Unknown prop: {prop.Key}); break; } } App.project.Save(); return $Props set on {controlName}; }实操心得属性名我做了大小写不敏感处理因为 Agent 有时候会传Width有时候传width。另外未知属性不要直接报错中断记录警告继续执行否则 Agent 很容易因为一个小属性名拼错就卡住整个流程。3.5 关系与适配的处理FairyGUI 的适配系统靠的是“关系”Relation。比如一个按钮要始终保持在父容器底部居中就需要设置bottom和center关系。这部分在 MCP 工具里我单独封装了set_relationpublic string SetRelation(string componentPath, string controlName, string relationType, bool enabled) { var control FindControl(componentPath, controlName); if (control null) return Control not found; var relation control.relations.Add(relationType); relation.percent enabled; App.project.Save(); return $Relation {relationType} set to {enabled} on {controlName}; }Agent 在规划布局时会根据界面需求自动决定要不要加关系。比如“登录按钮固定在底部”Agent 就会调用set_relation设置bottom关系。4. Agent 提示词设计与实际运行流程4.1 系统提示词的核心结构Agent 的表现很大程度上取决于提示词怎么写。我的系统提示词分成四个部分角色定义告诉 Agent 它是一个 FairyGUI UI 拼装助手只能通过 MCP 工具操作编辑器不能直接生成代码。工具说明列出所有可用工具及其参数格式。这部分 MCP 协议本身会提供但我会额外强调一些使用顺序上的约束。工作流程规定 Agent 的操作顺序——先查询工程状态再规划操作再逐步执行每步执行后检查结果。输出规范要求 Agent 在每次工具调用后简要说明当前进度方便我跟踪。提示词里最关键的一条约束是Agent 必须先调用list_packages和get_component_tree了解当前工程状态才能开始创建操作。否则 Agent 很容易凭空假设一个不存在的包名导致后续操作全部失败。4.2 一次完整的界面创建流程我拿一个实际例子来说明。需求是“在 Login 包里创建一个注册界面包含标题‘注册账号’、用户名输入框、密码输入框、确认密码输入框、注册按钮和返回登录链接。”Agent 收到需求后实际执行了以下步骤调用list_packages确认 Login 包存在。调用get_component_tree查看 Login 包现有组件避免命名冲突。调用create_component创建RegisterPanel尺寸 750x1334。调用add_control添加标题文本设置text为“注册账号”fontSize为 36位置居中。依次添加三个输入框设置text为占位提示宽度 500高度 80垂直间距 120。添加注册按钮设置title为“注册”宽度 400高度 90。添加返回链接文本设置text为“返回登录”颜色为蓝色位置在按钮下方。调用set_relation给按钮设置bottom关系确保在不同分辨率下位置合理。最后调用get_component_tree验证所有控件都已正确添加。整个过程 Agent 调用了大约 15 次工具耗时不到 2 分钟。如果手动操作这个界面至少需要 40 分钟。4.3 提示词中的关键约束有几个约束是我反复调整后才定下来的禁止 Agent 一次性规划所有步骤再执行。早期我让 Agent 先输出完整计划再执行结果它经常规划到一半就“幻觉”出一些不存在的工具。改成“边执行边规划”后稳定性大幅提升。要求 Agent 在每次创建控件后立即设置属性。不要先创建所有控件再统一设属性因为 FairyGUI 编辑器在控件创建后如果没设名称后续查找会很麻烦。限制单次工具调用的参数复杂度。比如set_control_props一次最多传 8 个属性超过就拆成多次调用。这样即使某个属性出错也不会影响其他属性。提示Agent 的提示词不是写一次就完事的。我前后改了七八版每改一版都拿同一个需求跑一遍对比成功率和耗时。建议你也建立一个简单的测试用例集每次调整提示词后回归测试。4.4 处理 Agent 的“幻觉”问题Agent 偶尔会调用不存在的工具或者传入错误的参数格式。我的处理方式是在 MCP Server 层面做严格的参数校验返回清晰的错误信息。比如 Agent 传了一个不存在的控件类型Server 返回Unsupported control type: slider. Available types: text, button, image, list, input。Agent 收到这个错误后通常能自我纠正换成正确的类型。另一个常见问题是 Agent 把组件路径写错。比如把ui://Login/RegisterPanel写成ui://Login/Register。MCP Server 在查找失败时会返回当前包里所有组件的列表Agent 看到列表后就能修正路径。5. 常见问题排查与避坑经验实录5.1 工具调用超时怎么办FairyGUI 编辑器的某些操作比较耗时比如创建复杂组件或者保存大型工程。如果 MCP 请求超时Agent 会收到错误但实际操作可能已经完成了。这会导致状态不一致。我的解决方案是给每个工具调用加一个操作 IDMCP Server 在执行前先检查这个 ID 是否已经执行过。如果已执行直接返回上次的结果不重复执行。这个幂等性设计在调试阶段特别有用因为 Agent 重试时不会造成重复创建。5.2 组件层级混乱的修复Agent 在添加控件时默认是追加到父容器末尾。如果界面需要特定的层级顺序比如背景图在最底层、按钮在最上层就需要显式设置层级。我封装了一个set_child_index工具Agent 可以在添加控件后调整顺序。实际跑下来Agent 有时候会忘记调整层级导致按钮被背景图遮住。后来我在系统提示词里加了一条“添加控件后如果该控件需要交互按钮、输入框确保它的层级在装饰性控件之上。”这个问题就很少出现了。5.3 中文文本的编码问题FairyGUI 编辑器对中文文本的处理在大部分情况下没问题但通过 MCP 传入中文时如果 JSON 编码没处理好会出现乱码。我一开始用的是默认的 JSON 序列化设置中文被转义成了\uXXXX格式FairyGUI 编辑器能识别但日志里看起来很不直观。后来改成JsonSerializerSettings里设置StringEscapeHandling.EscapeNonAscii false中文就能正常显示了。5.4 常见问题速查表问题现象可能原因解决方法Agent 调用工具后无响应MCP Server 未启动或端口被占用检查 FairyGUI 编辑器控制台是否有启动日志确认端口未被其他程序占用创建组件失败提示包不存在Agent 假设了错误的包名在提示词中强制要求先调用list_packages控件属性设置不生效属性名拼写错误或类型不匹配查看 MCP Server 日志中的警告信息修正属性名中文显示为乱码JSON 序列化转义了非 ASCII 字符调整序列化设置关闭非 ASCII 转义Agent 重复创建同名组件未检查现有组件列表在create_component里加名称冲突检查返回错误让 Agent 处理保存工程时报错工程文件被其他进程占用确保 FairyGUI 编辑器没有同时打开其他工程关闭不必要的编辑器实例5.5 性能优化的几个点Agent 操作 FairyGUI 的效率瓶颈主要在两个方面一是工具调用往返的网络延迟二是 FairyGUI 编辑器本身的 API 响应速度。对于第一点我把 MCP Server 和 Cursor 放在同一台机器上走本地回环延迟可以忽略。对于第二点我做了两件事一是批量操作比如set_control_props一次设多个属性二是延迟保存不是每次操作都调Save()而是每 5 次操作或遇到查询类工具调用前才保存一次。这样整体耗时能降低 30% 左右。注意延迟保存有个风险如果中间某步操作失败未保存的修改会丢失。所以我在 MCP Server 里加了一个事务日志记录每次操作的参数失败时可以回放。这个日志在调试阶段帮了大忙。6. 实际效果与后续扩展方向这套方案在我们团队跑了一个多月目前已经用它完成了 20 多个界面的初始拼装。平均每个界面的拼装时间从原来的 40 分钟压缩到 5 分钟以内而且因为 Agent 操作的一致性界面之间的命名规范和布局风格反而比手动操作更统一了。美术改版时只需要把新的设计描述给 Agent它就能在现有组件上做增量修改不用推倒重来。后续我打算在这几个方向继续扩展一是把蓝湖的设计稿解析接进来让 Agent 直接读设计稿的图层信息来生成 UI省掉人工描述这一步二是增加一个“样式检查”工具Agent 在拼装完成后自动检查字号、间距、颜色是否符合设计规范三是把常用的界面模板沉淀成 MCP 工具比如“创建一个标准弹窗”Agent 直接调用模板工具进一步减少工具调用次数。如果你也在做 FairyGUI 相关的开发并且对 Agent 和 MCP 这套东西感兴趣我建议先从一个小界面开始试把 MCP Server 的基础工具跑通再逐步增加工具种类和提示词的复杂度。不要一上来就想着全自动化先把“半自动”跑顺让 Agent 帮你完成 70% 的重复劳动剩下的 30% 手动微调这样投入产出比最高。
网站建设高端定制企业官网