把一个游戏引擎编辑器接入 AI 客户端,第一个要做的架构决定是:MCP server 跑在哪里。
有两条路。一条是"外挂桥接"------引擎侧装一个插件,开一个 socket 或者读写命令文件,外面再单独跑一个进程(很多方案是 Python 守护进程),由它对 AI 客户端说 MCP 协议、对引擎说私有协议。另一条是"嵌入式"------MCP server 本身就跑在引擎编辑器进程里,编辑器插件本身就是那个 HTTP MCP server。
Funplay 的三个引擎 MCP server------Unity、Cocos、Godot------全部选了后者。这篇讲这个选择背后的取舍。
两种架构
嵌入式
MCP over HTTP
AI 客户端
引擎编辑器插件
= MCP server 本体
引擎编辑器
外挂桥接
MCP
私有协议 / socket
AI 客户端
独立桥接进程
常见是 Python 守护进程
引擎侧插件
引擎编辑器
差别看着只是"中间多一个进程",但它牵动安装方式、进程数、故障面、状态访问四件事。
安装与部署成本
外挂桥接的安装链条是:装引擎插件 → 装运行时(Python 等)→ 装依赖 → 配置并拉起守护进程。四步里任何一步版本不对,整个链路就不通。而且桥接进程和引擎插件是两个独立发布物,二者协议版本不一致时会出现"插件升级了、守护进程没升级"的错配。
嵌入式只有一步:装引擎插件。Funplay 三个仓库的 README 里都有同一句话的不同说法------Cocos 版"不需要单独的 Python 守护进程或外部桥接进程",Godot 版"Godot 插件本身不需要外部 Python 守护进程"。对没有 Python 工具链的游戏开发者来说,这一步差别不小:嵌入式不要求他们的机器上有任何额外运行时。
| 维度 | 外挂桥接 | 嵌入式 |
|---|---|---|
| 安装步骤 | 插件 + 运行时 + 依赖 + 守护进程 | 仅插件 |
| 发布物数量 | 2(插件 + 桥接进程) | 1 |
| 版本错配风险 | 插件 / 桥接进程可能不同步 | 不存在 |
| 额外运行时依赖 | 通常需要 Python | 无 |
进程生命周期与故障面
外挂桥接是三进程模型:编辑器、桥接进程、AI 客户端。三个进程各有各的生命周期------桥接进程可能被孤儿化、可能崩溃、可能和编辑器插件失去同步。故障面里多了"守护进程挂了""socket 断了""桥接进程和插件协议对不上"这几类。
嵌入式是两进程模型:编辑器、AI 客户端。MCP server 的生命周期就是编辑器的生命周期------编辑器在,server 在;编辑器关,server 关。没有"中间进程独立死掉"这个故障类别。
两进程
编辑器 = server
AI 客户端
三进程
可能失同步
可能孤儿/崩溃
编辑器
桥接进程
AI 客户端
嵌入式要付的代价:编辑器生命周期事件
嵌入式不是没有代价。它的代价很具体:MCP server 必须活在编辑器自己的生命周期事件里。
最典型的是 Unity 的 domain reload。Unity 编辑器一旦重新编译脚本,会卸载整个 AppDomain------嵌在里面的 HTTP server 会被连根带走。一个独立的桥接进程没有这个问题,它是稳定的旁路进程;但嵌入式 server 必须自己处理:重载前同步停掉监听、释放端口,重载后把服务重新拉起来。
这件事没有想象中简单。Funplay Unity MCP 在 0.3.1 和 0.3.2 两个版本里专门为它做了加固:
- 重载前必须同步停掉 HttpListener------fire-and-forget 的异步停止来不及在 AppDomain 卸载前完成,端口会残留
- 重载后重启要能扛住
[InitializeOnLoad]与afterAssemblyReload的时序竞争、编辑器仍在编译、服务容器尚未就绪等情况 - 端口被上一个尚未完全释放的 listener 占用时,启动要退避重试
这就是嵌入式的取舍本质:它把"额外进程管理"换成了"必须处理宿主的生命周期事件"。外挂桥接把这个问题甩给了"反正我是独立进程",代价是前面说的部署成本和故障面。Funplay 选了嵌入式,也就选了去把 domain reload 这一类问题在插件内部一次性解决掉。
状态访问:有没有一层 marshalling
还有一个容易被忽略、但影响很深的差别:工具实现离引擎 API 有多远。
外挂桥接进程跑在引擎之外,它不能直接调编辑器 API。每一个"读场景层级""改组件字段"的操作,都要被序列化、穿过 socket、在引擎侧反序列化再执行。中间这层 marshalling 是必经之路。
嵌入式 server 跑在引擎进程内,工具实现就是对编辑器 API 的直接方法调用,没有跨进程序列化层。这一点让 Funplay 系列的主执行工具成为可能------Unity 的 execute_code、Cocos 的 execute_javascript、Godot 的 execute_code:AI 客户端提交一段代码,在引擎进程内编译执行,直接拿到完整的引擎 API 访问权。这种工具在外挂桥接架构里很难做干净,因为"任意代码"要么在桥接进程侧(够不到引擎 API),要么还是得绕回引擎侧执行。
需要补充一句:嵌入式不是没有边界问题。引擎编辑器 API 大多是主线程专用的,嵌入式 server 收到请求后仍要把它 marshal 到编辑器主线程上执行。但这是"进程内换线程",比"跨进程换序列化"轻得多。
那 stdio 包装器算不算"外挂进程"
这里有个容易混淆的点。Funplay 三个仓库其实也 提供 stdio 包装器------Unity / Cocos 走 NuGet / npm,Godot 在 stdio-wrapper/ 下。那它不就是"外挂进程"吗?
不是。区别在于这个包装器有没有状态、做不做翻译。
外挂桥接架构里的守护进程是有状态的:它管理连接、翻译协议、可能缓存引擎状态------它是架构的必需环节。Funplay 的 stdio 包装器是一个无状态的哑桥:它只把 stdio 上的 MCP 流量原样转发到嵌入式 server 的 HTTP 端点,不持有任何状态、不做任何协议翻译、不需要任何引擎知识。
stdio
HTTP 原样转发
AI 客户端
只支持 stdio
stdio 包装器
无状态哑桥
嵌入式 MCP server
它存在的唯一理由是:某些 MCP 客户端或注册表只接受 stdio 命令、不能直连 HTTP。能直连 HTTP 的客户端完全不需要它。它是可选的兼容垫片,不是架构的承重墙------这是它和"外挂桥接守护进程"的根本区别。
取舍总结
| 维度 | 外挂桥接进程 | 嵌入式 MCP server |
|---|---|---|
| 安装 | 多步,需额外运行时 | 单插件 |
| 进程数 | 3 | 2 |
| 故障面 | 多"中间进程"相关故障 | 收敛,但需处理宿主生命周期 |
| 主要代价 | 部署成本 + 版本错配 | domain reload 等编辑器事件 |
| 状态访问 | 跨进程 marshalling | 进程内直接调用 |
| 任意代码执行工具 | 难做干净 | 天然支持(execute_code / _javascript) |
写在最后
没有绝对正确的架构。外挂桥接在"桥接进程要对接多个异构引擎""引擎侧无法跑完整 server"这类场景下仍然合理。
但对 Funplay 想做的事------一个装上就能用、不要求 Python、能让 AI 在引擎进程内执行任意代码的 MCP server------嵌入式是更自然的选择。它把成本集中在一处可控的地方:把编辑器生命周期事件(domain reload 是最硬的那个)在插件内部一次性解决掉,换来单插件安装、更小的故障面、零 marshalling 的工具实现。
三个引擎的实现都开源在 github.com/FunplayAI(funplay-unity-mcp / funplay-cocos-mcp / funplay-godot-mcp),MIT 协议。嵌入式 server 怎么扛 domain reload、stdio 哑桥怎么写,都可以直接参考。