Python Apex Legends 武器自动识别与压枪 全过程记录

Posted mrathena

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python Apex Legends 武器自动识别与压枪 全过程记录相关的知识,希望对你有一定的参考价值。

博文目录

文章目录


本文为下面参考文章的学习与实践

[原文] FPS游戏自动枪械识别+压枪(以PUBG为例)
[转载] FPS游戏自动枪械识别+压枪(以PUBG为例)

环境准备

Python Windows 开发环境搭建

conda create -n apex python=3.9

操纵键鼠

由于绝地求生屏蔽了硬件驱动外的其他鼠标输入,因此我们无法直接通过py脚本来控制游戏内鼠标操作。为了实现游戏内的鼠标下移,我使用了罗技鼠标的驱动(lgs),而py通过调用ghub的链接库文件,将指令操作传递给ghub,最终实现使用硬件驱动的鼠标指令输入给游戏,从而绕过游戏的鼠标输入限制。值得一提的是,我们只是通过py代码调用链接库的接口将指令传递给罗技驱动的,跟实际使用的是何种鼠标没有关系,所以即便用户使用的是雷蛇、卓威、双飞燕等鼠标,对下面的代码并无任何影响。

驱动安装 链接库加载 代码准备和游戏外测试

罗技驱动分 LGS (老) 和 GHub (新), 必须装指定版本的 LGS 驱动(如已安装 GHub 可能需要卸载), 不然要么报未安装, 要么初始化成功但调用无效

网盘下载 LGS_9.02.65_x64_Logitech.exe

网盘下载 mouse.device.lgs.dll

