AI 怎么验证 Unity PlayMode 行为:截图 + 输入模拟的完整闭环

Unity 项目的自动化测试通常停留在 EditMode 单元测试层------验证函数逻辑、组件初始化、数值计算。但游戏的"真实行为"------按钮点击是否触发动画、角色是否在地板上、UI 在不同分辨率是否错位------只有进入 PlayMode 才能看到。

把 Unity Editor 暴露给 AI 客户端的设计如果停留在"修改场景资产",就漏掉了游戏开发中最关键的环节。Funplay Unity MCP 把 PlayMode 也作为一等公民暴露------AI 客户端能进入游戏运行态、模拟键鼠输入、回传 Game View 截图、读取 Console 日志、最后退出回到编辑态。本文记录这一套"视觉闭环"的设计与实现。

1. 为什么 PlayMode 验证是 AI 的刚需

AI 客户端要完成"帮我做一个能跑的原型"这类高阶任务时,必然涉及一个验证循环:


AI 修改场景
进入 PlayMode
模拟用户操作
截图 + 读日志
结果符合预期?
退出 + 调整代码
完成

没有这个闭环,AI 只能"盲改"------改完代码不知道效果,需要人工切到 Unity 看一眼然后报告。有了闭环,AI 自己就能形成"试 → 看 → 调"的迭代节奏。

2. PlayMode 工具链概览

Funplay Unity MCP 把这条闭环拆为 5 个原子工具:

工具 作用 关键 API
enter_play_mode 进入运行态 EditorApplication.EnterPlaymode()
simulate_mouse_click 模拟点击 自定义 InputEvent dispatch
simulate_key_press / simulate_key_combo 模拟键盘 同上
capture_game_view / capture_scene_view 截图 Texture2D.ReadPixels + base64
get_console_logs 读取 Unity Console LogEntries reflection + 过滤
exit_play_mode 退出运行态 EditorApplication.ExitPlaymode()

每个工具都是无状态的------客户端按需组合即可。下文按完整闭环顺序讨论几个关键实现点。

3. 进入 PlayMode 的等待问题

EditorApplication.EnterPlaymode() 是异步的------调用后 Unity 不会立刻进入运行态,需要经历"序列化 EditMode 状态 → 域重载 → 调用所有 [InitializeOnEnterPlayMode] → 进入 PlayMode"的过程。期间任何 MCP 调用都可能因 domain reload 被打断。

enter_play_mode 工具的实现要点:

csharp 复制代码
[ToolProvider("PlayMode")]
internal static class PlayModeFunctions
{
    [SceneEditingTool]
    [Description("Enter PlayMode and wait until the player is fully running")]
    public static async Task<string> EnterPlayMode(int timeoutSeconds = 10)
    {
        if (EditorApplication.isPlaying)
            return Response.Success("Already in PlayMode");

        EditorApplication.EnterPlaymode();

        // 等真正进入 isPlaying = true 且 isPlayingOrWillChangePlaymode = false
        var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds);
        while (DateTime.UtcNow < deadline)
        {
            if (EditorApplication.isPlaying && !EditorApplication.isPlayingOrWillChangePlaymode)
                return Response.Success("Entered PlayMode");
            await Task.Delay(100);
        }
        return Response.Error("PLAYMODE_TIMEOUT", "Failed to enter PlayMode within timeout");
    }
}

返回结构化结果让客户端能基于 success 字段决定后续动作。如果遇到 timeout,AI 通常会选择 get_compilation_errors 检查是不是脚本编译失败阻塞了 PlayMode 进入。

4. 输入模拟的实现要点

Unity 的 Input.GetKey / Input.GetMouseButton 不能从 Editor 脚本直接 fake------这些 API 读取的是底层 OS 输入队列。要在 Editor 自动化里模拟输入,常见两条路:
输入模拟
路径 1: Editor 端 dispatch Event
路径 2: 新 Input System EventSource
只对 EditorGUI / UGUI 有效
不触发 Input.GetKey
需项目用 New Input System
全栈生效

Funplay Unity MCP 走的是混合方案------优先使用 Input System 的 InputState.Change API(项目启用 New Input System 时),降级到 EditorWindow Event dispatch(旧 Input Manager 时只对 UGUI 生效)。

