FPS游戏自动枪械识别+压枪(以PUBG为例)


文章目录

  • FPS游戏自动枪械识别+压枪(以PUBG为例)
  • 前言
  • 一、明确压枪宏的功能需求
  • 二、实现游戏内的鼠标指针下移
    • 1.驱动安装和链接库的加载
    • 2.通过罗技驱动控制键鼠
  • 三、实现键盘、鼠标监听
    • 1、引入库
    • 2、键盘监听
    • 3、鼠标监听
  • 四、自动识别枪械配件及关键画面信息
    • 1、背包信息的识别
    • 2、开火状态识别
  • 五、实现压枪函数
    • 1、获取弹道表、配件的参数
    • 2、编写压枪函数

前言

本篇分享PUBG自动识别+压枪宏实现的完整思路,同样的思路可套用在其他FPS游戏上,开发语言使用Python3.9。
项目完整代码见:https://github.com/Cjy-CN/PUBGRecognizeAndGunpress


一、明确压枪宏的功能需求

自动压枪简单的理解就是控制鼠标下移。但是,鼠标下移量的多少却是多方面因素的共同影响结果,以PUBG(绝地求生)为例,每一发子弹射出时的后座,除了每把枪的弹道表,还与枪械配件(倍镜、握把、枪托)、射击时的人物姿势(站、蹲、趴)、开火状态(全自动、连发、单发)相关联。因此,要实现压枪的需求,就必须解决背包中的配枪、配件识别,按下开火键时的人物姿势、开火模式识别,最后才是如何调用硬件驱动以实现游戏内鼠标指针下移操作。

二、实现游戏内的鼠标指针下移

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

1.驱动安装和链接库的加载

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

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

2.通过罗技驱动控制键鼠

demo代码如下:

#按下鼠标按键
def press_mouse_button(button):if gmok:gm.mouse_down(button)#松开鼠标按键
def release_mouse_button(button):if gmok:gm.mouse_up(button)#点击鼠标
def click_mouse_button(button):press_mouse_button(button)release_mouse_button(button)#按下键盘按键
def press_key(code):if gmok:gm.key_down(code)#松开键盘按键
def release_key(code):if gmok:gm.key_up(code)#点击键盘按键
def click_key(code):press_key(code)release_key(code)# 鼠标移动
def mouse_xy(x, y, abs_move = False):if gmok:gm.moveR(int(x), int(y), abs_move)

鼠标移动的函数中,xy就是移动的横纵距离。
鼠标点击函数中,传入参数1,2,3分别代表鼠标左、中、右键
键盘按键函数中,传入的参数采用的是键盘按键对应的键码


三、实现键盘、鼠标监听

前面说到,要实现压枪就要对各种配件、状态做出识别。那么在写识别的函数之前,我们先要解决的是何时识别的问题。如果识别使用多线程\多进程的一直持续检测,无疑是一种巨大的开销,因此就需要对键盘、鼠标的状态进行监听。只有按下特定按键时,才触发特定相应的识别请求。
【本章节代码对应项目中monitor.py模块】

1、引入库

这里我使用的钩子是Pynput,其他可使用的库还有Pyhook3
由于我们只需要对绝地求生这个窗口进行监听,而不是想在任何界面都去让我们的鼠标在按下左键时自动下移,因此还引入win32gui进行当前窗口的判断。

from pynput import mouse,keyboard
from win32gui import GetWindowText, GetForegroundWindow

2、键盘监听

在PUBG中,Tab键是打开背包,因此键盘对应检测Tab键。代码都很浅显。只有以下需要注意

  • Listener中绑定on_press和on_release的函数( on_key_press、on_key_release),它们返回False的时候是结束监听,下文鼠标监听的函数同理,所以不要随便返回False
  • 键盘的特殊按键采用keyboard.Key.tab这种写法,普通按键用keyboard.KeyCode.from_char(‘c’)这种写法
