自动战斗(Auto Fight)
简介(Introduction)
本文档记录了BAAS自动战斗的框架以及原理, 并通过一个例子教学如何按照BAAS的自动战斗框架的规范书写自动战斗的轴文件
INFO
如果你是C++小白,不用理解代码是如何书写的,重点关注轴文件的格式, 跟着本文的指导一步一步理解也可以写出正确的轴文件
WARNING
轴文件为json格式, 请先学习json的书写规范, 否则你可能会因为json格式错误导致轴文件无法被解析
- 首先明确一点, 这里的自动战斗并非指使用游戏内的auto让角色盲目的释放技能, 而是指BAAS通过截取战斗时的图像并提取其中的信息, 根据一个用户指定的
流程
在恰当的条件
下释放技能
- 这个流程简称为
轴
,轴
是一个json
格式的文件, 里面包含了自动战斗的所有信息 - 使用者可以根据需求与自动战斗规范书写
轴
文件, BAAS负责解析轴
是否合法并运行合法的轴 - BAAS的自动战斗运作理念类似于编译原理中的有限状态自动机
例子(Example)
以下图的简单流程作为自动战斗轴文件的书写例子, 当你理解了这个例子, 你就能完全理解BAAS自动战斗的工作原理, 以及轴文件该如何书写
观察这个流程图, 我们可以发现:
流程中存在以下条件
condition
(每一个箭头上的文字)- 血量 > 500w
- 血量 < 500w
- 血量 > 0
- 血量 < 0
流程中存在以下动作
action
- 释放技能1 (
release_skill_1
) - 释放技能2 (
release_skill_2
) - 重开 (
restart
)
- 释放技能1 (
我们可以将流程拆分为以下状态
state
- 自动战斗开始 (
start
)- 下一个状态 : 释放技能1
- 释放技能1 (
release_skill_1
)- 动作 : 释放技能1
- 状态转移 :
- 条件 : 血量 > 500w | 下一个状态 : 重开
- 条件 : 血量 < 500w | 下一个状态 : 释放技能2
- 释放技能2 (
release_skill_2
)- 动作 : 释放技能2
- 状态转移 :
- 条件 : 血量 > 0 | 下一个状态 : 重开
- 条件 : 血量 < 0 | 下一个状态 : 结束
- 重开 (
restart
)- 动作 : 重开
- 下一个状态 : 释放技能1
- 结束 (
end
)
- 自动战斗开始 (
INFO
你或许会疑惑state
中的释放技能1 / 释放技能2 / 重开 是不是与上面的action
重复了, 这实际上是完全不同的概念, 状态名(state
)是可以和动作(action
)名相同的, 请注意区分同名的状态和动作
- 根据以上例子, 你可能发现了, BAAS自动战斗轴文件本质上需要你完成三个内容:
队伍配置
- 通过规定角色练度确定是否可以使用这个轴 (可选)
- 选择初始技能
截图数据监测
- 相关代码见
apps/BAAS/src/module/auto_fight/screenshot_data
文件夹
可监测的数据
以下图为例介绍在自动战斗中图像中可被提取的数据
- BOSS的最大血量 / 总血量 Code
- 每个技能槽学生的技能名以及技能释费用 Code
- (倍速 Code) / (自动 Code) 状态
- 当前可用于释放技能的费用 Code
- 战斗剩余时间
- 房间剩余时间
- 学生位置坐标及
- 敌对角色的位置
数据记录类
screenshot_data_recorder 使用类来记录可监测的数据中的数据, 以供动作(action) 和 条件(condition)使用
数据更新
按照下图的架构实现自动战斗的数据更新
每个数据更新都对应一个类, 他们都继承自
class BaseDataUpdater
类screenshot_data
中d_updater_mask
用于指示一轮数据更新中哪些update()
函数需要被调用, 每一位对应一个数据更新类, 对应关系如下Bit Updater 1
CostUpdater
2
BossHealthUpdater
3
SkillNameUpdater
4
SkillCostUpdater
5
AccelerationPhaseUpdater
6
AutoStateUpdater
example: 当
d_updater_mask
被设置为0b1101
则表示CostUpdater
,SkillNameUpdater
和SkillCostUpdater
的update()
函数需要被调用数据更新多线程并行进行, 线程数量由配置中的
/auto_fight/d_update_max_thread
字段决定数据更新任务提交至线程池的顺序由每个
updater
重写的estimated_time_cost
决定, 按照更新耗时由小到大进行更新
基准测试(Benchmark)
在不同的设备下测试了BAAS自动战斗各模块的性能, 数据仅供参考
模拟器截图/控制速度测试
截图/控制速度主要与模拟器 和 CPU 有关, 推荐使用雷电
模拟器 / MuMu
模拟器
CPU / 模拟器 | 雷电 | MuMu |
---|---|---|
Amd Ryzen 9 9950x | 0.8ms - 2.5ms 平均1.5ms | 1.9ms - 3.4ms 平均2.5ms |
Amd Ryzen 6 6800H | - | 4.9ms - 8.2ms 平均7.1ms |
Intel Core i5-9300H | - | 12.1ms - 21.3ms 平均15.3ms |
Intel Core i9-13900HX | 1.7ms - 6.8ms 平均3.5ms | 4.2ms - 8.7ms 平均5.9ms |
- 以上截图方式分别为
- 雷电 : ldopengl
- MuMu : nemu
CPU + 控制模式 / 模拟器 | MuMu |
---|---|
Amd Ryzen 9 9950x +adb | 4.9ms - 7.1ms 平均6.1ms |
Amd Ryzen 9 9950x +nemu | 40us - 90us 平均50us |
Amd Ryzen 9 9950x +scrcpy | 5.2ms - 5.9ms 平均5.5ms |
Amd Ryzen 6 6800H +adb | 12.5ms - 14.7ms 平均13.3ms |
Amd Ryzen 6 6800H +nemu | 43us - 87us 平均59us |
Amd Ryzen 6 6800H +scrcpy | 5.6ms - 5.7ms 平均5.6ms |
Intel Core i9-13900HX +adb | 11.9ms - 16.3ms 平均13ms |
Intel Core i9-13900HX +nemu | 50us - 110us 平均70us |
Intel Core i9-13900HX +scrcpy | 5.3ms - 6.4ms 平均5.6ms |
数据更新速度测试
纯CPU
更新数据CPU / 数据 Cost
SkillName
ObjPos
Amd Ryzen 9 9950x
3us - 9us
平均6us
1.8ms - 2.1ms
平均2ms
33.1ms - 39.5ms
平均35ms
Amd Ryzen 6 6800H
6us - 28us
平均15us
3.1ms - 4.5ms
平均3.5ms
79.1ms - 91.0ms
平均86ms
Intel Core i5-9300H
6us - 53us
平均20us
10.4ms - 14.9ms
平均12.5ms
159.3ms - 222.5ms
平均175.5ms
Intel Core i9-13900HX
3us - 19us
平均7us
1.9ms - 5.4ms
平均3.6ms
70.1ms - 107.0ms
平均90ms
CPU / 数据 BossHealth
SkillCost
Acc
Amd Ryzen 9 9950x
5.0ms - 9.0ms
平均7.8ms
1.6ms - 3ms
平均1.8ms
6us - 24us
平均13us
Amd Ryzen 6 6800H
35.0ms - 50.0ms
平均39ms
6.4ms - 8.4ms
平均7.5ms
14us - 21us
平均20us
Intel Core i5-9300H
52.0ms - 79.3ms
平均65ms
16.0ms - 22.7ms
平均20.2ms
25us - 36us
平均30us
Intel Core i9-13900HX
31.4ms - 90.1ms
平均60ms
5.1ms - 18.9ms
平均8.5ms
13us - 23us
平均16us
- note:
SkillCost
指更新一个槽技能的时间, 三个槽时间需要 x 3
CPU / 数据 Auto
Amd Ryzen 9 9950x
9us - 24us
平均16us
Amd Ryzen 6 6800H
16us - 22us
平均20us
Intel Core i5-9300H
27us - 43us
平均30us
Intel Core i9-13900HX
13us - 23us
平均19us
- note:
YOLO模型推理 :
CUDA加速
和纯CPU
速度对比
设备 / 数据 | ObjPos |
---|---|
RTX 5090 | 9.1ms - 11.0ms 平均 10.1ms |
Amd Ryzen 9 9950x | 33.1ms - 39.5ms 平均 35ms |
Amd Ryzen 6 6800H | 79.1ms - 91.0ms 平均 86ms |
Intel Core i5-9300H | 159.3ms - 222.5ms 平均 175.5ms |
Intel Core i9-13900HX | 70.1ms - 107.0ms 平均 90ms |
基本配置
下图为一个轴文件的基本配置, 规定了一下自动战斗的基本信息
- Boss血量的文字识别配置
- YOLO目标检测配置
- 出场角色以及所有出现的技能配置
你可以在本文找到这些配置的含义, 一般来说, 你只需要设置 3
所相关的内容, 也就是formation
字段下的内容
{
"formation": {
"front": ["Kayoko", "Koharu", "Mika", "Eimi"],
"back": ["Himari", "Fuuka (New Year)"],
"slot_count": 3,
"all_appeared_skills": [
"Himari",
"Kayoko",
"Fuuka (New Year)",
"Mika",
"Koharu",
"Eimi"
]
},
"BossHealth": {
"current_ocr_region": [549, 45, 656, 60],
"max_ocr_region": [666, 45, 775, 60],
"ocr_region": [549, 45, 775, 60],
"ocr_model_name": "en-us"
},
"yolo_setting": {
"model": "best.onnx",
"update_interval": 100
}
}
/formation/front
- description: 突击角色的名称列表
- type:
list
- elements:
string
: 角色名称
- note:
- 这些名称直接决定了yolo模型检测的角色列表
resource/yolo_models/data.yaml
中names
列举了所有可以被yolo模型识别的角色列表, 同时这也是BAAS YOLO模型的训练配置- 目前可识别的角色还较少, 随着数据集的逐渐扩充, BAAS将会适配大部分的学生 / 敌对角色 的识别, 如果你愿意为BAAS做一点贡献, 欢迎你参与数据集标注工作, 请在qq群内联系作者
/formation/back
- description: 后排角色的名称列表
- type:
list
- elements:
string
: 角色名称
/formation/slot_count
- description: 技能槽的数量
- type:
int
- note: 一般设置为
3
, 未来可能会支持更多的技能槽(爬塔玩法中六槽)
/formation/all_appeared_skills
- description: 所有出现的技能名称列表
- type:
list
- elements:
string
: 技能名称
- note:
- BAAS自动战斗在检测技能时, 只会检测该配置列出的技能
- 你可以在
/resource/images/CN/zh-cn/skill/active
查询已被录入的技能, 这个列表的技能名与该文件夹下的图片名一一对应
/BossHealth/current_ocr_region
- description: BOSS当前血量的文字识别区域
- type:
list
- elements:
int
: 文字识别区域的坐标, 由四个整数值组成, 分别表示左上角x坐标
,左上角y坐标
,右下角x坐标
,右下角y坐标
- length:
4
/BossHealth/max_ocr_region
- description: BOSS最大血量的文字识别区域
- type:
list
- elements:
int
- length:
4
- note: 这个区域的坐标和
/BossHealth/current_ocr_region
相同, 但是它的坐标是BOSS最大血量的坐标
/BossHealth/ocr_region
- description: BOSS血量的文字识别区域
- type:
list
- elements:
int
: 文字识别区域的坐标, 由四个整数值组成, 分别表示左上角x坐标
,左上角y坐标
,右下角x坐标
,右下角y坐标
- length:
4
- note: 这个区域的坐标同时包含最大血量和当前血量
/BossHealth/ocr_model_name
- description: 文字识别模型的名称
- type:
string
- note: 一般不需要修改
/yolo_setting/model
- description: YOLO模型的名称
- type:
string
- constrains:
值 含义 best.onnx
fp32模型 best_fp16.onnx
fp16模型
状态(State)
states
- 所有状态都在
states
中定义, 它是一个字典, 每个键值对表示一个状态,键表示状态名
, 值表示状态的参数
start_state
- 它必须在自动战斗流程文件中被定义, 指示自动战斗开始时的进入的状态
- 它的值是一个字符串, 和
states
中的一个状态名相等
example:
- 下图在
states
中定义了三个状态,状态一
/状态二
/状态三
, 起始状态为状态一
{
"state_state": "状态一",
"states": {
"状态一": {
},
"状态二": {
},
"状态三": {
}
}
}
单个state
的参数
states
中的例子列举了三个状态, 但是他们并没有任何实际内容, 我们需要在单个状态中设置以下参数以赋予状态意义
note: 以上个参数都是可选的, 你可以根据需要自行选择
action
- description: 到达这个状态后立即执行的行为(如技能释放, 开启auto/倍速, 重开战斗, 尝试跳过转阶段动画等)
- type:
string
- constrains: 指定的动作必须在
actions
中被定义
action_fail_transition
- description: 当
action
执行失败时(如未跳过转阶段动画时), 自动战斗会转移到这个状态 - type:
string
- constrains: 指定的状态必须在
states
中被定义
transitions
default_transition
- 当
transitions
中的所有条件都不被满足时(或transitions
没有任何条件), 默认转移状态
一个状态转移
每个状态转移转移表示在某个条件成立时转移到下一个状态, 我们需要指定条件
和 下一个状态
, 分别对应以下参数
condition
- 状态转移的条件名, 这个条件必须在
conditions
中被定义 - 它的值是一个字符串, 必须在
conditions
中被定义
- 状态转移的条件名, 这个条件必须在
next
示例
example:
- 当自动战斗转移到这个状态时, 会立即执行
释放技能一
- 如果释放技能失败, 转移到
重新开始战斗
- 如果释放技能成功, 并且
boss血量小于500w
, 转移到释放技能二
- 否则转移到
重新开始战斗
{
"状态一": {
"action": "释放技能一",
"action_fail_transition": "重新开始战斗",
"transitions": [
{
"condition": "boss血量小于500w",
"next": "释放技能二"
}
],
"default_transition": "重新开始战斗"
}
}
note:这个例子中的action
以及 condition
都还未被定义, 你需要在actions
和 conditions
中学习如何定义这些动作和条件
注意事项
- 初始状态
start_state
必须被定义 - 结束条件: 自动战斗会在没有任何可转移状态时退出循环, 可能情况如下:
transitions
中任何条件都不成立, 并且没有default_transition
transitions
中没有条件, 并且没有default_transition
- 值得一提的是, 没有
default_transition
, 自动战斗也可以实现它的功能, 你只需要找到所有其他都不成立的条件, 并将其作为transition的最后一个条件也可以实现default_transition
的功能
动作(Action)
actions
- 所有动作都在
actions
中定义, 它是一个字典, 每个键值对表示一个动作序列,键表示动作名
, 值是一个列表 - 注意再次强调, 每个
action
的值是一个列表, 这个列表中的每个元素表示一个动作
, 你可以将它理解为一个动作序列, 这个动作序列会被依次执行
example:
{
"actions": {
"释放技能1": [
{
"desc": "释放技能1的第一个操作"
},
{
"desc": "释放技能1的第二个操作"
}
],
"释放技能2": [
{
"desc": "释放技能2的第一个操作"
}
]
}
}
note: 设置单一action
是一个动作列表有许多好处, 如下
- 允许你自由定义技能释放流程
- 释放完第一个技能后你可以立刻释放第二个技能, 实现游戏中
反手拐
(在主c技能释放后释放辅助增伤技能)的效果 - 释放技能前你可以选择调整游戏倍速为
1
- 释放技能后你可以选择调整游戏倍速回到
3
- 释放完第一个技能后你可以立刻释放第二个技能, 实现游戏中
- 简化了
state
的书写state
仅需指定action
的名称, 而不需要重写所有action
单个action
的参数
actions
中的例子列举了一些动作, 但是他们并没有任何实际内容, 我们需要在单个动作中设置以下参数以赋予状态意义
- 首先你需要通过
t
字段指定action
的类型, 合法的t
如下, 接着你需要根据t
的值设置额外参数t
含义
额外需要的设置的参数 acc
调整游戏 倍速
acc动作额外参数
auto
调整游戏 auto
状态auto动作额外参数
skill
释放技能 skill动作额外参数
条件(Condition)
条件类(Condition Class)
我们希望可监测的数据满足一些条件时进行自动化操作, 条件类让我们能用一种规范的语言表达我们所需要的条件
- 为了持续监测条件
- 在循环中依次执行
截图 --> 数据更新 --> 条件判断
- 数据更新前, 遍历每个
condition
, 通过标志位指示需要被更新的数据 - 数据更新前, 首先判断界面是否为战斗中, 非战斗中则重新截图
- 数据更新前, 等待正在更新数据线程数为
0
(防止同一数据更新同时进行) - 由上一条可得, 如果
一条数据
不被当前任意一个condition
所需要, 则不更新会该数据 - 当某一条件成立 / 所有条件不成立时结束判断
- 在循环中依次执行
- 单一状态信息更新后紧接着进行条件判断
- 条件判断的耗时远低于数据更新, 如果有多条数据同时在更新, 其中一条数据更新完毕后立即进行条件判断
- 根据可监测的数据中任意一个进行比较的条件称为
原始条件 (primitive condition)
- 条件可根据
or / and
进行自由组合, 称为组合条件
- 每个条件包含
timeout
字段- 条件判断开始后时限内期望条件未被判断为(未)成立则认为该条件不成立
- 设计
timeout
的一大原因是可以有效避免条件判断陷入死循环
- 条件类仅在加载自动战斗工作流时初始化, 条件类可被重复使用, 每次使用
condition
前刷新上一次使用的数据 desc
字段作为这个条件的描述, 方便理解, 并不会对条件判断产生任何影响
条件判断 (Condition Judgement)
按照下图的架构实现自动战斗的条件判断, 该架构设计参照条件类所需的特性
conditions
- 所有条件都在
conditions
中定义, 它是一个字典, 每个键值对表示一个条件,键表示条件名
, 值是一个字典
example:
{
"conditions": {
"条件1": {
},
"条件2": {
}
}
}
重开条件(Restart Condition)
重开是凹分必不可少的环节之一, 列举以下重开条件以供参考
BOSS血量范围
- description: 检查BOSS血量是否在某一范围
- checkpoint:
- 战斗剩余时间达到某值
- 释放技能后x秒
- usage:
- 技能未暴击
- 其他异常 (学生退场 / 寿司开盾减伤 / 黑白转阶段)
BOSS血量减少
- description: 检查BOSS血量是否在某一时间段内下降期望值
- checkpoint:
- 战斗剩余时间达到某值计时x秒
- 释放技能后计时x秒
技能槽
- description:检查学生技能是否出现在技能槽
- checkpoint:
- 战斗剩余时间达到某值计时x秒
- 释放技能后计时x秒
- usage:
- 检查初始技能顺序是否正确
- 学生退场 --> 技能排序变化
auto
异常释放技能
技能Cost
- description:检查技能Cost是否为指定值
- checkpoint:
- 战斗剩余时间达到某值计时x秒
- 释放技能后计时x秒
- usage:
- 检查忧, 枫香(新年) 等减费角色的技能是否释放到期望目标
字段 | 含义 |
---|---|
0 | 开启auto 释放 (确保auto 被选中) |
1 | 自定义点击顺序 |
2 | 保证槽技能被选中-->释放 |
使用前须知
必要的游戏内设置
- 必须关闭游戏内释放技能动画
轴的基本信息
name
(轴名称)
- description: 轴的名称
- type:
string
- example: 特殊委托关卡L, 使用女仆爱丽丝的通关轴
{
"name": "Special-Task-L Aris Maid Workflow"
}
formation (配队信息)
完整组合
{
"name": "Special-Task-L Aris Maid Workflow",
"formation": {
"front": ["Aris (Maid)", "Wakamo", "Kayoko (New Year)", "Ui"],
"back": ["Ako", "Himari"],
"initial_skills": ["Wakamo", "Ako", "Himari"],
"all_appeared_skills": [
"Aris (Maid)",
"Wakamo",
"Kayoko (New Year)",
"Ui",
"Ako",
"Himari"
],
"borrow": ""
},
"battle": {
"boss_max_health": {
"phase1": 188796,
"phase2": 224756,
"phase3": 260729,
"phase4": 287696,
"phase5": 323657
}
}
}
Class AutoFight
- description: 集成 condition / action / state 并最终执行自动战斗的类
成员变量 (Members)
config
logger
default_active_skill_template
/ default_inactive_skill_template
template_j_ptr_prefix
d_update_max_thread
type: int description: 数据更新最大同时运行的线程数量 (线程池大小)
d_update_thread_pool
type: std::unique_ptr<ThreadPool>
description: 数据更新使用的线程池
d_update_thread_mutex
type: std::mutex
description: 用于线程同步的互斥量
d_updater_running_thread_count
type: std::atomic<int>
description: 当前正在运行的数据更新线程数量
d_updater_thread_finish_notifier
type: std::condition_variable
description: 用于通知数据更新线程结束的条件变量
d_updaters
type: std::vector<std::unique_ptr<BaseDataUpdater>>
description: 指向所有数据更新类的指针集合
d_wait_to_update_idx
type: std::vector<uint8_t>
description: 需要被执行的数据更新函数所对应的类在d_updaters
中的索引
d_updater_queue
type: std::queue<uint8_t>
description: 根据数据更新的预估耗时排序后的d_wait_to_update_idx
队列
d_updater_map
type: std::map<std::string, uint64_t>
description: 数据更新类的名称与偏移的映射
d_auto_f
type: auto_fight_d
description: 自动战斗的共享数据, 用于在 action
/ condition
/ updaters
/ state
之间传递数据
_cond_type
type: std::string
description: 目前正在录入的条件的类型名称
all_cond
type: std::vector<std::unique_ptr<BaseCondition>>
description: 所有条件的指针集合
cond_name_idx_map
type: std::map<std::string, uint64_t>
description: 条件名与all_cond
中位置的映射 note:
- 请注意条件名与条件类型名是完全不同的概念
_cond_is_matched_recorder
type: std::vector<bool>
description: 记录条件是否成立, 已被判断成立 / 不成立的条件不会参与以下内容
- 超时检查
- 条件成立判断
note:
- 长度与
all_cond
相同
_cond_checked
type: std::vector<bool>
description: 条件的 超时监测 / 重置状态 / 成立判断 是递归进行的, 记录每一项检查过的条件, 避免无限递归 note:
- 长度与
all_cond
相同
all_state
type: std::vector<state_info>
description: 所有状态集合
_state_trans_name_recorder
type: std::vector<std::vector<std::string>>
description: 每个state可能有多个状态转移, 记录每个状态的每个状态转移的下一个状态名 note: 当一个状态初始化时, 可能它的目标状态未初始化, 此时不知道该状态的索引, 该变量记录所有转移的下一个状态名, 以便再次遍历更新索引
_state_default_trans_name_recorder
type: std::vector<std::optional<std::string>>
description: 作用同_state_trans_name_recorder
, 使用optional
原因为state
允许没有默认转移
state_name_idx_map
type: std::map<std::string, uint64_t>
description: 状态名与all_state
中位置的映射
_curr_state_idx
type: uint64_t
description: 当前状态的索引
_state_cond_j_start_t
type: long long
description: 状态转移 条件判断循环的开始时间(ms)
_state_cond_j_loop_start_t
type: long long
description: 状态转移 条件判断循环每一轮循环的开始时间(ms)
_state_cond_j_elapsed_t
type: long long
description: 状态转移 条件判断循环的总耗时(ms)
_state_trans_cond_matched_idx
type: std::optional<uint64_t>
description: 第一个状态转移条件成立的转移索引
_state_flg_all_trans_cond_dissatisfied
type: bool
description: 指示是否所有状态转移条件都不成立
_state_cond_j_loop_running_flg
type: bool
description: 指示条件判断循环是否正在运行
start_state_name
type: std::string
description: 初始状态的名称
workflow_path
type: std::filesystem::path
description: 轴文件的路径
workflow_name
type: std::string
description: 轴的名称
baas
type: BAAS*
description: BAAS
实例