feat(InBox): save zhihu article - Codex基于规则的Unity主线自动化测试实现

This commit is contained in:
liulu
2026-06-20 03:08:51 +08:00
parent 595e2bef57
commit 842022c23b

View File

@@ -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
如果项目主要逻辑在 LuaC# 可以只做时间轮和调度,动作内部调用 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 策略。