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接口:
usingUnityEngine;usingUnityEditor;usingFunplay.Editor.Tools.Scripting;publicclassCommandScript:IFunplayCommand{publicvoidExecute(ExecutionContextctx){vargo=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的实现如下:
usingSystem.Linq;usingUnityEngine;usingUnityEditor;usingFunplay.Editor.Tools.Scripting;publicclassCommandScript:IFunplayCommand{publicvoidExecute(ExecutionContextctx){varenemies=Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None).Where(g=>g.name.StartsWith("Enemy_")).ToList();foreach(vareinenemies){varcol=e.GetComponent<BoxCollider>();if(col!=null){ctx.RegisterObjectModification(col);col.isTrigger=true;}varhit=newGameObject("HitBox");hit.transform.SetParent(e.transform,false);ctx.RegisterObjectCreation(hit);}varpath="Assets/Generated/Enemies.prefab";System.IO.Directory.CreateDirectory("Assets/Generated");varroot=newGameObject("Enemies");ctx.RegisterObjectCreation(root);foreach(vareinenemies)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 或讨论。