def start_key_listen(dict):def on_key_press(key):nonlocal dictif '绝地求生' in GetWindowText(GetForegroundWindow()):if key == keyboard.Key.tab:if dict['key_pressed']:return Truedict['key_pressed'] = Truedict['bag_signal'] = Trueelif key == keyboard.KeyCode.from_char('1'):dict['switch'] = 0elif key == keyboard.KeyCode.from_char('2'):dict['switch'] = 1if dict['fire_signal']:# 开火过程中才对卧(z),蹲(c/ctrl),切换开火模式(b)的按键进行检测,检测到则更新当前开火状态if key == keyboard.KeyCode.from_char('z') or key == keyboard.Key.ctrl or key == keyboard.KeyCode.from_char('b') or key == keyboard.KeyCode.from_char('c'):firestate_struct = get_firestate()dict['posture'] = firestate_struct.posturedict['firemode'] = firestate_struct.firetypedef on_key_release(key):nonlocal dictdict['key_pressed'] = Falsewith keyboard.Listener(on_press=on_key_press, on_release=on_key_release) as listener:listener.join()
传进start_key_listen的参数dict是由multiprocessing.Manager().dict()创建的多进程共享变量,该变量是进程安全的

pynput实现键盘监听需要我们先定义两个函数:一个是检测到按下按键触发的函数on_press,一个是检测到松开按键触发的函数on_release。然后把这两个函数作为创建Listener的两个参数。

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

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

3、鼠标监听

def on_button_click(x,y,button,pressed):global fire_signalif button == button.left:if pressed and '绝地求生' in GetWindowText(GetForegroundWindow()):fire_signal = Trueelse:fire_signal = Falseelse:passreturn Truedef start_mouse_listen():with mouse.Listener(on_click=on_button_click) as listener:listener.join()

四、自动识别枪械配件及关键画面信息

【本章节代码对应项目recognize.py模块】

1、背包信息的识别


背包画面的截图如上,我们的识别工作其实就是对红框中的内容进行识别,我没有把弹夹框上是因为弹夹不影响后座力。

在选框上其实是有几个值得注意的地方:

  • 选的框在足够囊括画面信息的前提下应该越小越好。注意到,对于枪械的识别我的识别区域是枪械名字而不是枪械的图形,并且对于Beryl M762这种名字长的枪,我并没有把它的名字截全,这就是因为这个小框里的图片内容已经足够表达出这把枪械的完整信息。如果你详细查看了我github项目里面截取的demo图片,你会发现倍镜的图也没截全,准确的说只是截了倍镜的一部分。这是因为框越大,与枪械信息无关的图片内容在框中的占比就越大,这就是图片噪声,噪声越大的图片对于我采用简单算法完成图片识别工作的误差影响就越大。
  • 一号武器位和二号武器位的图片区域应该对齐。这里说的“对齐”是指比如对于AKM这把枪,当它位于一号武器位和二号武器位的时候,你截出来的图片应该保证是一样的,不能有位置上的偏差。当然这个的根本原因是采用了较为简单的图形算法,而使用简单算法的目的是减少时间上的开销。

(这里顺带提一下为什么不用ocr吧。我看其他人写配件识别都去调用opencv的ocr,这其实是一个很笨的写法,首先OCR是一个泛用的文字识别算法,解决的是字体、大小都不确定下的文字识别问题。但是,我们这个游戏内的枪械其实是一个十分有限的集合,枪也不过二三十把枪,枪械的字体、位置、大小都是固定的,因此完全可以把这些固定的字体当做图片,然后比较出最相似的类别来确定当前是什么枪。这样做除了更小的算力开销,识别的准确率也要高于通用的文字识别库)

在解决完背包点位信息的截取,下面来说说具体是怎么做识别

  1. 如刚刚上文所述,在开发阶段,先把枪、配件的图截一遍,保存到项目里面(为了提高识别的准确率,防止半透明背景的干扰,可以对图片进行一次自适应二值化处理),作为demo图片,用于之后与用户装备截图的信息进行比较。
  2. 比较用户装备截图与预存的demo图,选择最相似的作为比对结果,先上代码再解释
