Skip to content

自动战斗(Auto Fight)

简介(Introduction)

本文档记录了BAAS自动战斗的框架以及原理, 并通过一个例子教学如何按照BAAS的自动战斗框架的规范书写自动战斗的轴文件

INFO

如果你是C++小白,不用理解代码是如何书写的,重点关注轴文件的格式, 跟着本文的指导一步一步理解也可以写出正确的轴文件

WARNING

轴文件为json格式, 请先学习json的书写规范, 否则你可能会因为json格式错误导致轴文件无法被解析

  • 首先明确一点, 这里的自动战斗并非指使用游戏内的auto让角色盲目的释放技能, 而是指BAAS通过截取战斗时的图像并提取其中的信息, 根据一个用户指定的流程恰当的条件释放技能
  • 这个流程简称为, 是一个json格式的文件, 里面包含了自动战斗的所有信息
  • 使用者可以根据需求与自动战斗规范书写文件, BAAS负责解析是否合法并运行合法的轴
  • BAAS的自动战斗运作理念类似于编译原理中的有限状态自动机

例子(Example)

以下图的简单流程作为自动战斗轴文件的书写例子, 当你理解了这个例子, 你就能完全理解BAAS自动战斗的工作原理, 以及轴文件该如何书写 procedure_draft

观察这个流程图, 我们可以发现:

  • 流程中存在以下条件condition (每一个箭头上的文字)

    1. 血量 > 500w
    2. 血量 < 500w
    3. 血量 > 0
    4. 血量 < 0
  • 流程中存在以下动作action

    1. 释放技能1 (release_skill_1)
    2. 释放技能2 (release_skill_2)
    3. 重开 (restart)
  • 我们可以将流程拆分为以下状态state

    1. 自动战斗开始 (start)
      • 下一个状态 : 释放技能1
    2. 释放技能1 (release_skill_1)
      • 动作 : 释放技能1
      • 状态转移 :
        1. 条件 : 血量 > 500w | 下一个状态 : 重开
        2. 条件 : 血量 < 500w | 下一个状态 : 释放技能2
    3. 释放技能2 (release_skill_2)
      • 动作 : 释放技能2
      • 状态转移 :
        1. 条件 : 血量 > 0 | 下一个状态 : 重开
        2. 条件 : 血量 < 0 | 下一个状态 : 结束
    4. 重开 (restart)
      • 动作 : 重开
      • 下一个状态 : 释放技能1
    5. 结束 (end)

INFO

你或许会疑惑state中的释放技能1 / 释放技能2 / 重开 是不是与上面的action重复了, 这实际上是完全不同的概念, 状态名(state)是可以和动作(action)名相同的, 请注意区分同名的状态和动作

  • 根据以上例子, 你可能发现了, BAAS自动战斗轴文件本质上需要你完成三个内容:

队伍配置

  • 通过规定角色练度确定是否可以使用这个轴 (可选)
  • 选择初始技能

截图数据监测

  • 相关代码见 apps/BAAS/src/module/auto_fight/screenshot_data 文件夹

可监测的数据

以下图为例介绍在自动战斗中图像中可被提取的数据

  • total_assault_general.png
  1. BOSS的最大血量 / 总血量 Codeboss_health.png
  2. 每个技能槽学生的技能名以及技能释费用 Codestudent_skill.png
  3. (倍速 Code) / (自动 Code) 状态 acc_auto_phase.png
  4. 当前可用于释放技能的费用 Codecurrent_cost.png
  5. 战斗剩余时间 fight_left_time.png
  6. 房间剩余时间 room_left_time.png
  7. 学生位置坐标及 student_position.png
  8. 敌对角色的位置 enemy_position.png

数据记录类

screenshot_data_recorder 使用类来记录可监测的数据中的数据, 以供动作(action)条件(condition)使用

数据更新

