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

博文目录

文章目录

  • 环境准备
    • 操纵键鼠
      • 驱动安装 链接库加载 代码准备和游戏外测试
        • toolkit.py
      • 游戏内测试
    • 键鼠监听
    • 武器识别
      • 如何简单且高效判断是否在游戏内
      • 如何简单且高效判断背包状态 无武器/1号武器/2号武器
      • 如何简单且高效判断武器子弹类别
      • 如何简单且高效判断武器名称
      • 如何简单且高效判断武器模式 全自动/连发/单发
      • 何时触发识别
    • 压枪思路
    • 组织数据
  • 第一阶段实现 能自动识别出所有武器
    • cfg.py
    • toolkit.py
    • apex.py
  • 第二阶段实现 能自动识别出所有武器并采用对应压枪参数执行压枪
  • 第三阶段实现 放弃抖枪术 转常规后座抵消法


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

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

环境准备

Python Windows 开发环境搭建

conda create -n apex python=3.9

操纵键鼠

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

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

罗技驱动使用 LGS_9.02.65_X64(请自行找资源安装,官网新版罗技驱动没找到对应的链接库文件),链接库文件在项目链接里面可以找到。下面是载入链接库的代码。

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

LGS_9.02.65_x64_Logitech.exe, 网盘下载
其他地址1
其他地址2

try:
    gm = CDLL(r'./ghub_device.dll')
    gmok = gm.device_open() == 1
    if not gmok:
        print('未安装ghub或者lgs驱动!!!')
    else:
        print('初始化成功!')
except FileNotFoundError:
    print('缺少文件')

装了该驱动后, 无需重启电脑, 当下就生效了. 遗憾的是, 没有对应的文档, 只能猜测参数了

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,其他可使用的库还有Pyhook3

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()

Listener中绑定on_press和on_release的函数( on_key_press、on_key_release),它们返回False的时候是结束监听,下文鼠标监听的函数同理,所以不要随便返回False

键盘的特殊按键采用keyboard.Key.tab这种写法,普通按键用keyboard.KeyCode.from_char(‘c’)这种写法

这里有一点非常坑,on_press和on_release的参数只能有一个key,这个key就是对应键盘按下的哪颗按键。但这是不足以满足我们的需求的,因为我们应该在钩子函数内部,在按下指定按键时对信号量做出修改,但因为参数的限制,我们无法把信号量传进函数内部,这里我也是想了很久,最后才想到用嵌套函数的写法解决这个问题。

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

武器识别

如何简单且高效判断是否在游戏内

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

一般能用于取色的点, 它的颜色RGB都是相同的, 这种点的颜色非常稳定

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

如何简单且高效判断背包状态 无武器/1号武器/2号武器

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

如何简单且高效判断武器子弹类别

可以和上面的放在一起, 同一个点直接判断出背包状态和武器子弹类别

如何简单且高效判断武器名称

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

如何简单且高效判断武器模式 全自动/连发/单发


需要压枪的只有全自动和半自动两种模式的武器, 单发不需要压枪(后面有可能做自动单发, 到时候在考虑), 喷子和狙不需要压枪

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

收起武器, 部分武器可以通过[V]标判断, 放弃

何时触发识别

键盘 1/2/3/E/V 释放, 鼠标 右键 按下, 这个如果不影响开枪就这个了, 影响的话就改成侧下键. 键位和键在游戏内的功能不冲突的

压枪思路

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

  • 左右抖动抵消水平后坐力, 下拉抵消垂直后坐力. 这种方法简单, 但是画面会抖动, 效果也不是很好
  • 根据武器配件等测试不同情况下的武器后坐力数据, 然后做反向抵消.
    可以通过取巧的方式, 只做无配件状态下的反向抵消, 还省了找配件的麻烦
    这种方法太难太麻烦了, 但是做的好的话, 基本一条线, 强的离谱

我先试试 抖枪大法

组织数据

武器数据, 通过子弹类型分组, 组里的每个成员指定序号, 名称, 压枪参数等信息

配置数据, 按分辨率分组, 再按是否在游戏中, 是否有武器, 武器位置, 武器子弹类型, 武器索引等信息分类

信号数据, 程序运行时, 进程线程间通讯

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

目前测试下来, 一波识别大概六七十毫秒的样子, 最多也不超过一百毫秒, 主要耗时在取色函数(1-10ms), 性能已经够用了

cfg.py


mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack'  # 背包
color = 'color'
point = 'point'
index = 'index'
bullet = 'bullet'  # 子弹
differ = 'differ'
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: {  # 武器模式, 全自动/半自动/单发/其他
            point: (3148, 1349),
            '0xf8f8f8': 1,  # 全自动
            '0xfefefe': 2  # 半自动
        },
        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: 克雷贝尔狙击枪
                    (2983, 1384),  # 2: 敖犬霰弹枪
                    (3003, 1383),  # 3: 波塞克
                    (3014, 1383),  # 4: 暴走
                ]
            },
            '2': {
                differ: 195
            }
        }
    },
    "2560:1440": {

    },
    "2560:1080": {

    },
    "1920:1080": {

    }
}

