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 与讨论。

相关推荐
cxr8289 分钟前
高分子复合材料 AI 逆向设计合——验证闭环、决策优化与中试放大
人工智能·材料逆向设计合成
win4r9 分钟前
MiniMax M3 深度体验:这可能是国产模型里最接近“全能工程师”的一次
aigc·ai编程·claude
litble15 分钟前
如何速成LLM以伪装成一个AI研究者(6)——LoRA,Adapter,P-tuning,量化,QLoRA
人工智能·lora·量化·peft·qlora·高效微调
开发者每周简报23 分钟前
网海三部曲·无名宗师传
javascript·人工智能
卷毛的技术笔记37 分钟前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
Cosolar1 小时前
从零写一个 Attention Is All You Need
人工智能·面试·架构
adrninistrat0r1 小时前
Java调用链MCP分析工具
java·python·ai编程
ai_xiaogui1 小时前
PanelAI:新一代AI算力调度系统,支持本地大模型一键部署与商业运营
人工智能·panelai·panelai算力调度系统·本地大模型一键部署平台·ai应用市场管理面板·企业级部署·2026本地ai私有化解决方案
冬奇Lab1 小时前
Agent 系列(9):多 Agent 架构设计模式——Supervisor 与 Pipeline
人工智能·源码·agent