主要需要用到的库有:pywin32gui,PIL,cv2(opencv-python),pywin32api,pyautogui
这篇文章是用于记录我自己的思路,当初在淘宝买了fgo的挂后,也买了商家的自动刷图脚本,但奈何老是更新用不了,我就打算用python写一个简单的战斗选牌以及重复刷图的脚本。(这个脚本是基于模拟器以及能一键秒杀的情况下,挂自己上某宝搜,基本所有商家都是一个产的(指搬运汉化外国大神卖二手国内))。
实现自动挂机,需要实现两个基本功能,一是图像匹配,二是鼠标移动定位,三是自动选牌。
一,图像匹配和鼠标移动
图像匹配自然分为截图和匹配
截图那自然就是自己预先截图保证到一个文件夹中,然后调用图片(使用PIL的Image函数,opencv的我试过不知道为什么不行,懒得想了),再使用pyautogui.locateCenterOnScreen进行对屏幕的图像匹配,im2是这个攻击按钮的图片,confidence是置信度。
然后如果匹配成功,会返回x值,这个x值是图片的中心坐标。我使用win32api.SetCursorPos进行鼠标移动定位,然后再用pyautogui.click实现鼠标单击。(我也不知道为什么我当时要用两个库实现一个功能,可能是为了学习多一个库的用法吧。)
from PIL import Image
import pyautogui
import win32api
def attack_star():
im2 = Image.open('./fgo国服截图/开始.png')
x = pyautogui.locateCenterOnScreen(im2, confidence=0.9)
if x:
win32api.SetCursorPos(x)
pyautogui.click()
return x
二,自动选牌
这一点是脚本的核心所在!
因为fgo的5张牌是随机生成,如果按顺序直接选前3张牌,有可能不能一波结束一个回合,而我需要写一个小算法,简单实现匹配出3张不一样的牌,用列表保存3张牌的图片,在依次用图像匹配寻找坐标位置,在用鼠标点击即可实现快速结束战斗。
1,截取窗口位置
因为我使用的是逍遥模拟器,我需要先定位模拟器窗口的坐标信息
这里我用的是win32gui.FindWindow先获取窗口的权柄,然后再win32gui.GetWindowRect(handle)返回窗口的坐标信息,返回的坐标(x1,y1,x2,y2),(x1,y1)是窗口的左上角位于屏幕的坐标,而(x2,y2)是窗口右下角的坐标。
import win32gui
def get_windows_info():#获取窗口信息
# wdname = u'Fate/GO - MuMu模拟器'
wdname = u'逍遥模拟器'
handle = win32gui.FindWindow(0, wdname) #注意大写
if handle == 0:
return None
else:
return win32gui.GetWindowRect(handle)#窗口坐标位置
2,截取模拟器窗口内的卡牌位置
思路是,卡牌是永远固定再模拟器窗口里面的固定位置,而我们需要的卡牌坐标自然也是相对于模拟器的坐标(即从模拟器左上角为0点开始计算卡牌坐标位置),而用鼠标选牌的时候,再加上模拟器左上角的坐标(x1,y1),就能得出绝对坐标(相对于电脑屏幕的坐标位置)。
但要注意的第一点是,模拟器的窗口是可以变大变小的,所以我们计算坐标的时候,需要用比例关系算出卡牌位于模拟器的位置。这样一来,无论窗口变化,按照对应的比例我们都能截取到卡牌的位置。
计算公式可能有点复杂,我们需要知道,基于逍遥模拟器,是有两个边框的(上边和右边),这两个边框的宽度是不变的,所以需要再计算比例的时候留意这个固定数值。
def abc_zh(i): # 五张攻击卡的截图位置(i=0,1,2,3,4),基于逍遥模拟器
s(1)
x1, y1, x2, y2 = get_windows_info() # 得到模拟器窗口坐标
x_in = (x2 - x1 - 52) // 1600 # 游戏实际窗口的x坐标的比例系数 =(模拟器窗口的宽-模拟器右边框的宽)//某次游戏窗口的宽
y_in = (y2 - y1 - 39) // 900 # 游戏实际窗口的y坐标的比例系数 =(模拟器窗口的高-模拟器上边框的高)//某次游戏窗口的高
x_dis = 193 # 某次游戏卡牌的宽
y_dis = 123 # 某次游戏卡牌的高
''''''
xu =(64 + 320 * i) * x_in + x1 # 卡牌左上角的x绝对坐标 = (某次相对于游戏窗口的第一张卡牌左上角x坐标 + 距离第 i+1 卡牌的距离)* x比例系数 + 模拟器窗口左上角x坐标
yu =530 * y_in + y1 # 卡牌左上角的y绝对坐标 = 某次相对于游戏窗口的第一张卡牌左上角y坐标 +y比例系数 + 模拟器左上角x坐标
xd =xu + x_dis * x_in # 卡牌右下角的x绝对坐标 = 左上角坐标 + 卡牌宽度 * x比例系数
yd =yu + y_dis * y_in # 卡牌右下角的y绝对坐标 = 左上角坐标 + 卡牌高度 * y比例系数
return (xu,yu,xd,yd) # 返回卡牌坐标信息
简单解读一下代码,例如x_in的注释里的
(模拟器窗口的宽-模拟器右边框的宽)//某次游戏窗口的宽
中
(模拟器窗口的宽-模拟器右边框的宽)= 当前游戏窗口的宽
而里面的固定数字,都是我在某次写代码的时候,游戏窗口的长宽(游戏窗口指的是除去固定的边框后,只有游戏画面的窗口。),这些数据都是用qq截图的坐标显示记录下来的,用qq截图一个一个获取对应卡牌的相对坐标。
i值为1的时候,就是第二张牌的x相对位置坐标,因为牌与牌之间的间隔是一样的,而高度y不变,所以只需要知道第一个牌的坐标就能推导出其他卡牌位置。
对了,卡牌的截取并不是整一张牌,因为我们需要的是打出不同角色的牌,而同一个角色的牌会有三种文字,匹配的时候如果匹配整张牌,会把同一个角色的红绿蓝牌匹配为不同角色的牌,所以我们需要截取的位置是卡牌的人物头像部分即可。
3,给图片进行排位置
这是自动选牌的核心所在,思路是通过图片与图片匹配,得出置信度,当置信度高于一个阈值,则把这两张牌认为是同一个人。(这里用的是cv2.TM_SQDIFF_NORMED,返回的值越低反而代表图片越相似,如果需要正常的置信度值,可以找cv2.matchTemplate的用法。)
图片匹配:
import cv2
from PIL import ImageGrab
import time
def s(i):
return time.sleep(i)
def fenlei(img1,img2): # 匹配图片相似度,接近 0 为相似
result = cv2.matchTemplate(img1, img2, cv2.TM_SQDIFF_NORMED)
return result # 返回一个值,范围为(0,1), 接近 0 表示相似
选牌算法:
def attack_list(l): # 用于判断5张卡牌图片的排序,返回的a表包含3张牌
# set = 0.55
set = 0.4 # 设定一个阈值
a =[]
if not fenlei(l[0],l[1])
图片的保存,调用排序函函数,实现点击3张不同的牌:
def attack_zh():
s(1)
i = 0
for xx in range(5):
card = ImageGrab.grab(abc_zh(i)) # 调用函数abc_zh返回坐标,用PIL截取每一张卡牌
i += 1
card.save('card'+str(i)+'.png') # 保存卡牌图片
card1 = cv2.imread("card1.png") # 读取图片
card2 = cv2.imread("card2.png")
card3 = cv2.imread("card3.png")
card4 = cv2.imread("card4.png")
card5 = cv2.imread("card5.png")
l = [card1,card2,card3,card4,card5] #把图片放入列表
a = attack_list(l) # 把列表用选牌算法得出列表a,列表a包含了3张牌
for i in range(3):
x = pyautogui.locateCenterOnScreen(a[i], confidence=0.8) # 允许误差0.9
win32api.SetCursorPos(x)
pyautogui.click()
三,自动挂机
if __name__=='__main__':
pass
s(2)
i = 1
while True:#战斗选卡
# pyautogui.click()
if pyautogui.position() == (0, 0): # 鼠标移动到左上角,停止死循环
print('中止程序')
break
if other.attack_blue(): # 是否有开始键
print('第 ', i, ' 回合')
attack_zh() # 选牌攻击
i = i + 1
# if other.attack_over(): # 是否到了结束画面
# print('战斗结束')
# break
最后一步把所有功能整合起来到main中就行啦,本篇文章只是记录了一小部分的重要代码,其他的像判断回合结束,点击继续战斗等,都是“匹配图片,鼠标点击”,只不过麻烦点在于每个模拟器的边框、比例不一样,国服日服的图片不一样,都是些重复性的工作了。
但这种编写脚本的方式也有很多缺点,首先图片匹配,有时候放大缩小窗口,图标也跟着变小,它这个匹配反而不好使了,可能找不到。我的截取卡牌图片的公式也好像有点问题,因为我后来调整了模拟器窗口大小后,截取的位置反而偏小了,解决的办法只能是普通运行脚本的时候,不要老是变换窗口大小,默认的窗口大小默认的数据就行了。
2022.5.29