# 武器数据
weapon = {
    '1': {  # 轻型弹药武器
        '1': {
            name: 'RE-45 自动手枪',
        },
        '2': {
            name: '转换者冲锋枪',
        },
        '3': {
            name: 'R-301 卡宾枪',
        },
        '4': {
            name: 'R-99 冲锋枪',
        },
        '5': {
            name: 'P2020 手枪',
        },
        '6': {
            name: '喷火轻机枪',
        },
        '7': {
            name: 'G7 侦查枪',
        },
        '8': {
            name: 'CAR (轻型弹药)',
        }
    },
    '2': {  # 重型弹药武器
        '1': {
            name: '赫姆洛克突击步枪',
        },
        '2': {
            name: '猎兽冲锋枪',
        },
        '3': {
            name: '平行步枪',
        },
        '4': {
            name: '30-30',
        },
        '5': {
            name: 'CAR (重型弹药)',
        }
    },
    '3': {  # 能量弹药武器
        '1': {
            name: 'L-STAR能量机枪',
        },
        '2': {
            name: '三重式狙击枪',
        },
        '3': {
            name: '电能冲锋枪',
        },
        '4': {
            name: '专注轻机枪',
        },
        '5': {
            name: '哈沃克步枪',
        },
    },
    '4': {  # 狙击弹药武器
        '1': {
            name: '哨兵狙击步枪',
        },
        '2': {
            name: '充能步枪',
        },
        '3': {
            name: '辅助手枪',
        },
        '4': {
            name: '长弓',
        },
    },
    '5': {  # 霰弹弹药武器
        '1': {
            name: '和平捍卫者霰弹枪',
        },
        '2': {
            name: '莫桑比克',
        },
        '3': {
            name: 'EVA-8',
        },
    },
    '6': {  # 空投武器
        '1': {
            name: '克雷贝尔狙击枪',
        },
        '2': {
            name: '敖犬霰弹枪',
        },
        '3': {
            name: '波塞克',
        },
        '4': {
            name: '暴走',
        },
    }
}

toolkit.py

import mss  # pip install mss
import ctypes

from ctypes import CDLL

import cfg
from cfg import detect, weapon

# 全局 dll
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
hdc = user32.GetDC(None)

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 point():
        return user32.GetCursorPos()

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

    @staticmethod
    def moveHumanoid(x, y, absolute=False):
        """
        仿真移动(还没做好)
        """
        if ok:
            ox, oy = user32.GetCursorPos()  # 原鼠标位置
            mx, my = x, y  # 相对移动距离
            if absolute:
                mx = x - ox
                my = y - oy
            tx, ty = ox + mx, oy + my
            print(f'({ox},{oy}), ({tx},{ty}), x:{mx},y:{my}')
            # 以绝对位置方式移动(防止相对位置丢失精度)
            adx, ady = abs(mx), abs(my)
            if adx <= ady:
                # 水平方向移动的距离短
                for i in range(1, adx):
                    ix = i if mx > 0 else -i
                    temp = int(ady / adx * abs(ix))
                    iy = temp if my > 0 else -temp
                    Mouse.move(ox + ix, oy + iy, absolute=True)
                    # time.sleep(0.001)
            else:
                # 垂直方向移动的距离短
                for i in range(1, ady):
                    iy = i if my > 0 else -i
                    temp = int(adx / ady * abs(iy))
                    ix = temp if mx > 0 else -temp
                    Mouse.move(ox + ix, oy + iy, absolute=True)
                    # time.sleep(0.001)

    @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)


class Monitor:
    """
    显示器
    """
    sct = mss.mss()

    @staticmethod
    def grab(region):
        """
        region: tuple, (left, top, width, height)
        pip install mss
        """
        left, top, width, height = region
        return Monitor.sct.grab(monitor={'left': left, 'top': top, 'width': width, 'height': height})

    @staticmethod
    def pixel(x, y):
        """
        效率很低且不稳定, 单点检测都要耗时1-10ms
        获取颜色, COLORREF 格式, 0x00FFFFFF
        结果是int,
        可以通过 print(hex(color)) 查看十六进制值
        可以通过 print(color == 0x00FFFFFF) 进行颜色判断
        """
        # hdc = user32.GetDC(None)
        return gdi32.GetPixel(hdc, x, y)

    class Resolution:
        """
        分辨率
        """

        @staticmethod
        def display():
            """
            显示分辨率
            """
            w = user32.GetSystemMetrics(0)
            h = user32.GetSystemMetrics(1)
            return w, h

        @staticmethod
        def virtual():
            """
            多屏幕组合的虚拟显示器分辨率
            """
            w = user32.GetSystemMetrics(78)
            h = user32.GetSystemMetrics(79)
            return w, h

        @staticmethod
        def physical():
            """
            物理分辨率
            """
            # hdc = user32.GetDC(None)
            w = gdi32.GetDeviceCaps(hdc, 118)
            h = gdi32.GetDeviceCaps(hdc, 117)
            return w, h


