12 KiB
source, url, created, tags
| source | url | created | tags | ||||
|---|---|---|---|---|---|---|---|
| 知乎专栏 | https://zhuanlan.zhihu.com/p/2047460516247888185?utm_psn=2051393077303103836 | 2026-06-20 |
|
Codex基于规则的 Unity 主线自动化测试实现
来源:知乎专栏
今天在水群中讨论到关于实现游戏自动化相关的内容是,我分析了《网易伏羲实验室-智能游戏测试.pdf》中的任务回归测试思路,然后也基于我们已经实现过的一套自动化思路,整理一套非 AI、规则驱动、可快速落地的 Unity 主线自动化测试实现方案。然后我用 codex 把自己的想法在基于上面的网易伏羲实验室-智能游戏测试的基础思路上,实现自己的构想。下面是 codex 给我的它认为可行的方案,基于此,我先分享出来,有没有时间在验证。
- 想法是否可行
可行。
PDF 中任务回归测试的核心思路是:
不一定从图像识别开始,可以直接从游戏引擎获取状态。 将游戏状态抽象为任务 ID、任务步骤、角色状态、UI 状态、敌人状态、背包状态等。 将游戏操作封装成动作,例如 RunToObj、ClickObj、UseItem、CastSkill、SubmitTask。 根据当前状态提取合法动作,再执行动作。 从游戏画面中提取当前可执行的游戏状态 将画面中的各类游戏状态提取出来 提取出来的各种游戏状态,扔到动作集合中 对于某个动作,制定执行的规则,然后根据提取的数据,匹配动作是否满足执行条件
上面这几轮是《网易伏羲实验室-智能游戏测试》的核心思路。
基于此,我的想法是这个方案的简化版本:
判断当前界面 / 游戏状态属于哪类 -> 主线、战斗、剧情、引导、Loading、死亡等 -> 执行对应封装动作 -> 用时间轮持续轮询 大体的方案流程
这不需要强化学习,也不需要图像识别。只要项目代码能暴露当前状态和基础操作接口,就可以先跑通一条主线自动化流程。
适合场景:
主线任务自动跑通。 新手流程冒烟测试。 每日构建基础回归。 检查 NPC 不可达、任务卡住、对话断链、战斗无法结束等问题。
不适合场景:
无规则探索整个游戏。 UI 遮挡、花屏、黑边等图像异常检测。 复杂策略玩法验证。 需要大量随机探索的开放世界测试。 2. 最简架构
核心模块可以拆成四部分:
GameStateProvider:获取当前游戏状态 ActionClassifier:判断当前应该执行哪类动作 AutoAction:封装具体动作 AutoRunner:时间轮轮询执行
整体流程:
每 0.5 秒轮询一次 -> 读取当前 GameState -> 按优先级判断状态类型 -> 执行对应动作 -> 等待下一轮
推荐状态优先级:
死亡 > Loading > 引导 > 剧情 / 对话 > 战斗 > 可提交任务 > 主线寻路 > 完成
原因是这些状态有明显的阻塞关系。比如角色死亡时,不应该继续寻路;Loading 时不应该点击 UI;剧情对话没点完时,也不能继续执行战斗或提交任务。
- 状态抽象
最小状态可以这样设计:
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
不要一开始就做复杂决策,否则会很快变成另一个难维护的自动化系统。
- 总结
这套方案可以理解为“规则版智能任务测试”:
状态抽象 + 动作封装 + 优先级判断 + 时间轮轮询
它没有 AI,也不做复杂搜索,但胜在简单、稳定、容易接入项目代码。对于主线任务、新手流程和每日构建冒烟测试来说,已经足够有价值。
后续如果需要提升覆盖率,再考虑引入更复杂的动作选择、任务文本解析、路径搜索或 AI 策略。