按照下图的架构实现自动战斗的数据更新 screenshot_data_update_cycle.png

  • 每个数据更新都对应一个类, 他们都继承自class BaseDataUpdaterscreenshot_datad_updater_mask 用于指示一轮数据更新中哪些update() 函数需要被调用, 每一位对应一个数据更新类, 对应关系如下

    BitUpdater
    1CostUpdater
    2BossHealthUpdater
    3SkillNameUpdater
    4SkillCostUpdater
    5AccelerationPhaseUpdater
    6AutoStateUpdater

    example: 当d_updater_mask 被设置为 0b1101 则表示 CostUpdater , SkillNameUpdaterSkillCostUpdaterupdate() 函数需要被调用

  • 数据更新多线程并行进行, 线程数量由配置中的/auto_fight/d_update_max_thread字段决定

  • 数据更新任务提交至线程池的顺序由每个updater重写的estimated_time_cost决定, 按照更新耗时由小到大进行更新

基准测试(Benchmark)

在不同的设备下测试了BAAS自动战斗各模块的性能, 数据仅供参考

模拟器截图/控制速度测试

截图/控制速度主要与模拟器 和 CPU 有关, 推荐使用雷电模拟器 / MuMu模拟器

CPU / 模拟器雷电MuMu
Amd Ryzen 9 9950x0.8ms - 2.5ms 平均1.5ms1.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-13900HX1.7ms - 6.8ms 平均3.5ms4.2ms - 8.7ms 平均5.9ms
  • 以上截图方式分别为
    • 雷电 : ldopengl
    • MuMu : nemu
CPU + 控制模式 / 模拟器MuMu
Amd Ryzen 9 9950x+adb4.9ms - 7.1ms 平均6.1ms
Amd Ryzen 9 9950x+nemu40us - 90us 平均50us
Amd Ryzen 9 9950x+scrcpy5.2ms - 5.9ms 平均5.5ms
Amd Ryzen 6 6800H+adb12.5ms - 14.7ms 平均13.3ms
Amd Ryzen 6 6800H+nemu43us - 87us 平均59us
Amd Ryzen 6 6800H+scrcpy5.6ms - 5.7ms 平均5.6ms
Intel Core i9-13900HX+adb11.9ms - 16.3ms 平均13ms
Intel Core i9-13900HX+nemu50us - 110us 平均70us
Intel Core i9-13900HX+scrcpy5.3ms - 6.4ms 平均5.6ms

数据更新速度测试

  1. 纯CPU更新数据

    CPU / 数据CostSkillNameObjPos
    Amd Ryzen 9 9950x3us - 9us
    平均6us
    1.8ms - 2.1ms
    平均2ms
    33.1ms - 39.5ms
    平均35ms
    Amd Ryzen 6 6800H6us - 28us
    平均15us
    3.1ms - 4.5ms
    平均3.5ms
    79.1ms - 91.0ms
    平均86ms
    Intel Core i5-9300H6us - 53us
    平均20us
    10.4ms - 14.9ms
    平均12.5ms
    159.3ms - 222.5ms
    平均175.5ms
    Intel Core i9-13900HX3us - 19us
    平均7us
    1.9ms - 5.4ms
    平均3.6ms
    70.1ms - 107.0ms
    平均90ms
    CPU / 数据BossHealthSkillCostAcc
    Amd Ryzen 9 9950x5.0ms - 9.0ms
    平均7.8ms
    1.6ms - 3ms
    平均1.8ms
    6us - 24us
    平均13us
    Amd Ryzen 6 6800H35.0ms - 50.0ms
    平均39ms
    6.4ms - 8.4ms
    平均7.5ms
    14us - 21us
    平均20us
    Intel Core i5-9300H52.0ms - 79.3ms
    平均65ms
    16.0ms - 22.7ms
    平均20.2ms
    25us - 36us
    平均30us
    Intel Core i9-13900HX31.4ms - 90.1ms
    平均60ms
    5.1ms - 18.9ms
    平均8.5ms
    13us - 23us
    平均16us
    • note: SkillCost指更新一个槽技能的时间, 三个槽时间需要 x 3
    CPU / 数据Auto
    Amd Ryzen 9 9950x9us - 24us
    平均16us
    Amd Ryzen 6 6800H16us - 22us
    平均20us
    Intel Core i5-9300H27us - 43us
    平均30us
    Intel Core i9-13900HX13us - 23us
    平均19us
  2. YOLO模型推理 : CUDA加速纯CPU速度对比