class Game:
    """
    游戏工具
    """

    @staticmethod
    def game():
        """
        是否在游戏内
        太耗时了, 所以不能调的多了
        """
        w, h = Monitor.Resolution.display()
        data = detect.get(f'{w}:{h}').get(cfg.game)
        for item in data:
            x, y = item.get(cfg.point)
            if Monitor.pixel(x, y) != item.get(cfg.color):
                return False
        return True

    @staticmethod
    def index():
        """
        武器索引和子弹类型索引
        :return: 武器位索引, 1:1号位, 2:2号位, None:无武器, 拳头(这个暂时无法判断)
                 子弹类型索引, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投, None:无武器
        """
        w, h = Monitor.Resolution.display()
        data = detect.get(f'{w}:{h}').get(cfg.pack)
        x, y = data.get(cfg.point)
        color = Monitor.pixel(x, y)
        if data.get(cfg.color) == color:
            return None, None
        else:
            bullet = data.get(hex(color))
            return (1, bullet) if color == Monitor.pixel(x, y + 1) else (2, bullet)

    @staticmethod
    def weapon(index, bullet):
        """
        通过武器位和子弹类型识别武器, 参考:config.detect.name
        :param index: 武器位, 1:1号位, 2:2号位
        :param bullet: 子弹类型, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投
        :return:
        """
        w, h = Monitor.Resolution.display()
        data = detect.get(f'{w}:{h}').get(cfg.name)
        color = data.get(cfg.color)
        if index == 1:
            lst = data.get(str(index)).get(str(bullet))
            for i in range(len(lst)):
                x, y = lst[i]
                if color == Monitor.pixel(x, y):
                    return i + 1
        elif index == 2:
            differ = data.get(str(index)).get(cfg.differ)
            lst = data.get(str(1)).get(str(bullet))
            for i in range(len(lst)):
                x, y = lst[i]
                if color == Monitor.pixel(x + differ, y):
                    return i + 1
        return None

    @staticmethod
    def mode():
        """
        武器模式
        :return:  1:全自动, 2:半自动, None:其他
        """
        w, h = Monitor.Resolution.display()
        data = detect.get(f'{w}:{h}').get(cfg.mode)
        x, y = data.get(cfg.point)
        color = Monitor.pixel(x, y)
        return data.get(hex(color))

    @staticmethod
    def detect():
        """
        决策是否需要压枪, 向信号量写数据
        """
        if Game.game() is False:
            print('not in game')

            return
        index, bullet = Game.index()
        if (index is None) | (bullet is None):
            print('no weapon')

            return
        if Game.mode() is None:
            print('not in full auto or semi auto mode')

            return
        arms = Game.weapon(index, bullet)
        if arms is None:
            print('detect weapon failure')

            return
        # 检测通过, 需要压枪
        print(weapon.get(str(bullet)).get(str(arms)).get(cfg.name))
        return weapon.get(str(bullet)).get(str(arms)).get(cfg.name)

apex.py

import time

import pynput  # conda install pynput

import toolkit

ExitFlag = False


def down(x, y, button, pressed):
    global ExitFlag
    if ExitFlag:
        print(ExitFlag)
        return False  # 结束监听线程
    if pressed:  # 按下
        if pynput.mouse.Button.right == button:
            toolkit.Game.detect()


mouseListener = pynput.mouse.Listener(on_click=down)
mouseListener.start()


def release(key):
    if key == pynput.keyboard.Key.end:
        print('end')
        global ExitFlag
        ExitFlag = True
        return False
    if key == pynput.keyboard.KeyCode.from_char('1'):
        toolkit.Game.detect()
    elif key == pynput.keyboard.KeyCode.from_char('2'):
        toolkit.Game.detect()
    elif key == pynput.keyboard.KeyCode.from_char('3'):
        toolkit.Game.detect()
    elif key == pynput.keyboard.KeyCode.from_char('e'):
        toolkit.Game.detect()
    elif key == pynput.keyboard.KeyCode.from_char('v'):
        toolkit.Game.detect()


keyboardListener = pynput.keyboard.Listener(on_release=release)
keyboardListener.start()
keyboardListener.join()

Python Apex Legends 武器自动识别与压枪 全过程记录_第1张图片

第二阶段实现 能自动识别出所有武器并采用对应压枪参数执行压枪

第三阶段实现 放弃抖枪术 转常规后座抵消法


你可能感兴趣的:(python,python)