简化版的鼠标点击实现:

csharp 复制代码
[SceneEditingTool]
[Description("Simulate a mouse click at the given Game View coordinates")]
public static string SimulateMouseClick(float x, float y, int button = 0)
{
    if (!EditorApplication.isPlaying)
        return Response.Error("NOT_IN_PLAYMODE", "Must be in PlayMode to simulate input");

    var gameView = GetGameView();
    if (gameView == null)
        return Response.Error("NO_GAME_VIEW", "Game View window is not open");

    gameView.Focus();
    var evtDown = new Event { type = EventType.MouseDown, mousePosition = new Vector2(x, y), button = button };
    var evtUp = new Event { type = EventType.MouseUp, mousePosition = new Vector2(x, y), button = button };
    gameView.SendEvent(evtDown);
    gameView.SendEvent(evtUp);

    return Response.Success($"Clicked at ({x}, {y})");
}

实际生产实现里还包含坐标系转换(Game View 与底层 Display 的 DPI 缩放)、多窗口选择(多个 Game View Tab 时选哪个)、以及 Input System 路径的并行实现。

5. 截图:base64 内联 vs 文件路径

截图工具有两个返回选项:

  • base64 内联:直接把 PNG 字节 base64 编码塞进工具响应,AI 客户端可作为 image content 解析
  • 文件路径 :截图存 Assets/Screenshots/,工具返回 path,客户端按需读

Funplay Unity MCP 默认走 base64 内联------MCP 协议原生支持 image content type,客户端直接渲染:

csharp 复制代码
[ReadOnlyTool]
[Description("Capture the Game View as a PNG image returned inline")]
public static string CaptureGameView()
{
    var gameView = GetGameView();
    if (gameView == null)
        return Response.Error("NO_GAME_VIEW", "Game View window is not open");

    var tex = ScreenCapture.CaptureScreenshotAsTexture();
    var png = tex.EncodeToPNG();
    var base64 = Convert.ToBase64String(png);
    UnityEngine.Object.DestroyImmediate(tex);

    return Response.Success("Captured", new
    {
        format = "png",
        width = tex.width,
        height = tex.height,
        encoding = "base64",
        data = base64
    });
}

base64 编码会让响应膨胀 ~33%------一张 1920×1080 截图原始 ~500KB,base64 后 ~670KB。MCP 客户端能接受这个大小,但如果项目需要 4K 截图或连续多帧,应换成文件路径方案。Funplay 提供了两个版本以供选择。

6. 用 Console 日志做断言

视觉断言("按钮变红了")需要 AI 自己看截图判断。但很多游戏逻辑用 Debug.Log 输出状态------这部分可以直接读 Console 日志做断言,不需要视觉判断。

csharp 复制代码
[ReadOnlyTool]
[Description("Get recent Unity Console log entries with optional filter")]
public static string GetConsoleLogs(string filter = null, int limit = 100, string level = null)
{
    // LogEntries 是 internal API,通过反射访问
    var logEntries = typeof(EditorWindow).Assembly.GetType("UnityEditor.LogEntries");
    var startMethod = logEntries.GetMethod("StartGettingEntries");
    var getEntryMethod = logEntries.GetMethod("GetEntryInternal");
    var endMethod = logEntries.GetMethod("EndGettingEntries");

    int totalCount = (int)startMethod.Invoke(null, null);
    var entries = new List<object>();
    /* ... 按 filter / level 过滤 ... */
    endMethod.Invoke(null, null);

    return Response.Success($"Got {entries.Count} logs", new { entries });
}

AI 客户端在执行验证时通常组合使用:截图判断视觉、日志判断状态。例如"按下空格 → 角色应该跳跃"这种断言,截图看角色 Y 坐标变化、日志看 Player jumped at time X 这种业务输出,二者交叉验证。

7. 完整闭环示例

把上述工具串成一个验证流程------让 AI 验证"游戏开始时点击 Start 按钮后角色出现在场景里":
Unity Editor Funplay MCP AI 客户端 Unity Editor Funplay MCP AI 客户端 enter_play_mode EnterPlaymode + 等 isPlaying success success capture_game_view ScreenCapture png base64 图片 1(初始 UI 含 Start 按钮) simulate_mouse_click x=960 y=540 Event MouseDown/Up success success capture_game_view ScreenCapture png base64 图片 2(应该看到角色) get_console_logs filter=Player { entries: [Player spawned at ...] } exit_play_mode ExitPlaymode success