def current_equipment():gun1 = ''gun2 = ''gun1_distance = 11  #武器识别的汉明距离阈值gun2_distance = 11# print('识别当前配枪')gun_path = './picture/gun/' #预先截取的demo图片路径equi1_path = './picture/equiment/im_1.png' #当前武器图片路径equi2_path = './picture/equiment/im_2.png'content = os.listdir(gun_path)for each in content:demopath = gun_path+eachtmp_dist1 = compare2pic(equi1_path,demopath,10)tmp_dist2 = compare2pic(equi2_path, demopath, 10)if tmp_dist1 < gun1_distance:gun1 = str(each)[:-4]gun1_distance = tmp_dist1if tmp_dist2 < gun2_distance:gun2 = str(each)[:-4]gun2_distance = tmp_dist2print('1号武器是:'+gun1)print('2号武器是:' +gun2)return [gun1,gun2]def compare2pic(equi, demo, threshold):equi_hash = get_hash(equi)demo_hash = get_hash(demo)distance = get_Hamming(equi_hash, demo_hash)if distance <= threshold:return distancereturn threshold+1#图像哈希
def get_hash(img):hash = ''image = Image.open(img)image = np.array(image.resize((9, 8), Image.ANTIALIAS).convert('L'), 'f')for i in range(8):for j in range(8):if image[i, j] > image[i, j + 1]:hash += '1'else:hash += '0'hash = ''.join(map(lambda x: '%x' % int(hash[x: x + 4], 2), range(0, 64, 4)))  # %x:转换无符号十六进制return hash#汉明距离
def get_Hamming(hash1, hash2):Hamming = 0for i in range(len(hash1)):if hash1[i] != hash2[i]:Hamming += 1return Hamming

识别原理:缩小图片->转换灰度图->哈希计算

  • current_equipment():这个函数就是拿截取到的当前用户装备的图片,跟预先截取的目录里面的文件一个个进行比较。然后取汉明距离最小的作为比较结果(最相似的图片)。如果比较出来最小的汉明距离都比原先设置的比对阈值要大,那么就是没有匹配项。
  • get_hash(img):在这个函数中,先是把读取的图片缩小为(9*8),使压缩后图片一个点可以代表原图多个点信息,增加运算效率的同时,也是压缩了原图半透明的背包界面背景,相当于降噪。然后将图片转换为灰度图,这里的功能也是进一步降低半透明背景的影响,并且由于我们检测的枪械名称是白色的,灰度化的处理也会强化了这部分白色枪械名称的信息。然后是哈希转化为二进制,这里用相邻点灰度比较的方法进行哈希,原理直观理解成丐版的边缘检测,很适用于PUBG背包界面中枪械名称与背景对比大的场景和二值化后的图片比较。

2、开火状态识别

【这部分对应recognize.py下的get_firestate()函数】
开火状态识别主要是判断开火状态是什么姿势,有没有子弹,射击是全自动、单发还是连发。
以上的这几个信息,各位可以在开镜模式下,屏幕的正下方看到。
这一部分的代码在取检测点时不是使用比较图片的方式,而是直接在屏幕上取固定坐标的像素点,判断白色或者红色(红色是指子弹打完时子弹那里会有一个红色的0)。唯一注意一点,白色的话也不是全白,所以把像素点取下来后,计算灰度值(用RGB均值算)不要把判断设置成255的全白,差不多220、230就行了。具体代码不贴了,项目里也有,没什么内容可以介绍。
这里顺带提一个很好用的工具,微信截图。在整个项目的开发过程中,我频繁使用微信截图,因为不仅能判断当前鼠标的坐标,还能显示坐标点的RGB值。

五、实现压枪函数

【本章节代码对应项目gun_press.py模块】

1、获取弹道表、配件的参数

这一部分就不是编程的工作了,都是苦力活。主要获取的内容是每发子弹射出间隔时间、每把枪打出第几发时对应的后坐力、各配件对后坐力的影响系数。对于弹道的获取这里给一个简单的实现思路
假设一把新的枪有40发,子弹间隔、弹道未知:
子弹间隔:记录下按下鼠标左键的时间和松开鼠标左键的时间,记住从按下到松开这段时间不要打空弹夹,就假设你打了15发,那么用(松开时间-按下时间)/15就是这把枪每发的间隔
弹道:直接预设40发的后座都是40,然后尝试压枪,压不住就加,压过了就减。

2、编写压枪函数

