--- source: 知乎专栏 url: https://zhuanlan.zhihu.com/p/2047460516247888185?utm_psn=2051393077303103836 created: 2026-06-20 tags: [unity, 自动化测试, codex, 游戏开发] --- # Codex基于规则的 Unity 主线自动化测试实现 > 来源:[知乎专栏](https://zhuanlan.zhihu.com/p/2047460516247888185?utm_psn=2051393077303103836) 今天在水群中讨论到关于实现游戏自动化相关的内容是,我分析了《网易伏羲实验室-智能游戏测试.pdf》中的任务回归测试思路,然后也基于我们已经实现过的一套自动化思路,整理一套非 AI、规则驱动、可快速落地的 Unity 主线自动化测试实现方案。然后我用 codex 把自己的想法在基于上面的网易伏羲实验室-智能游戏测试的基础思路上,实现自己的构想。下面是 codex 给我的它认为可行的方案,基于此,我先分享出来,有没有时间在验证。 1. 想法是否可行 可行。 PDF 中任务回归测试的核心思路是: 不一定从图像识别开始,可以直接从游戏引擎获取状态。 将游戏状态抽象为任务 ID、任务步骤、角色状态、UI 状态、敌人状态、背包状态等。 将游戏操作封装成动作,例如 RunToObj、ClickObj、UseItem、CastSkill、SubmitTask。 根据当前状态提取合法动作,再执行动作。 从游戏画面中提取当前可执行的游戏状态 将画面中的各类游戏状态提取出来 提取出来的各种游戏状态,扔到动作集合中 对于某个动作,制定执行的规则,然后根据提取的数据,匹配动作是否满足执行条件 上面这几轮是《网易伏羲实验室-智能游戏测试》的核心思路。 基于此,我的想法是这个方案的简化版本: 判断当前界面 / 游戏状态属于哪类 -> 主线、战斗、剧情、引导、Loading、死亡等 -> 执行对应封装动作 -> 用时间轮持续轮询 大体的方案流程 这不需要强化学习,也不需要图像识别。只要项目代码能暴露当前状态和基础操作接口,就可以先跑通一条主线自动化流程。 适合场景: 主线任务自动跑通。 新手流程冒烟测试。 每日构建基础回归。 检查 NPC 不可达、任务卡住、对话断链、战斗无法结束等问题。 不适合场景: 无规则探索整个游戏。 UI 遮挡、花屏、黑边等图像异常检测。 复杂策略玩法验证。 需要大量随机探索的开放世界测试。 2. 最简架构 核心模块可以拆成四部分: GameStateProvider:获取当前游戏状态 ActionClassifier:判断当前应该执行哪类动作 AutoAction:封装具体动作 AutoRunner:时间轮轮询执行 整体流程: 每 0.5 秒轮询一次 -> 读取当前 GameState -> 按优先级判断状态类型 -> 执行对应动作 -> 等待下一轮 推荐状态优先级: 死亡 > Loading > 引导 > 剧情 / 对话 > 战斗 > 可提交任务 > 主线寻路 > 完成 原因是这些状态有明显的阻塞关系。比如角色死亡时,不应该继续寻路;Loading 时不应该点击 UI;剧情对话没点完时,也不能继续执行战斗或提交任务。 3. 状态抽象 最小状态可以这样设计: public class GameState { public int questId; public int questStep; public bool isLoading; public bool isDead; public bool guideOpened; public bool dialogueOpened; public bool inBattle; public bool canSubmitQuest; public bool finished; public int enemyId; } 真实项目中可以继续扩展: 状态 示例 任务状态 当前任务 ID、步骤 ID、目标 NPC、目标怪物 角色状态 坐标、是否死亡、是否寻路中、是否战斗中 UI 状态 是否打开引导、剧情、背包、奖励界面 战斗状态 敌人 ID、距离、血量、技能 CD 背包状态 是否有任务道具、是否需要使用道具 场景状态 当前地图、是否 Loading、是否副本中 4. 动作抽象 最小动作接口: public interface IGameApi { GameState GetState(); void ClickGuide(); void ClickDialogueNext(); void RunToQuestTarget(); void CastSkill(int enemyId, int skillId); void SubmitQuest(); void Revive(); } 后续可以补充: RunToNpc(npcId) RunToObj(objId) ClickObj(objId) UseItem(itemId) PickUpItem(itemId) EnterDungeon(dungeonId) ExitUI() ClaimReward() 动作封装的原则: 动作要尽量原子化。 动作内部可以调用项目已有 C# 或 Lua 方法。 动作执行后不要假设一定成功,下一轮仍然要重新读取状态。 高风险动作,例如消耗货币、购买、强化,先不要纳入最小流程。 5. Unity C# 最小可跑 Demo 新建脚本: QuestAutoRunner.cs 挂到场景任意 GameObject 上即可运行。 下面代码使用 MockGameApi 模拟项目接口,可以直接在 Unity 中看到自动流程日志。 using System.Collections; using UnityEngine; public class QuestAutoRunner : MonoBehaviour { private IGameApi game; private bool running; private void Start() { game = new MockGameApi(); running = true; StartCoroutine(TimeWheel()); } private IEnumerator TimeWheel() { while (running) { GameState state = game.GetState(); ActionType type = Classify(state); Debug.Log($"[AutoTest] state={type}, questStep={state.questStep}"); switch (type) { case ActionType.Guide: game.ClickGuide(); break; case ActionType.Dialogue: game.ClickDialogueNext(); break; case ActionType.Battle: game.CastSkill(state.enemyId, 1001); break; case ActionType.MainQuest: game.RunToQuestTarget(); break; case ActionType.SubmitQuest: game.SubmitQuest(); break; case ActionType.Finished: Debug.Log("[AutoTest] 主线流程已跑完"); running = false; break; case ActionType.Loading: Debug.Log("[AutoTest] Loading 中,等待"); break; case ActionType.Dead: game.Revive(); break; } yield return new WaitForSeconds(0.5f); } } private ActionType Classify(GameState s) { if (s.finished) return ActionType.Finished; if (s.isDead) return ActionType.Dead; if (s.isLoading) return ActionType.Loading; if (s.guideOpened) return ActionType.Guide; if (s.dialogueOpened) return ActionType.Dialogue; if (s.inBattle && s.enemyId > 0) return ActionType.Battle; if (s.canSubmitQuest) return ActionType.SubmitQuest; return ActionType.MainQuest; } } public enum ActionType { Loading, Dead, Guide, Dialogue, Battle, MainQuest, SubmitQuest, Finished } public class GameState { public int questId; public int questStep; public bool isLoading; public bool isDead; public bool guideOpened; public bool dialogueOpened; public bool inBattle; public bool canSubmitQuest; public bool finished; public int enemyId; } public interface IGameApi { GameState GetState(); void ClickGuide(); void ClickDialogueNext(); void RunToQuestTarget(); void CastSkill(int enemyId, int skillId); void SubmitQuest(); void Revive(); } public class MockGameApi : IGameApi { private GameState state = new GameState { questId = 10001, questStep = 0, guideOpened = true }; public GameState GetState() { return state; } public void ClickGuide() { Debug.Log("[GameApi] 点击引导按钮"); state.guideOpened = false; state.dialogueOpened = true; state.questStep = 1; } public void ClickDialogueNext() { Debug.Log("[GameApi] 点击剧情对话下一步"); state.dialogueOpened = false; state.inBattle = true; state.enemyId = 20001; state.questStep = 2; } public void RunToQuestTarget() { Debug.Log("[GameApi] 自动寻路到主线目标"); state.canSubmitQuest = true; state.questStep = 3; } public void CastSkill(int enemyId, int skillId) { Debug.Log($"[GameApi] 对 enemy={enemyId} 释放 skill={skillId}"); state.inBattle = false; state.enemyId = 0; state.canSubmitQuest = true; state.questStep = 3; } public void SubmitQuest() { Debug.Log("[GameApi] 提交任务"); state.canSubmitQuest = false; state.finished = true; state.questStep = 4; } public void Revive() { Debug.Log("[GameApi] 复活角色"); state.isDead = false; } } 6. 运行效果 运行后 Console 会看到类似日志: [AutoTest] state=Guide, questStep=0 [GameApi] 点击引导按钮 [AutoTest] state=Dialogue, questStep=1 [GameApi] 点击剧情对话下一步 [AutoTest] state=Battle, questStep=2 [GameApi] 对 enemy=20001 释放 skill=1001 [AutoTest] state=SubmitQuest, questStep=3 [GameApi] 提交任务 [AutoTest] state=Finished, questStep=4 [AutoTest] 主线流程已跑完 这就完成了一个最小主线自动化闭环: 引导 -> 剧情 -> 战斗 -> 提交任务 -> 完成 7. 接入真实项目 真实项目里,只需要把 MockGameApi 换成真实游戏接口。 示例: public class RealGameApi : IGameApi { public GameState GetState() { return new GameState { questId = QuestManager.CurrentQuestId, questStep = QuestManager.CurrentStep, guideOpened = GuideManager.IsGuideShowing, dialogueOpened = DialogueManager.IsDialogueOpen, inBattle = BattleManager.InBattle, enemyId = BattleManager.GetNearestEnemyId(), canSubmitQuest = QuestManager.CanSubmit(), isLoading = SceneLoader.IsLoading, isDead = PlayerManager.IsDead }; } public void ClickGuide() { GuideManager.ClickCurrentGuide(); } public void ClickDialogueNext() { DialogueManager.ClickNext(); } public void RunToQuestTarget() { QuestManager.AutoRunToCurrentTarget(); } public void CastSkill(int enemyId, int skillId) { BattleManager.CastSkill(enemyId, skillId); } public void SubmitQuest() { QuestManager.SubmitCurrentQuest(); } public void Revive() { PlayerManager.Revive(); } } 然后在 Start 中替换: game = new RealGameApi(); 8. 如果项目逻辑在 Lua 如果项目主要逻辑在 Lua,C# 可以只做时间轮和调度,动作内部调用 Lua。 示例: public void RunToQuestTarget() { LuaManager.Call("AutoTest.RunToQuestTarget"); } public void ClickDialogueNext() { LuaManager.Call("AutoTest.ClickDialogueNext"); } public void SubmitQuest() { LuaManager.Call("AutoTest.SubmitQuest"); } Lua 侧再封装真实逻辑: AutoTest = {} function AutoTest.RunToQuestTarget() QuestManager:AutoRunToCurrentTarget() end function AutoTest.ClickDialogueNext() DialogueManager:ClickNext() end function AutoTest.SubmitQuest() QuestManager:SubmitCurrentQuest() end 9. 最小版本先别做太复杂 第一版建议只支持这些动作: 动作 目的 ClickGuide 跳过或点击引导 ClickDialogueNext 推进剧情 RunToQuestTarget 自动寻路到任务目标 CastSkill 自动战斗 SubmitQuest 提交任务 Revive 死亡后复活 先跑通一条主线,再逐步补: UseItem PickUpItem RunToNpc EnterDungeon ExitUI ClaimReward RunToTeleport 不要一开始就做复杂决策,否则会很快变成另一个难维护的自动化系统。 10. 总结 这套方案可以理解为“规则版智能任务测试”: 状态抽象 + 动作封装 + 优先级判断 + 时间轮轮询 它没有 AI,也不做复杂搜索,但胜在简单、稳定、容易接入项目代码。对于主线任务、新手流程和每日构建冒烟测试来说,已经足够有价值。 后续如果需要提升覆盖率,再考虑引入更复杂的动作选择、任务文本解析、路径搜索或 AI 策略。