设备 / 数据ObjPos
RTX 50909.1ms - 11.0ms
平均10.1ms
Amd Ryzen 9 9950x33.1ms - 39.5ms
平均35ms
Amd Ryzen 6 6800H79.1ms - 91.0ms
平均86ms
Intel Core i5-9300H159.3ms - 222.5ms
平均175.5ms
Intel Core i9-13900HX70.1ms - 107.0ms
平均90ms

基本配置

下图为一个轴文件的基本配置, 规定了一下自动战斗的基本信息

  1. Boss血量的文字识别配置
  2. YOLO目标检测配置
  3. 出场角色以及所有出现的技能配置

你可以在本文找到这些配置的含义, 一般来说, 你只需要设置 3 所相关的内容, 也就是formation 字段下的内容

json
{
  "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:
    1. 这些名称直接决定了yolo模型检测的角色列表
    2. resource/yolo_models/data.yamlnames 列举了所有可以被yolo模型识别的角色列表, 同时这也是BAAS YOLO模型的训练配置 formation_names.png
    3. 目前可识别的角色还较少, 随着数据集的逐渐扩充, 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:
  1. BAAS自动战斗在检测技能时, 只会检测该配置列出的技能
  2. 你可以在/resource/images/CN/zh-cn/skill/active查询已被录入的技能, 这个列表的技能名与该文件夹下的图片名一一对应 auto_fight_skill_templates

/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.onnxfp32模型
      best_fp16.onnxfp16模型

状态(State)

states

  1. 所有状态都在states中定义, 它是一个字典, 每个键值对表示一个状态, 键表示状态名, 值表示状态的参数

start_state

  1. 必须在自动战斗流程文件中被定义, 指示自动战斗开始时的进入的状态
  2. 它的值是一个字符串, 和states中的一个状态名相等

example:

  1. 下图在states中定义了三个状态, 状态一 / 状态二 / 状态三, 起始状态为状态一
json
{
  "state_state": "状态一",
  
  "states": {
    
    "状态一": {

    },
    
    "状态二": {
      
    },
    
    "状态三": {
      
    }
  }  
}

单个state的参数

states 中的例子列举了三个状态, 但是他们并没有任何实际内容, 我们需要在单个状态中设置以下参数以赋予状态意义

  1. action
  2. action_fail_transition
  3. transitions
  4. default_transition

note: 以上个参数都是可选的, 你可以根据需要自行选择

action

  • description: 到达这个状态后立即执行的行为(如技能释放, 开启auto/倍速, 重开战斗, 尝试跳过转阶段动画等)
  • type: string
  • constrains: 指定的动作必须在actions中被定义

action_fail_transition

  • description: 当action执行失败时(如未跳过转阶段动画时), 自动战斗会转移到这个状态
  • type: string
  • constrains: 指定的状态必须在states中被定义

transitions

default_transition

  1. transitions中的所有条件都不被满足时(或transitions没有任何条件), 默认转移状态

一个状态转移

每个状态转移转移表示在某个条件成立时转移到下一个状态, 我们需要指定条件下一个状态, 分别对应以下参数

  1. condition
    • 状态转移的条件名, 这个条件必须在conditions中被定义
    • 它的值是一个字符串, 必须在conditions中被定义
  2. next
    • 下一个状态名, 这个状态必须在states中被定义
    • 它的值是一个字符串, 必须在states中被定义

示例

example:

  1. 当自动战斗转移到这个状态时, 会立即执行释放技能一
  2. 如果释放技能失败, 转移到重新开始战斗
  3. 如果释放技能成功, 并且boss血量小于500w, 转移到释放技能二
  4. 否则转移到重新开始战斗
json
{
  "状态一": {
    "action": "释放技能一",
    "action_fail_transition": "重新开始战斗",
    "transitions": [
      {
        "condition": "boss血量小于500w",
        "next": "释放技能二"
      }
    ],
    "default_transition": "重新开始战斗"
  }
}

note:这个例子中的action 以及 condition 都还未被定义, 你需要在actionsconditions中学习如何定义这些动作和条件

注意事项

  1. 初始状态start_state必须被定义
  2. 结束条件: 自动战斗会在没有任何可转移状态时退出循环, 可能情况如下:
    • transitions中任何条件都不成立, 并且没有default_transition
    • transitions中没有条件, 并且没有default_transition
  3. 值得一提的是, 没有default_transition, 自动战斗也可以实现它的功能, 你只需要找到所有其他都不成立的条件, 并将其作为transition的最后一个条件也可以实现default_transition的功能

动作(Action)

actions

  1. 所有动作都在actions中定义, 它是一个字典, 每个键值对表示一个动作序列, 键表示动作名, 值是一个列表
  2. 注意再次强调, 每个action的值是一个列表, 这个列表中的每个元素表示一个动作, 你可以将它理解为一个动作序列, 这个动作序列会被依次执行

example:

json
{
  "actions": {
    "释放技能1": [
      {
        "desc": "释放技能1的第一个操作"
      },
      {
        "desc": "释放技能1的第二个操作"
      }
    ],
    "释放技能2": [
      {
        "desc": "释放技能2的第一个操作"
      }
    ]
  }
}

note: 设置单一action是一个动作列表有许多好处, 如下

  1. 允许你自由定义技能释放流程
    • 释放完第一个技能后你可以立刻释放第二个技能, 实现游戏中反手拐(在主c技能释放后释放辅助增伤技能)的效果
    • 释放技能前你可以选择调整游戏倍速为1
    • 释放技能后你可以选择调整游戏倍速回到3
  2. 简化了state的书写
    • state仅需指定action的名称, 而不需要重写所有action

单个action的参数

actions 中的例子列举了一些动作, 但是他们并没有任何实际内容, 我们需要在单个动作中设置以下参数以赋予状态意义

  1. 首先你需要通过t字段指定action的类型, 合法的t如下, 接着你需要根据t的值设置额外参数

条件(Condition)

条件类(Condition Class)

我们希望可监测的数据满足一些条件时进行自动化操作, 条件类让我们能用一种规范的语言表达我们所需要的条件

  1. 为了持续监测条件
    • 在循环中依次执行 截图 --> 数据更新 --> 条件判断
    • 数据更新前, 遍历每个condition, 通过标志位指示需要被更新的数据
    • 数据更新前, 首先判断界面是否为战斗中, 非战斗中则重新截图
    • 数据更新前, 等待正在更新数据线程数为0 (防止同一数据更新同时进行)
    • 由上一条可得, 如果一条数据不被当前任意一个condition所需要, 则不更新会该数据
    • 当某一条件成立 / 所有条件不成立时结束判断
  2. 单一状态信息更新后紧接着进行条件判断
    • 条件判断的耗时远低于数据更新, 如果有多条数据同时在更新, 其中一条数据更新完毕后立即进行条件判断
  3. 根据可监测的数据中任意一个进行比较的条件称为原始条件 (primitive condition)
  4. 条件可根据or / and进行自由组合, 称为组合条件
  5. 每个条件包含timeout字段
    • 条件判断开始后时限内期望条件未被判断为(未)成立则认为该条件不成立
    • 设计timeout的一大原因是可以有效避免条件判断陷入死循环
  6. 条件类仅在加载自动战斗工作流时初始化, 条件类可被重复使用, 每次使用condition前刷新上一次使用的数据
  7. desc字段作为这个条件的描述, 方便理解, 并不会对条件判断产生任何影响

条件判断 (Condition Judgement)

按照下图的架构实现自动战斗的条件判断, 该架构设计参照条件类所需的特性 condition_judgement.png

conditions

  1. 所有条件都在conditions中定义, 它是一个字典, 每个键值对表示一个条件, 键表示条件名, 值是一个字典

example:

json
{
  "conditions": {
    
    "条件1": {

    },
    
    "条件2": {

    }
  }
}

重开条件(Restart Condition)

重开是凹分必不可少的环节之一, 列举以下重开条件以供参考

BOSS血量范围

  • description: 检查BOSS血量是否在某一范围
  • checkpoint:
    1. 战斗剩余时间达到某值
    2. 释放技能后x秒
  • usage:
    1. 技能未暴击
    2. 其他异常 (学生退场 / 寿司开盾减伤 / 黑白转阶段)

BOSS血量减少

  • description: 检查BOSS血量是否在某一时间段内下降期望值
  • checkpoint:
    1. 战斗剩余时间达到某值计时x秒
    2. 释放技能后计时x秒

技能槽

  • description:检查学生技能是否出现在技能槽
  • checkpoint:
    1. 战斗剩余时间达到某值计时x秒
    2. 释放技能后计时x秒
  • usage:
    1. 检查初始技能顺序是否正确
    2. 学生退场 --> 技能排序变化
    3. auto异常释放技能

技能Cost

  • description:检查技能Cost是否为指定值
  • checkpoint:
    1. 战斗剩余时间达到某值计时x秒
    2. 释放技能后计时x秒
  • usage:
    1. 检查忧, 枫香(新年) 等减费角色的技能是否释放到期望目标

字段含义
0开启auto释放 (确保auto被选中)
1自定义点击顺序
2保证槽技能被选中-->释放

使用前须知

必要的游戏内设置

  1. 必须关闭游戏内释放技能动画

轴的基本信息

name (轴名称)

  • description: 轴的名称
  • type: string
  • example: 特殊委托关卡L, 使用女仆爱丽丝的通关轴
json
{
  "name": "Special-Task-L Aris Maid Workflow"
}

formation (配队信息)

完整组合

json
{
  "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::mutexdescription: 用于线程同步的互斥量

d_updater_running_thread_count

type: std::atomic<int>description: 当前正在运行的数据更新线程数量

d_updater_thread_finish_notifier

type: std::condition_variabledescription: 用于通知数据更新线程结束的条件变量

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_ddescription: 自动战斗的共享数据, 用于在 action / condition / updaters / state 之间传递数据

_cond_type

type: std::stringdescription: 目前正在录入的条件的类型名称

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:

  1. 请注意条件名条件类型名是完全不同的概念

_cond_is_matched_recorder

type: std::vector<bool>description: 记录条件是否成立, 已被判断成立 / 不成立的条件不会参与以下内容

  1. 超时检查
  2. 条件成立判断

note:

  1. 长度与all_cond相同

_cond_checked

type: std::vector<bool>description: 条件的 超时监测 / 重置状态 / 成立判断 是递归进行的, 记录每一项检查过的条件, 避免无限递归 note:

  1. 长度与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_tdescription: 当前状态的索引

_state_cond_j_start_t

type: long longdescription: 状态转移 条件判断循环的开始时间(ms)

_state_cond_j_loop_start_t

type: long longdescription: 状态转移 条件判断循环每一轮循环的开始时间(ms)

_state_cond_j_elapsed_t

type: long longdescription: 状态转移 条件判断循环的总耗时(ms)

_state_trans_cond_matched_idx

type: std::optional<uint64_t>description: 第一个状态转移条件成立的转移索引

_state_flg_all_trans_cond_dissatisfied

type: booldescription: 指示是否所有状态转移条件都不成立

_state_cond_j_loop_running_flg

type: booldescription: 指示条件判断循环是否正在运行

start_state_name

type: std::stringdescription: 初始状态的名称

workflow_path

type: std::filesystem::pathdescription: 轴文件的路径

workflow_name

type: std::stringdescription: 轴的名称

baas

type: BAAS*description: BAAS实例