文章前面已经介绍了背包信息、状态信息的获取方法,键鼠钩子的设置以及压枪函数应该在一个独立的线程\进程内运行。
那么,首先要把前面步骤识别到的信息传进我们的压枪线程\进程里面

传进fire的参数dict是由multiprocessing.Manager().dict()创建的多进程共享变量,该变量是进程安全的
def fire(dict):Guns = []while True:if dict['bag_signal']:if is_bag_open():Guns = recognize_equiment()dict['bag_signal'] = Falseif dict['fire_signal']:if not bullet_check():continuestart_time = round(time.perf_counter(), 3) * 1000firestate_struct = get_firestate()dict['posture'] = firestate_struct.posturedict['firemode'] = firestate_struct.firetypeif len(Guns) > dict['switch']:gun = Guns[dict['switch']]if gun.name == 'None':continuei = 0if gun.single == False:  #不是单发的枪while True:posture_ratio = gun.posture_states[dict['posture']]down = gun.para_range[i] * posture_ratio * gun.ki += 1if i == gun.maxBullets or not dict['fire_signal']:breakmouse_xy(0, down)elapsed = (round(time.perf_counter(), 3) * 1000 - start_time)sleeptime = gun.interval - elapsedtime.sleep(sleeptime/1000)start_time = round(time.perf_counter(), 3) * 1000

这个是多进程写法的压枪,用dict传入信号,dict是一个字典,字典里包含了背包状态检测的信号,开火(按下左键的信号)。起初是用多线程写法,但多线程进游戏后鼠标会有移动缓慢的bug。

首先在该函数中拥有一个局部变量Guns,用来存储检测到背包打开后识别的枪械结果。
在开始压枪时,会先获取当前人物的身姿和开火状态,然后判断当前持枪,在确定手上有持枪后才会进入到压枪步骤
在压枪函数中,通过检测开火信号量来进入压枪的流程。压枪的总数值多少通过以下代码计算

down = gun.para_range[i] * posture_ratio * gun.k
gun.para_range[i]:是第i颗子弹的后座
posture_ratio:是当前的姿势系数
k:各配件系数累乘后的结果(k值计算在背包识别过程完成)

在压枪之后需要设置一个进程的sleeptime防止过度压枪
sleeptime=‘枪械子弹射击间隔时间’-‘每颗子弹压枪过程中代码执行时间’

