Python Apex Legends 武器自动识别与压枪 全过程记录
Posted mrathena
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python Apex Legends 武器自动识别与压枪 全过程记录相关的知识,希望对你有一定的参考价值。
文章目录
- 环境准备
- 第一阶段实现 能自动识别出所有武器
- 第二阶段实现 能自动识别出所有武器并采用对应抖枪参数执行压枪
- 第三阶段实现 放弃抖枪术 转常规后座抵消法 (进行中)
- 第四阶段实现 AI 目标检测, 移动鼠标, 彻底告别压枪
本文为下面参考文章的学习与实践
环境准备
conda create -n apex python=3.9
操纵键鼠
由于绝地求生屏蔽了硬件驱动外的其他鼠标输入,因此我们无法直接通过py脚本来控制游戏内鼠标操作。为了实现游戏内的鼠标下移,我使用了罗技鼠标的驱动(ghub),而py通过调用ghub的链接库文件,将指令操作传递给ghub,最终实现使用硬件驱动的鼠标指令输入给游戏,从而绕过游戏的鼠标输入限制。值得一提的是,我们只是通过py代码调用链接库的接口将指令传递给罗技驱动的,跟实际使用的是何种鼠标没有关系,所以即便用户使用的是雷蛇、卓威、双飞燕等鼠标,对下面的代码并无任何影响。
驱动安装 链接库加载 代码准备和游戏外测试
罗技驱动使用 LGS_9.02.65_X64(请自行找资源安装,官网新版罗技驱动没找到对应的链接库文件),链接库文件在项目链接里面可以找到。下面是载入链接库的代码。
罗技驱动分LGS(老)和GHub(新), 必须装指定版本的LGS驱动(如已安装GHub可能需要卸载), 不然要么报未安装, 要么初始化成功但调用无效
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
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, 导致鼠标键盘失效
Listener中绑定on_press和on_release的函数( on_key_press、on_key_release),它们返回False的时候是结束监听,下文鼠标监听的函数同理,所以不要随便返回False
键盘的特殊按键采用keyboard.Key.tab这种写法,普通按键用keyboard.KeyCode.from_char(‘c’)这种写法
有些键不知道该怎么写, 可以 print(key)
查看写法
这里有一点非常坑,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/Tab/Esc/Alt 键释放, 识别武器
- Home 键释放, 切换开关
- end 键释放, 结束程序
压枪思路
apex 的压枪有3个思路, 因为 apex 不同武器的弹道貌似是固定的, 没有随机值?, 其他游戏也是??
- 左右抖动抵消水平后坐力, 下拉抵消垂直后坐力. 这种方法简单, 但是画面会抖动, 效果也不是很好
- 根据武器配件等测试不同情况下的武器后坐力数据, 然后做反向抵消.
可以通过取巧的方式, 只做无配件状态下的反向抵消, 还省了找配件的麻烦
这种方法太难太麻烦了, 但是做的好的话, 基本一条线, 强的离谱 - 还有就是现在很火的AI目标检测(yolov5), 我也有尝试做, 环境搭好了, 但是中途卡住了. 一是毕竟python是兴趣, 很多基础不到位, 相关专业知识更是空白, 参考内容也参差不齐, 导致对检测和训练的参数都很模糊. 二是数据集采集, 网上找了些, 自己做了些, 但是任然只有一点点, 不着急, 慢慢找吧. 据说要想效果好, 得几千张图片集 …
我先试试 抖枪大法
组织数据
武器数据, 通过子弹类型分组, 组里的每个成员指定序号, 名称, 压枪参数等信息
配置数据, 按分辨率分组, 再按是否在游戏中, 是否有武器, 武器位置, 武器子弹类型, 武器索引等信息分类
信号数据, 程序运行时, 进程线程间通讯
第一阶段实现 能自动识别出所有武器
目前测试下来, 一波识别大概六七十毫秒的样子, 最多也不会超过一百毫秒, 主要耗时在取色函数(1-10ms), 性能已经够用了
我的配置: AMD R7 2700x, Nvidia RTX 2080, 3440*1440 分辨率
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 # 直接用1的坐标, 横坐标右移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 APEX_WEB_SERVICE.MAKE_REST_REQUEST 导致 ORA-29248:使用无法识别的 WRL 打开钱包