91 个 MCP 工具中最关键的一个:Funplay Unity MCP 的 execute_code 设计与实现
Funplay Unity MCP 是一个运行在 Unity Editor 内部的 HTTP MCP server,外部 AI 客户端(Claude Code、Cursor、Codex、VS Code 等)通过它驱动编辑器与 PlayMode。截至 v0.3.0,仓库一共注册了 91 个工具,按 [ToolProvider] 标注分布在 20 个模块下。
在所有这些工具中,使用频率最高、对客户端工作流影响最深的是 execute_code——一个允许 AI 客户端提交 C# 代码片段、由 Editor 在内存中编译并立即执行的工具。本文记录它的设计动因、实现要点以及在工具集合中扮演的角色。
1. 系统位置
整体的请求链路如下:
execute_code 是 Tools/Builtins/ScriptExecutionFunctions.cs 注册的众多工具方法之一,但它的输入不是预定义参数,而是任意 C# 源代码。
2. 为什么需要一个"通用工具"
工具系统最自然的演进路径是"专用工具优先"——为每个高频操作单独定义一个 RPC:create_primitive、set_transform、add_component、set_material_shader。这条路线的优点是参数明确、单元测试容易、文档化彻底。
但一旦面对组合性需求,专用工具的代价会迅速放大。考虑这样一个任务:
找到场景中所有名字以
Enemy_开头的 GameObject,将它们的BoxCollider改为 trigger,给每个对象添加一个名为HitBox的子物体,最后把整批对象保存为一个 prefab。
这一句需求拆解为专用工具调用至少需要 8 次 round-trip,且每次调用都要在 AI 客户端与 Editor 之间传递 instanceId 等中间状态。任意一步失败都需要回滚或重新拼装。
更进一步,有些操作根本无法预先定义工具——例如调用项目内自定义 ScriptableObject 的某个静态方法,或访问第三方插件暴露的 Editor API。任何专用工具集合都不可能覆盖这些场景。
execute_code 的设计目标,就是为这一类"在工具表中找不到对应项"的需求提供统一出口。
3. 执行模型
execute_code 不是把代码写到 .cs 文件再触发 AssetDatabase.Refresh(),而是采用 CodeDom 在内存中编译后通过反射调用:
此模型相对文件落盘方案有三点关键差异:
- 不触发 domain reload:编译产物是临时 in-memory assembly,不会引发脚本域重启,编辑器不会出现 1–3 秒的卡顿。
- 失败隔离:代码错误仅影响当次调用,不会让整个项目陷入编译失败状态。其他工具继续可用。
- 执行前主动同步:
EditorReadyHelper.RefreshAndWaitForReady()在编译前自动 refresh asset 并等待 Unity 完成增量编译,调用方无需额外request_recompile。
4. IFunplayCommand:统一的执行入口
为了让 AI 生成的代码能稳定参与 Undo 堆栈与结构化返回,定义了 IFunplayCommand 接口:
using UnityEngine;
using UnityEditor;
using Funplay.Editor.Tools.Scripting;
public class CommandScript : IFunplayCommand
{
public void Execute(ExecutionContext ctx)
{
var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
ctx.RegisterObjectCreation(go);
ctx.Log("Created {0} (id={1})", go.name, go.GetInstanceID());
ctx.ReturnValue = new { instanceId = go.GetInstanceID() };
}
}
ExecutionContext 提供三类基础能力:
| API | 作用 |
|---|---|
RegisterObjectCreation(go) |
等价 Undo.RegisterCreatedObjectUndo,确保用户可 Ctrl+Z 撤销 |
RegisterObjectModification(obj) |
等价 Undo.RecordObject,在修改前调用 |
Log(format, args) / LogWarning / LogError |
写入工具响应中的 messages 字段,不污染 Unity Console |
为了向后兼容旧脚本,框架保留了对 public static string Run() 入口的支持——若编译产物中未找到 IFunplayCommand 实现,则反射调用 Run 方法。新代码建议一律使用 IFunplayCommand。
5. 实现细节:可见性与反射
CodeDom 编译生成的临时 Assembly 与 Funplay 包本身不在同一个程序集。Unity 包的常规做法是将内部类型标记为 internal,避免外部用户依赖私有 API。这意味着如果将所有 helper 都设为 internal,AI 提交的代码将无法引用 ObjectsHelper、TypeResolver 等查询/序列化工具。
实际表现为运行期编译错误:
'Funplay.Editor.Tools.Helpers.ObjectsHelper' is inaccessible due to its protection level
解决方案是建立明确的暴露边界:
| 类型 | 可见性 | 理由 |
|---|---|---|
IFunplayCommand / ExecutionContext |
public |
脚本入口契约 |
ObjectsHelper / TypeResolver |
public |
高频查询能力 |
ComponentSerializer / GameObjectSerializer |
public |
结构化读写 |
Response / EditorReadyHelper |
internal |
框架内部,脚本无需直接调用 |
这样既保证了 AI 脚本的可表达能力,又控制了向外暴露的 API 表面。
6. 何时优先使用专用工具
execute_code 并非取代专用工具,而是作为其互补存在。下列场景仍应优先使用专用工具:
- 状态读取:
get_selection、get_prefab_stage、get_tags、get_layers、get_build_settings直接返回结构化 JSON,比脚本中手动序列化更稳。 - 菜单项执行:
execute_menu_item接受单字符串参数即可触发任意菜单项,比包裹IFunplayCommand调用EditorApplication.ExecuteMenuItem更紧凑。 - 组件字段写入:
set_component_property/set_component_properties基于SerializedPropertyAPI,能正确处理[SerializeField] private、Object 引用赋值、prefab 上下文,远比脚本中手写反射稳定。
判断标准为:当前任务能否由单次工具调用完成? 若可以,使用专用工具;若需要在调用之间维护状态或拼接逻辑,使用 execute_code。
7. 完整示例:单次调用闭环
回到第 2 节中的需求,使用 execute_code 的实现如下:
using System.Linq;
using UnityEngine;
using UnityEditor;
using Funplay.Editor.Tools.Scripting;
public class CommandScript : IFunplayCommand
{
public void Execute(ExecutionContext ctx)
{
var enemies = Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None)
.Where(g => g.name.StartsWith("Enemy_"))
.ToList();
foreach (var e in enemies)
{
var col = e.GetComponent<BoxCollider>();
if (col != null)
{
ctx.RegisterObjectModification(col);
col.isTrigger = true;
}
var hit = new GameObject("HitBox");
hit.transform.SetParent(e.transform, false);
ctx.RegisterObjectCreation(hit);
}
var path = "Assets/Generated/Enemies.prefab";
System.IO.Directory.CreateDirectory("Assets/Generated");
var root = new GameObject("Enemies");
ctx.RegisterObjectCreation(root);
foreach (var e in enemies) e.transform.SetParent(root.transform, true);
PrefabUtility.SaveAsPrefabAsset(root, path);
ctx.Log("Processed {0} enemies → {1}", enemies.Count, path);
ctx.ReturnValue = new { count = enemies.Count, prefab = path };
}
}
整个任务在一次 RPC 中完成:一个 Undo group、一次结构化返回({ count, prefab })、一次 ctx.Log 记录。客户端无需在多次工具调用之间传递 instanceId,也不会因中间任一步失败而留下半成品。
8. 在工具集中的定位
v0.3.0 的 core 默认 profile 共暴露 29 个工具,其中 20 个为只读 / 状态读取(get_*、list_*),9 个为关键写操作。execute_code 是这 9 个写操作中唯一支持任意逻辑的入口。其余写工具(set_component_property、add_component、execute_menu_item 等)覆盖单步高频操作,组合性需求由 execute_code 承接。
这种结构在工具数量与表达能力之间取得了平衡:客户端默认可见的工具数量保持精简(29 个),但实际可表达的操作空间仍然覆盖整个 Unity Editor API。
9. 总结
为 AI 客户端设计工具与为人类设计 SDK 是两种不同的范式。人类开发者偏好职责清晰、参数明确的 API,而 AI 客户端在面对未预期的组合需求时,需要一个可以现场拼接逻辑的"逃生出口"。
execute_code 正是为此而设计:
- 内存编译,不写文件,不触发 domain reload
IFunplayCommand统一入口,自动接入 Undo 与结构化返回- 与专用工具互补,由调用方依据任务粒度自行选择
仓库地址:FunplayAI/funplay-unity-mcp,MIT 协议。如果你正在为 Unity 项目接入 AI 工作流,欢迎提交 issue 或讨论。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)