FPS游戏自动枪械识别+压枪(以PUBG为例)相关推荐

  1. Ai实现FPS游戏自动瞄准 yolov5fps自瞄

    大家好 我是毕加锁 (锁!) 今天来分享一个Yolov5 FPS跟枪的源码解析和原理讲解.代码比较粗糙 各位有什么优化的方式可以留言指出,可以一起交流学习. 需要了解的东西和可能会遇到的问题 1.xy ...

  2. FPS游戏自动瞄准敌人头部?是如何实现的(三)准星算法与实现自动瞄准

    准星算法 知道了准星的变化规律: 1.准星水平位置摇摆角 正北是π,逆时针逐渐减小,正南是0,继续逆时针减小到正北为-π π和-π重叠 (正北方向Y轴逐渐增加,正东方向X轴逐渐增加) 2.准星高度位置 ...

  3. 基于yolov5实现FPS游戏自瞄,理论上通杀所有射击游戏

    1.参考大佬细致教学:Python Apex YOLO V5 6.2 目标检测 全过程记录_mrathena的博客-CSDN博客 [Yolov5]使用Ai实现FPS游戏自动瞄准 yolov5fps自瞄 ...

  4. 基于yolo5制作的AI识别FPS游戏自动化

    介绍 本项目为CF网游,人物角色识别定位模型,采用yolov5框架实现,仅供学习研究使用. 模型效果展示 开发思路与流程 思路 CV领域的目标检测模型已经非常成熟,特别是yolov5系列 为什么不是用 ...

  5. 【游戏逆向】FPS网络游戏自动瞄准漏洞分析以及实现二

    开始分析人物结构 由于人物结构是一个结构体,该结构体的起始地址为人物对象地址,所以,我们继续用CE的结构体分析工具去分析人物对象地址,也就是刚才的eax地址: 通过观察,我们立刻就得到了人物名称的偏移 ...

  6. 【游戏逆向】FPS网络游戏自动瞄准漏洞分析以及实现

    了解FPS游戏自瞄漏洞 经常玩游戏的朋友,应该知道FPS游戏,例如:穿越火线,逆战等等,他们的特点就是以第一人称视角进行操作人物,屏幕中间会有一个准星,通过准星瞄准敌人进行攻击以达到击杀效果和游戏体验 ...

  7. 关于yolov5进行FPS游戏的目标检测,实现自动瞄准。

    yolov5进行FPS游戏的自瞄 前言 类似效果 main.py代码 使用方法 注意 完整链接↓ 前言 YOLOV5是一个基于视觉识别的开源项目,本人制作的目的是研究YOLO的应用,并非制作游戏外挂. ...

  8. 深度解析FPS游戏外挂+解决方案

     1974年,由Steve Colley开发的世界上第一款FPS游戏<迷宫战争>诞生,这款看似简单的游戏却为未来近半个世纪的FPS类游戏的蓬勃发展埋下了一颗种子. 据今年2月STEAM热门 ...

  9. 深度解析FPS游戏外挂形成原因与“破局”方案

    1974年,由Steve Colley开发的世界上第一款FPS游戏<迷宫战争>诞生,这款看似简单的游戏却为未来近半个世纪的FPS类游戏的蓬勃发展埋下了一颗种子. 据今年2月STEAM热门游 ...

  10. 如何在FPS游戏中快速实现简单的人体定位算法

    概述 最近在很多B站的视频上看到大佬们分享的AI应用案例,其中有一个引起了我的兴趣:基于深度学习网络, 在CSGO中实现人体定位算法,并进行自动瞄准与射击.当然,这种明显有悖于游戏公平性的行为我是不会 ...

最新文章

  1. 获取图片中感兴趣区域的信息(Matlab实现)
  2. 『设计模式』就因为多收了我2块5,我追着收银员问是不是不懂设计模式--策略模式
  3. 演示方法:有抱负的分析师
  4. 从3000米高空,一跃而下…
  5. Wordpress 修改 mysql 插件_史上最详细的WordPress安装教程(三):安装php 5.6及fpm、pdo、mysql等插件...
  6. 微软2010年1月安全公告 Windows2000独领风骚
  7. python什么时候用类设计_关于python:类模板的习惯用法或设计模式?
  8. jvm垃圾收集器与内存分配策略
  9. Baksmali用法
  10. 【机器学习】EM算法详细推导和讲解
  11. pojo类中list存储其他字段_如何从其他包含pojo类对象的数组列表中删除数组列表记录...
  12. 无卡支付,快捷支付,认证支付,协议支付,代扣区别与联系
  13. svn分支和主干的同步操作
  14. LeetCode 01:有人相爱,有人夜里开车看海,有人LeetCode第一题都做不出来
  15. linux fat get entry,操作系统--主引导程序控制权的转移
  16. Delphi与JAVA 互通AES文件加解密源码(支持D6-XE10)
  17. 2020java开发面试题
  18. 网络安全进阶篇(十一章-7)APP渗透测试篇(下)
  19. 杂项-一张图片和爆破一
  20. 少儿编程是选择线上与线下?

热门文章

  1. 几行python代码实现Windows软件卸载
  2. matlab:randn函数产生图像高斯噪声
  3. oracle 能被2整除_整除专题基础篇 “刀法四式”
  4. 怎么通过当地时区计算格林尼治_时间规划局:时间能看到,标注在你的手臂上,那时的你会怎么样...
  5. vue-cli 2.6.9 安装卸载及创建一个工程
  6. Electron IPC(进程间通信)之ipcMain和ipcRenderer
  7. 使用Native API查询Windows硬盘分区系统设备名称
  8. db2查询字段备注_SQL基础7:SQLSERVER、ORACLE、DB2中SQL语句查询表字段名、注释、字段类型...
  9. QGraphicsView实现局部缩放,平移,并且能进行选中数据
  10. TcpSocket的Qt串口实现与QtSocket接收数据不完整处理方法