diff --git a/InBox/zhihu_Codex基于规则的Unity主线自动化测试实现.md b/InBox/zhihu_Codex基于规则的Unity主线自动化测试实现.md new file mode 100644 index 0000000..0ba9cd0 --- /dev/null +++ b/InBox/zhihu_Codex基于规则的Unity主线自动化测试实现.md @@ -0,0 +1,470 @@ +--- +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 策略。