前言
前段时间因为朋友的原因,我重新入坑了英雄联盟,拿起了放了好几年的一区账号。我们最开始一直打的是大乱斗。但是最近双城之战2的上映,让大乱斗也改版了,感觉新版地图让我有点摸不着头脑,遂开始玩云顶,我对云顶的认知还是我退坑时的状态(那时云顶之弈才出来不久,当时我也没怎么玩这个模式),所以我还相当于云顶萌新,对羁绊、装备等机制都还不熟悉。每局基本都在678名。突然我脑海蹦出一个想法,我可不可以自己写一个程序,让它自动帮我安排阵容,或者根据对局灵活切换阵容。
软件定位
首先考虑的是这个软件的定位,我并不想称它为外挂,我也不想因为这程序破坏游戏的公平性,所以我不会用它来做自动D牌,自动站位等操作,而是用类似Wegame工具的方法来做阵容安排(在我写这段文字时,我发现Wegame确实有一个云顶助手,但是因为官方改版原因,暂时还不能使用)。
另外,毕竟是个软件,就该有个名字,因为云顶之弈的英文缩写是TFT,所以我打算取名为TFTool。
功能结构
- 商店提醒:作为一个辅助工具,提醒一下我的卡牌商店里有我想要的牌很合理吧。我预想的效果是:我可以先标记某个英雄,在底部栏出现这个英雄的时候软件就能提醒我。这和游戏内置的小队规划器差不多,但是可以更加灵活的标记。
- 阵容推荐:首先我可以使用现成的推荐阵容,在各个平台上就能爬取到。还能根据其他玩家的阵容灵活地更改自己阵容。
- 跟随版本更新:毕竟我并不是给当前这一个版本做的,每个赛季的英雄,玩法都会变,我的软件也要根据游戏版本进行更新。
开发历程
技术栈
- 语言:其实最初是想使用游戏的接口内嵌在游戏中,就像注入一样,但是发现这个方法太危险了,不太靠谱。后来想想还是用Python方便一些,毕竟以往也用Python做过许多项目。
- 窗口界面:前段时间接触了一个pyqt的UI库,叫做qfluentwidgets(这是官网:链接),感觉挺好看的,这次打算试试。
- 模型训练:因为游戏没有提供内部操作的接口,自己注入又太危险了,保险起见还是用图像识别来做。遂打算使用YOLOv8来训练模型。
功能实现
商店提醒
因为商店想要做识别的话,还得单独训练一套模型出来,但是商店中的内容本来是自带文字的,所以打算直接使用OCR识别,刚好有一个ddddocr工具,用着也挺方便的。识别的大致流程如下:
截取屏幕图像
class WindowCapture:
# constructor
def __init__(self, window_name=None):
"""初始化,默认捕获整个屏幕"""
if window_name is None:
# 捕获整个屏幕
self.hwnd = win32gui.GetDesktopWindow()
else:
# 捕获指定窗口
self.hwnd = win32gui.FindWindow(None, window_name)
if not self.hwnd:
raise Exception(f"窗口未找到: {window_name}")
# 获取窗口或屏幕大小
window_rect = win32gui.GetWindowRect(self.hwnd)
self.w = window_rect[2] - window_rect[0]
self.h = window_rect[3] - window_rect[1]
def get_screenshot(self):
"""获取屏幕截图"""
# 获取窗口的设备上下文
wDC = win32gui.GetWindowDC(self.hwnd)
dcObj = win32ui.CreateDCFromHandle(wDC)
cDC = dcObj.CreateCompatibleDC()
dataBitMap = win32ui.CreateBitmap()
dataBitMap.CreateCompatibleBitmap(dcObj, self.w, self.h)
cDC.SelectObject(dataBitMap)
cDC.BitBlt((0, 0), (self.w, self.h), dcObj, (0, 0), win32con.SRCCOPY)
# 转换为 OpenCV 格式的图像
signedIntsArray = dataBitMap.GetBitmapBits(True)
img = np.frombuffer(signedIntsArray, dtype='uint8')
img.shape = (self.h, self.w, 4)
# 释放资源
dcObj.DeleteDC()
cDC.DeleteDC()
win32gui.ReleaseDC(self.hwnd, wDC)
win32gui.DeleteObject(dataBitMap.GetHandle())
# 删除 Alpha 通道(OpenCV 不支持带 Alpha 的图像)
img = img[..., :3]
return np.ascontiguousarray(img)
@staticmethod
def list_window_names():
"""列出所有窗口名称"""
def win_enum_handler(hwnd, ctx):
if win32gui.IsWindowVisible(hwnd):
print(hex(hwnd), win32gui.GetWindowText(hwnd))
win32gui.EnumWindows(win_enum_handler, None)
裁剪商店窗口
def by_cords(self, image, x1, y1, x2, y2):
"""根据坐标裁剪图像"""
return image[y1:y2, x1:x2]
def shop_window(self, image):
"""裁剪商店窗口"""
return self.by_cords(image, 481, 952, 1476, 1070)
裁剪出每个英雄的名字
def cells_of_shop_window(self, shop_window_image):
"""从商店窗口裁剪 5 个商品槽"""
slot_width = 189
gutter_width = 12
gutter_fix = 0
crops = []
for i in range(5):
if i == 3:
gutter_fix = 1
crops.append(self.by_cords(shop_window_image, slot_width * i + gutter_width * i + gutter_fix, 0,
slot_width * (i + 1) + gutter_width * i + gutter_fix, 118))
return crops
对每个英雄进行OCR识别
hero_names = []
for idx, cell in enumerate(cells):
hero_name = self.extract_hero_name(cell)
# 使用匹配算法来找到最接近的英雄名
best_hero_name = self.match_hero_name(hero_name)
hero_names.append((idx + 1, best_hero_name))
# 保存裁剪结果(可选)
cv2.imwrite(f"temp/cell_{idx + 1}_hero_name.png", self.crop_hero_name(cell))
return hero_names
这里有一个小细节,OCR识别出来的名字可能会出现误差,比如“婕拉”会识别成“捷拉”,我这里的解决方法是使用difflib库的get_close_matches方法,把识别结果和现有的所有英雄名进行对比,最终挑选出差值最小的那个名字。具体代码如下:
匹配最佳识别名
def match_hero_name(self, ocr_name):
"""根据 OCR 提供的英雄名进行匹配,找到最相似的英雄名"""
all_heroes = self.tft_data.all_chess_name_str.split('-') # 假设英雄名以'-'分隔
# 使用 difflib 匹配最相似的英雄名称
best_match = difflib.get_close_matches(ocr_name, all_heroes, n=1, cutoff=0.4)
return best_match[0] if best_match else "-"