整个验证不需要写一行测试代码------AI 通过截图判断 UI 状态、通过日志确认业务事件、通过比较两张截图判断角色是否出现。所有工具都是声明式调用,AI 客户端把它们拼成有意义的验证序列。

8. PlayMode 退出的状态清理

exit_play_mode 看起来只是 EditorApplication.ExitPlaymode() 的薄封装,但实际有几个隐藏陷阱:

  • 退出动作也是异步的 ------同样需要等 !isPlaying && !isPlayingOrWillChangePlaymode
  • PlayMode 期间创建的 GameObject 在退出后会消失 ------这与 Edit 模式下的 GameObject.CreatePrimitive 行为完全不同
  • PlayMode 期间的脚本字段修改不会持久化到 EditMode ------除非显式调用 EditorUtility.CopySerialized

Funplay 的 exit_play_mode 工具默认会等待退出完全完成才返回 success,并附带提示告诉客户端"PlayMode 期间的修改已丢弃"。这种显式提示能避免 AI 在退出后误以为对象/字段还在。

9. 与 EditMode 工具的协同

PlayMode 工具不是孤立的------它们与 EditMode 的场景编辑工具配合使用才有意义:
根据结果调整
EditMode 阶段
set_component_property 改字段
execute_code 改脚本
request_recompile 触发编译
PlayMode 阶段
enter_play_mode
simulate_mouse_click 测试交互
capture_game_view 抓状态
get_console_logs 读断言
exit_play_mode

AI 客户端在"修改 + 验证"循环里会不断在两组工具之间切换。Funplay 在工具命名、返回结构、错误码上保持一致,让这种切换对 LLM 是无缝的------任何一类工具调用失败都会返回相同形态的 error JSON,AI 不需要为不同工具组学习不同的错误处理逻辑。

10. 写在最后

让 AI 能"看见游戏在跑"是 Unity AI 工具区别于通用 IDE AI 的关键能力。单元测试无法覆盖的视觉/交互/时序问题,PlayMode 视觉闭环都能让 AI 自主验证、自主迭代。

实现层面要解决的核心问题是:异步状态等待(PlayMode 进出)、输入事件 dispatch(两套 Input 系统)、截图编码与传输(base64 体积、Game View 选择)、日志读取(反射 internal API)。每一项都有自己的踩坑空间,但落地后形成的回环是质变的------AI 不再是"写完代码等人测",而是"写完代码自己测、看到问题自己调"。

Funplay Unity MCP 的完整 PlayMode 工具实现在 FunplayAI/funplay-unity-mcp,源码位于 Editor/Tools/Builtins/PlayMode*.csInputSimulation*.csScreenCapture*.cs。MIT 协议,欢迎 issue 与讨论。

相关推荐
Harvy_没救了1 小时前
【github 爆款】Agent Skill项目全景汇报 + 联动deepseek-TUI
人工智能·github
学术小白人1 小时前
往届EI检索稳定!第二届可信大数据与人工智能学术会议(ICTBAI 2026)
大数据·人工智能·物联网·microsoft·数字能源
阳明山水1 小时前
MAPE仅2%为何业务仍不满意?
人工智能·深度学习·机器学习·微信·微信开放平台
千里马学框架1 小时前
WMS/AMS深入WindowState如何正确找到自己在层级结构树中位置进行挂载
android·wms·ai编程·性能·系统开发·车载开发·framework工程师
wuxinyan1231 小时前
工业级大模型学习之路011:RAG 零基础入门教程(第七篇):查询优化技术
人工智能·学习·rag
caijing3651 小时前
全方位解析建筑设备系统解决方案:提升建筑效率与安全的关键
大数据·人工智能·安全
code bean1 小时前
【LangChain】 输出解析器(Output Parsers)完全指南
大数据·人工智能·langchain
薛定猫AI1 小时前
Codex 与 Claude Code 安装配置完整教程(Windows/Mac/Linux)
人工智能
LinDaiDai_霖呆呆1 小时前
做 Agent 开发入门必懂的 10 个 Agent 核心概念
前端·agent·ai编程