本项目仅限于学习交流不可商用,不可用于非法用途(包括但不限于:用于制作游戏外挂等)。
一开始是觉得不应该讲得太详细,但实际上不管是原理还是代码,在网上都是一找一大堆了,真有心也能拿去用了,还是记录一下。
实现一个AI自瞄,基本是三步走,屏幕截图——目标检测——鼠标移动,只有目标检测这一步需要用到AI,接下来各个部分分开讲一下。
首先是屏幕截图,如果确定要用Pytorch作为框架的话,那么Python的截图方法主要是三种:
第一种是用PIL或者Pillow进行图像处理,这两个库是大部分Python图像处理类的项目都会有的库,优点是很方便,但是效率不怎么样。
第二种是用pyautogui,这个库是自动化图形界面库,除了截图以外还能够控制鼠标,但是可能是为了跨平台使用,pyautogui的运行速度也很差,在我的电脑上截一张图需要0.02s左右,额,我跑一次Yolo才0.04秒,这都占一半时间了
第三种是用win32api,这个是微软给的应用程序编程接口,可以进行截图,缺点是代码比较长,不能跨平台,但是应该也没人会用Linux,Mac打游戏吧。
除此之外还有一些其他方法,比方说OBS提供了Python的api用于构建脚本,这个效率也很高,毕竟广泛用于直播推流,但是OBS是写在C平台上的,如果是用TensorFlow会比较方便。
在GitHub上有一个快速截图的库:d3dshot,至少在我测试下来,这个速度应该是最快的,但是缺点是一旦切屏出去了,截图就会出问题,没找出问题在哪。
这样看来最适合的截图库就只剩下了win32api和d3dshot,时间上差不多,但是win32api似乎BUG更少。
键盘事件监听
在开始之前我们需要监听键盘的动作,这一部分通过pynput实现:
mode = False
def on_press(key):
global mode
if key == keyboard.Key.f11:
mode = not mode
def on_release(key):
"""松开按键时执行。"""
# if key == keyboard.Key.esc:
# pass
pass
if __name__ == '__main__':
listener = keyboard.Listener(
on_press=on_press,
on_release=on_release)
listener.start()
这样就可以通过f11来进行开启或关闭,当然也可以改成别的按键,因为python貌似并没有哪个库支持鼠标侧键的读取,所以可以在鼠标驱动写一个简单的按键触发。
屏幕截取
def window_prepare():
hwnd = 0 # 窗口的编号,0号表示当前活跃窗口
# 根据窗口句柄获取窗口的设备上下文DC(Divice Context)
hwnd_dc = win32gui.GetWindowDC(hwnd)
# 根据窗口的DC获取mfcDC
mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
# mfcDC创建可兼容的DC
save_dc = mfc_dc.CreateCompatibleDC()
# 创建bigmap准备保存图片
save_bit_map = win32ui.CreateBitmap()
# 获取监控器信息
width = 640
height = 360
# 为bitmap开辟空间
save_bit_map.CreateCompatibleBitmap(mfc_dc, width, height)
return save_bit_map, save_dc, mfc_dc, width, height
def window_capture(save_bit_map, save_dc, mfc_dc, width, height):
# 高度saveDC,将截图保存到saveBitmap中
save_dc.SelectObject(save_bit_map)
# 截取从左上角(0,0)长宽为(w,h)的图片
# saveDC.BitBlt((0, 0), (w, h), mfcDC, (0, 0), win32con.SRCCOPY)
save_dc.BitBlt((0, 0), (width, height), mfc_dc, (640, 360), win32con.SRCCOPY)
signed_ints_array = save_bit_map.GetBitmapBits(True)
img = np.frombuffer(signed_ints_array, dtype='uint8')
img.shape = (height, width, 4)
img0 = img[:, :, :3]
return img0
程序初始化阶段先运行window_prepare,之后通过键盘控制是否循环触发window_capture,捕获屏幕中央640x360的空间,进行检测,移动鼠标。(我这里的分辨率是1920x1080)
检测的部分跟之前的行人检测的demo是一致的,这里就不提了。总之是把截图丢给YoloV5,跑出结果后把坐标返回。
行人检测demo | 莉莉娅! (diduseemyelk.github.io)
当然我们这里还有一个小问题,YoloV5跑完之后的结果坐标默认是按照置信度进行排序的,但很显然,如果有多个目标的时候,一般采取就近原则,谁离准心近,就瞄哪,所以还有一个小函数计算哪一组坐标离准心最近。
def closest(self, boxes):
distance_list = []
for (x1, y1, x2, y2, label, conf) in boxes:
mid_x = (x1 + x2) / 2
mid_y = (y1 + y2) / 2
distance = (mid_x - 960)**2 + (mid_y - 540)**2
distance_list.append(distance)
value = min(distance_list)
index = distance_list.index(value)
return index
鼠标移动
鼠标的移动也有很多库可以用,还是跟上面一样,pyautogui的效率很低,我很难想象只是移动一下鼠标的操作,在pyautogui上都需要0.02s,GitHub上有一个mouse的第三方库,用这个库实现的话耗时是检测不到的。
具体代码我们连着前面两部分一起讲
def aiming(save_bit_map, save_dc, mfc_dc, width, height):
# 读取每帧图片
im = window_capture(save_bit_map, save_dc, mfc_dc, width, height)
bboxes = detector.detect(im)
# 如果画面中有bbox,即detector探测到待检测对象
if len(bboxes) > 0:
box_index = detector.closest(bboxes)
check_point_x = int(bboxes[box_index][0] + ((bboxes[box_index][2] - bboxes[box_index][0]) * 0.5)) + 640
check_point_y = int(bboxes[box_index][1] + ((bboxes[box_index][3] - bboxes[box_index][1]) * 0.5)) + 360
move_x = int(1.4 * (check_point_x - 960))
move_y = int(1.4 * (check_point_y - 540))
mouse.move(move_x, move_y, absolute=False, duration=0)
pass
else:
pass
check_point_x和check_point_y是目标检测的中心点,这个点是离准心最近的点。但是值得注意的是,并不是直接把这个参数扔进去move就可以了,因为这种第一人称射击游戏,他都不需要记录你鼠标的位置,只需要记录你鼠标相对位移,并根据你的DPI和鼠标灵敏度进行转向即可。虽然这个目标离你的准心差了可能200个像素点,但转动和平移是两件事情,如何保证你转动过后目标恰好就在你的准心上呢?这就需要用到球面理论去进行计算了。
但是在游戏里,人物模型是有大小的,对于很远的人物,Yolo识别不出来,对于很近的人物,人物HitBox又足够大,即便第一时间没有瞄到最中央,仍然能够命中,因此很大程度上,我们不需要百分百精确地计算出move_x和move_y,完全可以利用中间的线性区域作线性拟合,找到一个经验常数,能够从check_point_x和check_point_y生成出误差较小的move_x和move_y即可,不同的分辨率可能会有所不同,我这分辨率取1.4差不多是比较稳定的。最后用mouse.move函数进行鼠标操作,这里的duration是指延迟,就是执行这个鼠标操作的耗时,如果取0.1的话其实会看起来比较丝滑。
ok最后附上一段bot测试吧。