try:
    driver = ctypes.CDLL(r'mouse.device.lgs.dll')  # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\\'
    ok = driver.device_open() == 1
    if not ok:
        print('初始化罗技驱动失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
    print('初始化罗技驱动失败, 缺少文件')

装了该驱动后, 无需重启电脑, 当下就生效了. 遗憾的是, 这个 dll 文件里面的方法都没有对应的文档, 只能猜测参数了

toolkit.py

import time
from ctypes import CDLL

import win32api  # conda install pywin32


try:
    driver = CDLL(r'mouse.device.lgs.dll')  # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\\'
    ok = driver.device_open() == 1
    if not ok:
        print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
    print('初始化失败, 缺少文件')


class Mouse:

    @staticmethod
    def move(x, y, absolute=False):
        if ok:
            mx, my = x, y
            if absolute:
                ox, oy = win32api.GetCursorPos()
                mx = x - ox
                my = y - oy
            driver.moveR(mx, my, True)

    @staticmethod
    def down(code):
        if ok:
            driver.mouse_down(code)

    @staticmethod
    def up(code):
        if ok:
            driver.mouse_up(code)

    @staticmethod
    def click(code):
        """
        :param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
        :return:
        """
        if ok:
            driver.mouse_down(code)
            driver.mouse_up(code)


class Keyboard:

    @staticmethod
    def press(code):
        if ok:
            driver.key_down(code)

    @staticmethod
    def release(code):
        if ok:
            driver.key_up(code)

    @staticmethod
    def click(code):
        """
        :param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
        :return:
        """
        if ok:
            driver.key_down(code)
            driver.key_up(code)

游戏内测试

在游戏里面试过后, 管用, 但是不准, 猜测可能和游戏内鼠标灵敏度/FOV等有关系

from toolkit import Mouse
import pynput  # conda install pynput

def onClick(x, y, button, pressed):
    if not pressed:
        if pynput.mouse.Button.x2 == button:
            Mouse.move(100, 100)


mouseListener = pynput.mouse.Listener(on_click=onClick)
mouseListener.start()
mouseListener.join()

键鼠监听

Pynput 说明

def onClick(x, y, button, pressed):
    print(f'button button "pressed" if pressed else "released" at (x,y)')
    if pynput.mouse.Button.left == button:
        return False  # 正常不要返回False, 这样会结束监听并停止监听线程, 在关闭程序前返回False就好了
listener = pynput.mouse.Listener(on_click=onClick)
listener.start()


def onRelease(key):
    print(f'key released')
    if key == pynput.keyboard.Key.end:
        return False  # 正常不要返回False, 这样会结束监听并停止监听线程, 在关闭程序前返回False就好了
listener = pynput.keyboard.Listener(on_release=onRelease)
listener.start()

注意调试回调方法的时候, 不要打断点, 不要打断点, 不要打断点, 这样会卡死IO, 导致鼠标键盘失效

回调方法如果返回 False, 监听线程就会自动结束, 所以不要随便返回 False

键盘的特殊按键采用 keyboard.Key.tab 这种写法,普通按键用 keyboard.KeyCode.from_char('c') 这种写法, 有些键不知道该怎么写, 可以 print(key) 查看信息

另外,钩子函数本身是阻塞的。也就是说钩子函数在执行的过程中,用户正常的键盘/鼠标操作是无法输入的。所以在钩子函数里面必须写成有限的操作(即O(1)时间复杂度的代码),也就是说像背包内配件及枪械识别,还有下文会讲到的鼠标压枪这类时间开销比较大或者持续时间长的操作,都不适合写在钩子函数里面。这也解释了为什么在检测到Tab(打开背包)、鼠标左键按下时,为什么只是改变信号量,然后把这些任务丢给别的进程去做的原因。

武器识别

如何判断是否在游戏内

先是判断游戏窗体是否在最前端, 然后判断游戏内是否正在持枪界面

找几个特征点取色判断, 如血条左上角和生存物品框左下角

一般能用于取色的点, 它的颜色RGB都是相同的, 这种点的颜色非常稳定, 不会受不同背景色的影响

我原本以为屏幕点取色应该不会超过1ms的耗时, 结果万万没想到, 取个色居然要1-10ms, 效率奇低, 暂无其他优雅方法

如何判断背包状态 无武器/1号武器/2号武器


黄圈内的武器面板, 可以分为两个部分, 上边是边框, 下边是名字, 上边的边框又可以分为上半部分a和下半部分b

看武器边框上红色圈住的部分颜色, a为灰色说明没有武器, ab不同色说明使用2号武器, ab同色说明使用1号武器

如何判断武器子弹类别 轻型/重型/能量/狙击/霰弹/空投

因为不同子弹类型的武器, 边框颜色不一样. 所以可以和上面的检测放在一起, 同一个点直接判断出背包状态和子弹类别

如何判断武器名称 在确定当前使用的背包的基础上判断

在根据子弹类型分类后的基础上, 通过 背包状态 确定要检查颜色的位置(1号位/2号位), 通过 武器子弹类别 缩小判断范围, 在每个武器的名字上找一个纯白色的点, 确保这个点只有这把武器是纯白色, 然后逐个对比, 判断当前持有的武器和哪个点对上了, 那就说明是哪把武器

先从名字最长的武器开始找点, 从最左边或最右边找, 即可确保该点一定不在别的武器上或在别的武器上该点不是指定的颜色

如何判断武器模式 全自动/连发/单发


需要压枪的只有全自动和半自动两种模式的武器, 单发不需要压枪(想把单发武器做成自动连发), 喷子和狙不需要压枪

所以需要找一个能区分三种模式的点(不同模式这个点的颜色不同但是稳定), 且这个点不能受和平和三重的特殊标记影响

一开始找了个不是纯白色的点, 后来发现这个点的颜色会被背景颜色影响到, 不是恒定不变的. 最终还是放弃了一个点即可分辨模式的想法, 采用了稳妥的纯白色点, 保证该点只在该模式下是纯白色的, 在其他模式都不是纯白色即可

如何判断是否持有武器

暂无法判断, 收起武器和持有武器, 没有能明确分辨两种情况的固定点

部分武器可以通过[V]标判断, 因为不全, 先不采用

也可以通过监听按按[3]键(收起武器操作)来设置标记, 其他操作去除标记, 然后通过读取该标记判断是否持有武器, 但不优雅, 先不采用

目前已有的一个特征是, 使用拳头时, 准星是一个大号方形准星, 使用武器时, 都是圆准星. 但是使用拳头不等于未持有武器

如何判断弹夹是否打空


弹夹中子弹数大多为两位数(LSTAR可能为三位数), 所以只需确认十位不为0, 即可认为不空, 十位为0且个位为0, 即可认为空

  • 十位的点, 在数字正中间即可, 1-9都是纯白色, 0是灰色. 注意, 这个灰色不是定色, 该颜色会随着背景改变而改变
  • 个位的点, 在数字0中间斜线的最左端, 这个点是纯白色, 且其他1-9时, 这个点都不是纯白色

何时触发识别

  • 鼠标右键(瞄准模式) 按下, 识别武器. 和游戏内原本的按键功能不冲突
  • 1(1号武器) / 2(2号武器) / 3(收起武器) / E(交互/换枪) / V(切换射击模式) / R(更换弹夹) / Tab(打开背包) / Esc(关闭各种窗口) / Alt(求生物品) 键释放, 识别武器
  • Home 键释放 / 鼠标侧下键按下, 切换开关
  • end 键释放, 结束程序

几个细节点

  • 通过测试发现, 所有武器的发射间隔都大于50毫秒, 所以压枪时, 这50毫秒内可以做一些操作, 比如判断弹夹是否打空, 避免触发压枪

压枪思路

apex 的压枪有3个思路, 因为 apex 不同武器的弹道貌似是固定的, 没有随机值?, 其他游戏也是??

  • 左右抖动抵消水平后坐力, 下拉抵消垂直后坐力. 这种方法简单, 但是画面会抖动, 效果也不是很好
  • 根据武器配件等测试不同情况下的武器后坐力数据, 然后做反向抵消.
    • 可以通过取巧的方式, 只做无配件状态下的反向抵消, 还省了找配件的麻烦. 这种方法太难太麻烦了, 但是做的好的话, 基本一条线, 强的离谱
  • 还有就是现在很火的AI目标检测(yolov5), 我也有尝试做, 环境搭好了, 但是中途卡住了
    • 一是毕竟python是兴趣, 很多基础不到位, 相关专业知识更是空白, 参考内容也参差不齐, 导致对检测和训练的参数都很模糊
    • 二是数据集采集, 网上找了些, 自己做了些, 但是任然只有一点点, 不着急, 慢慢找吧. 据说要想效果好, 得几千张图片集 …

组织数据

  • 武器数据, 通过子弹类型分组, 组里的每个成员指定序号, 名称, 抖枪参数, 压枪参数等信息
  • 配置数据, 按分辨率分组, 再按是否在游戏中, 是否有武器, 武器位置, 武器子弹类型, 武器索引等信息将配置分类
  • 信号数据, 程序运行时, 进程线程间通讯

具体看 cfg.py 中的 detect, weapon 和 apex.py 中的 init

开发过程

第一阶段实现 能自动识别出所有武器

找对点就行了

第二阶段实现 能自动采用对应抖枪参数执行压枪

  • 游戏内鼠标灵敏度越高, 越容易抖枪且效果更好, 但是开到5的话, 会感到有点晕
  • 游戏内鼠标灵敏度越高, 代码里抖动的像素就需要设置的更小, 比如5的灵敏度, 抖动2像素就可以了
  • 抖枪能减小后坐力, 但不能完全消除, 所以还需配合对应方向的移动
  • 后坐力越大的武器, 前几枪容易跳太高, 下压力度可以大点

能量武器, 专注和哈沃克, 预热和涡轮有很大影响, 这里暂时没管, 在第三阶段实现

第三阶段实现 能自动采用对应压枪参数执行压枪


我的游戏内鼠标设置是这样的, 要确保每个瞄镜的ADS都是一样的, 鼠标DPI是3200

最终的效果是, 20米前一半子弹比较稳, 30米将就, 50米不太行, 有几率一梭子打倒, 差不多够用, 就没再认真调了

如何调压枪参数

我觉得调参数最重要的一点, 就是先算出正确的子弹射速(平均每发子弹耗时), 如果用了错误的数据, 那很可能调了半天白费功夫

测试方法我总结了下, 首先, 每发子弹耗时通常都是50到150毫秒, 先假设是100, 看有多少发子弹, 就复制多少条压枪数据, 举例

R-301 这把枪, 加上金扩容, 28发子弹, 那就先准备下面的初始数据, 三个参数分别是, 鼠标水平移动的值/垂直移动的值/移动后休眠时间, 当然也可以有其他的参数

先把对应最后一发子弹的鼠标移动值设置为10000, 看是否打完子弹时, 鼠标正好产生大幅位移, 然后调后面的100, 直到恰好匹配, 然后就可以开始调鼠标参数了

[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],  #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],  #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],  #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],  #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],  #
[0, 0, 100],
[0, 0, 100],
[10000, 0, 100],

调鼠标参数时, 要从上往下逐个调, 因为上面的一个变动, 对下面的影响非常大, 很可能导致下面的白调了

比如调纵向压制的时候, 1倍镜30米瞄这这道杠打, 争取基本全都在杠上, 纵向就ok了, 横向同理

也可以借助录像工具, 录制屏幕中心部分区域, 然后以0.1倍速播放, 仔细查看压制力度是否合适

最终的效果就是, 不太稳定, 123倍镜表现不太一致, 3倍镜偏差最大. 难不成各个镜子做一套参数?

游戏中实测

压枪参数大多都是随便调了下, 并不是很精细, 20米内的表现还行吧, 当然距离一条线还差得远. 专注轻机枪因为射速不固定所以没调

存在的问题

  • 采用取色判断法, 单点取色耗时1-10ms, 性能不足 (已有优化思路, 一种是, 通过GetCurrentObject和GetObject获取和hdc相关的位图对象数据区起始地址, 拿到BitMap, 直接取对应坐标的颜色. 受限于个人水平, 暂无法用Python实现)
  • 检测武器名称使用的是O(n)时间复杂度的遍历方式, 在取色判断法效率低的情况下, 性能不够优秀和稳定, 期望做到O(1)
  • 暂无法判断是否持有武器(有武器但我用拳头, 可能引起错误地触发压枪)
  • 暂无法实现按着左键时模拟左键点击效果, 所以暂无法实现单发枪变连发枪的功能

工程源码

也可以直接拷贝下方代码. 因条件限制, 只适配了 3440*1440 分辨率的游戏数据, 其他分辨率需自行适配

游戏偶尔会调整武器射速, 发现数据不对劲时, 可能需要重新调整压枪参数

GitHub python.apex.legends.weapon.auto.recognize.and.suppress

网盘下载 LGS_9.02.65_x64_Logitech.exe

网盘下载 mouse.device.lgs.dll

cfg.py

mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack'
color = 'color'
point = 'point'
index = 'index'
shake = 'shake'
speed = 'speed'
count = 'count'
armed = 'armed'
empty = 'empty'
switch = 'switch'
bullet = 'bullet'  # 子弹
differ = 'differ'
turbo = 'turbo'
trigger = 'trigger'
restrain = 'restrain'
strength = 'strength'
positive = 'positive'  # 肯定的
negative = 'negative'  # 否定的

# 检测数据
detect = 
    "3440:1440": 
        game: [  # 判断是否在游戏中
            
                point: (236, 1344),  # 点的坐标, 血条左上角
                color: 0x00FFFFFF  # 点的颜色, 255, 255, 255
            ,
            
                point: (2692, 1372),  # 生存物品右下角
                color: 0x959595  # 149, 149, 149
            
        ],
        pack:   # 背包状态, 有无武器, 选择的武器
            point: (2900, 1372),  # 两把武器时, 1号武器上面边框分界线的上半部分, y+1 就是1号武器上面边框分界线的下半部分
            color: 0x808080,  # 无武器时, 灰色, 128, 128, 128
            '0x447bb4': 1,  # 轻型弹药武器, 子弹类型: 1/2/3/4/5/6/None(无武器)
            '0x839b54': 2,  # 重型弹药武器
            '0x3da084': 3,  # 能量弹药武器
            '0xce5f6e': 4,  # 狙击弹药武器
            '0xf339b': 5,  # 霰弹枪弹药武器
            '0x5302ff': 6,  # 空投武器
        ,
        mode:   # 武器模式, 全自动/半自动/单发/其他
            color: 0x00FFFFFF,
            '1': (3151, 1347),  # 全自动
            '2': (3171, 1351),  # 半自动
        ,
        armed:   # 是否持有武器(比如有武器但用拳头就是未持有武器)

        ,
        empty:   # 是否空弹夹(武器里子弹数为0)
            color: 0x00FFFFFF,
            '1': (3204, 1306),  # 十位数, 该点白色即非0, 非0则一定不空
            '2': (3229, 1294),  # 个位数, 该点白色即为0, 十位为0且个位为0为空
        ,
        name:   # 武器名称判断
            color: 0x00FFFFFF,
            '1':   # 1号武器
                '1': [  # 轻型弹药武器
                    (2959, 1386),  # 1: RE-45 自动手枪
                    (2970, 1385),  # 2: 转换者冲锋枪
                    (2972, 1386),  # 3: R-301 卡宾枪
                    (2976, 1386),  # 4: R-99 冲锋枪
                    (2980, 1386),  # 5: P2020 手枪
                    (2980, 1384),  # 6: 喷火轻机枪
                    (2987, 1387),  # 7: G7 侦查枪
                    (3015, 1386),  # 8: CAR (轻型弹药)
                ],
                '2': [  # 重型弹药武器
                    (2957, 1385),  # 1: 赫姆洛克突击步枪
                    (2982, 1385),  # 2: 猎兽冲锋枪
                    (2990, 1393),  # 3: 平行步枪
                    (3004, 1386),  # 4: 30-30
                    (3015, 1386),  # 5: CAR (重型弹药)
                ],
                '3': [  # 能量弹药武器
                    (2955, 1386),  # 1: L-STAR 能量机枪
                    (2970, 1384),  # 2: 三重式狙击枪
                    (2981, 1385),  # 3: 电能冲锋枪
                    (2986, 1384),  # 4: 专注轻机枪
                    (2980, 1384),  # 5: 哈沃克步枪
                ],
                '4': [  # 狙击弹药武器
                    (2969, 1395),  # 1: 哨兵狙击步枪
                    (2999, 1382),  # 2: 充能步枪
                    (2992, 1385),  # 3: 辅助手枪
                    (3016, 1383),  # 4: 长弓
                ],
                '5': [  # 霰弹枪弹药武器
                    (2957, 1384),  # 1: 和平捍卫者霰弹枪
                    (2995, 1382),  # 2: 莫桑比克
                    (3005, 1386),  # 3: EVA-8
                ],
                '6': [  # 空投武器
                    (2958, 1384),  # 1: 克雷贝尔狙击枪
                    (2959, 1384),  # 2: 手感卓越的刀刃
                    (2983, 1384),  # 3: 敖犬霰弹枪
                    (3003, 1383),  # 4: 波塞克
                    (3014, 1383),  # 5: 暴走
                ]
            ,
            '2': 
                differ: 195  # 直接用1的坐标, 横坐标右移195就可以了
            
        ,
        turbo:   # 涡轮
            color: 0x00FFFFFF,
            '3': 
                differ: 2,  # 有涡轮和没涡轮的索引偏移
                '4': (3072, 1358),  # 专注轻机枪 涡轮检测位置
                '5': (3034, 1358),  # 哈沃克步枪 涡轮检测位置
            
        ,
        trigger:   # 双发扳机
            color: 0x00FFFFFF,
            '1': 
                differ: 2,
                '7': (3072, 1358),  # G7 侦查枪 双发扳机检测位置
            ,
            '5': 
                differ: 1,
                '3': (3034, 以上是关于Python Apex Legends 武器自动识别与压枪 全过程记录的主要内容,如果未能解决你的问题,请参考以下文章

APEX_WEB_SERVICE.MAKE_REST_REQUEST 导致 ORA-29248:使用无法识别的 WRL 打开钱包

apex官网周边能送到中国吗

基于图像识别的指挥控制系统集成测试自动化平台

对于League of Legends的分析

敏捷开发是拯救业务人员的终极秘密武器?!

markdown Apex测试的独立自动编号