资讯动态

Python+OpenCV王者荣耀自动化脚本:图像识别与坐标换算实战

发布时间:2026/10/14 16:41:18 来源:尧图企业网站定制
简介这是一套基于 Python 编写的《王者荣耀》自动化操作脚本面向对游戏辅助开发与自动化原理感兴趣的编程学习者也适合有 Python 基础的玩家做技术研究与功能延伸。压缩包内含相对完整的项目结构覆盖界面操作、运行配置与调试工具便于从工程视角理解脚本的运行流程。压缩包共 62 个文件整体约 667KB内容以 JSON 配置、Python 脚本及 pyc 编译模块为主另有少量辅助工具、说明文档与开源许可文件。多分辨率配置与公共基础模块为不同机型适配提供了可读性示例脚本项目围绕自动化游戏操作这一主题组织内置说明文档与设备调试工具并将核心入口、截图定位、调试等拆分为独立模块可帮助梳理图像识别、事件监听与游戏逻辑控制的实现思路。已有 23954 人学习适合拆解自动化脚本的目录设计并在二次修改与排错中提升代码阅读和调试能力。1. 基于Python的王者荣耀脚本到底能做什么很多人一听到“Python玩王者荣耀”就觉得是玄学其实最常见的落地方式是用OpenCV加pyautogui做一套视觉闭环脚本程序不断截取游戏画面通过模板匹配和颜色过滤找到“开始战斗”“领取奖励”这类按钮再模拟鼠标点击把重复的刷图、领奖励、做日常动作跑起来。王者荣耀这类游戏没有开放数据接口画面又允许拖动窗口所以脚本真正难的不是代码而是坐标怎么换算、识别怎么稳定、状态机怎么组织。这篇笔记复盘一个最小可运行版本适合刚接触Python自动化、想在模拟器里做重复任务验证图像识别能力的新手也适合想看窗口坐标和模板匹配细节的老手。2. 先解决坐标系统窗口位置、DPI缩放和两次坐标换算写脚本的第一步不是打开IDE而是让代码知道游戏窗口此刻在哪、有多大。很多新手上来就写pyautogui.click(960, 540)在自己电脑上能跑换一台分辨率或缩放比例不同的电脑就废了。问题多半出在系统 DPI 缩放和窗口偏移上。我习惯先把窗口信息拿到手再把所有点击都走一遍“截图局部坐标 → 窗口偏移 → 屏幕绝对坐标”的换算。2.1 为什么硬编码坐标会翻车真实分辨率与系统缩放先说第一个坑Windows 窗口可以被用户拖到屏幕任何位置分辨率也不一样。你以为窗口在中心实际可能偏左 100 像素。更隐蔽的是系统显示缩放常见的笔记本是 125% 或 150% 缩放。如果不先对进程开启 DPI 感知用 win32gui 拿到的窗口坐标是经过缩放计算的和 pyautogui 截图用的坐标对不上所有点击都会精准地偏到按钮旁边。常见做法是脚本启动后先调用SetProcessDPIAware()让后续的窗口函数和图像截图都基于真实物理像素。下面这段代码就是最基础的窗口查找逻辑。import ctypes import win32gui # 让进程感知DPI避免系统缩放导致坐标失真 ctypes.windll.user32.SetProcessDPIAware() def find_window_by_title(title: str): # 通过窗口标题查找游戏窗口 hwnd win32gui.FindWindow(None, title) if not hwnd: raise RuntimeError(f找不到窗口: {title}) # 激活并显示窗口保证截图时窗口在前台 win32gui.ShowWindow(hwnd, 5) win32gui.SetForegroundWindow(hwnd) return hwnd说明一下参数FindWindow(None, title)传None表示按窗口标题逐个比对如果游戏窗口标题会变可以用win32gui.EnumWindows遍历所有窗口按标题后缀或进程名匹配。ShowWindow的5是SW_SHOW作用是让最小化的窗口恢复SetForegroundWindow将窗口置前避免截图被其他窗口遮挡。有些环境下前台切换会被系统限制把这两个调用放在try/except里即可即使失败也不影响后续拿窗口矩形。拿到窗口句柄后下一步是获取窗口的屏幕坐标。这里用GetWindowRect取四个角然后算宽高。def get_window_rect(hwnd): left, top, right, bottom win32gui.GetWindowRect(hwnd) return { left: left, top: top, width: right - left, height: bottom - top }GetWindowRect返回的是窗口外边框在屏幕坐标系下的位置包含标题栏和边框。如果直接拿整个窗口去截图模板匹配到的坐标就是相对整张截图的后续点击时需要把left和top加回去。如果游戏客户区与整个窗口存在标题栏差值要么截图时裁掉顶部要么使用GetClientRect和ClientToScreen转换客户区原点。这两种方式都可以我推荐用客户区因为按钮坐标更接近游戏实际渲染区域。2.2 从截图局部坐标到屏幕绝对坐标的换算有了窗口矩形就可以对窗口区域截图。这里的关键是pyautogui.screenshot(region(left, top, width, height))返回的图像坐标是从(0, 0)开始的局部坐标必须先映射回屏幕坐标再执行点击。import pyautogui def screenshot_window(rect): # region参数是(left, top, width, height) region (rect[left], rect[top], rect[width], rect[height]) return pyautogui.screenshot(regionregion, all_screensTrue) def local_to_screen(local_x, local_y, rect): # 把截图内的局部坐标转换成屏幕绝对坐标 return local_x rect[left], local_y rect[top]建议把local_to_screen做成项目里唯一的坐标转换入口所有模板匹配和颜色过滤结果都先经过它再进入点击函数。这样即使窗口位置变了也只需要改rect不需要改每个调用点的逻辑。点击按钮时还要考虑模板匹配返回的是模板左上角坐标所以点中心坐标要再加上模板宽高的一半。完整调用可以这样写def click_template(hwnd, rect, template_path, threshold0.9): import numpy as np import cv2 region (rect[left], rect[top], rect[width], rect[height]) screen pyautogui.screenshot(regionregion, all_screensTrue) screen_bgr cv2.cvtColor(np.array(screen), cv2.COLOR_RGB2BGR) template cv2.imread(template_path) if template is None: raise FileNotFoundError(f模板图片不存在: {template_path}) h, w template.shape[:2] res cv2.matchTemplate(screen_bgr, template, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(res) if max_val threshold: return False x, y max_loc # 局部坐标转屏幕坐标 screen_x x rect[left] w // 2 screen_y y rect[top] h // 2 pyautogui.click(screen_x, screen_y) return True这段代码里res是每个位置的相似度矩阵minMaxLoc找出相似度最高的点max_loc是模板左上角在截图中的位置。然后rect[left]和rect[top]是把局部坐标拉回屏幕坐标w // 2和h // 2是跳到按钮中心。实际项目里我会把模板匹配单独放到图像识别模块这里只是演示坐标换算的完整链路。还有一类情况是游戏窗口本身允许调整大小窗口变宽后按钮像素也变宽固定模板就不再好使。这时可以按当前窗口宽度和基准窗口宽度的比例用cv2.resize缩放模板后再匹配。常见做法是基准分辨率在配置里保存启动时读取当前窗口rect[width]计算缩放比例scale width / base_width模板尺寸和阈值都按比例调整。这个技巧后面还会再提到。3. 图像识别模板匹配定位按钮、HSV过滤识别血条和动态色块坐标换算解决的是“看到目标后点哪里”而图像识别解决的是“怎么找到目标”。王者荣耀界面里的元素可以分成两类一类是固定图标比如“开始战斗”按钮按钮形状颜色基本不变另一类是动态元素比如英雄血条、技能冷却进度颜色和位置都在变化。对固定元素用模板匹配对动态元素用颜色过滤两者配合才能覆盖主流程。3.1 模板匹配识别按钮和固定图标模板匹配的原理是把一张小图作为模板在截图中从左到右、从上到下做滑动窗口比较计算每个位置的相似度。OpenCV 提供好几种匹配算法TM_CCOEFF_NORMED对光照和亮度变化相对不敏感适合游戏界面这种可能带高光的场景。代码里只需要关心返回的相似度矩阵和全局最大值位置。import numpy as np import cv2 def match_template(screen_bgr, template_bgr, threshold0.85): h, w template_bgr.shape[:2] # TM_CCOEFF_NORMED 相似度范围约-1到10.85以上基本可确认 res cv2.matchTemplate(screen_bgr, template_bgr, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(res) if max_val threshold: return None return max_loc[0], max_loc[1], h, w, max_val注意这里的返回值max_loc[0]是模板左上角 xmax_loc[1]是 yh和w是模板尺寸max_val是相似度。阈值threshold建议先设 0.85 测试如果出现误点再逐步提高。同一界面可能存在多个相同按钮比如“领取”按钮出现多次这时需要用np.where过滤所有高于阈值的点并做简单去重。def match_all_templates(screen_bgr, template_bgr, threshold0.8): h, w template_bgr.shape[:2] res cv2.matchTemplate(screen_bgr, template_bgr, cv2.TM_CCOEFF_NORMED) ys, xs np.where(res threshold) points [] for x, y in zip(xs, ys): points.append((x, y, h, w, float(res[y, x]))) return points这里np.where(res threshold)返回两个数组第一个是 y 坐标第二个是 x 坐标所以zip(xs, ys)是修正顺序后组成坐标对。实际使用时同一个按钮可能连续多个位置都高于阈值还需要按坐标距离去重。我一般会按相似度排序保留第一个点然后剔除与其矩形区域相交的其他点这是简单的非极大值抑制思路。3.2 HSV颜色过滤动态血条和技能冷却不再依赖模板模板匹配的局限是模板必须和屏幕上按钮几乎一样。对于动态变化的血条红色区域面积随血量变化模板根本覆盖不过来。这时候要用 HSV 颜色空间。HSV 把颜色拆成色相、饱和度、明度和人的视觉感知更接近对光照变化也更稳定。常见场景是识别英雄血条里的红色部分。红色在 HSV 色相环上是个特例0 和 180 两个方向都算红所以要取两段范围做或运算。import cv2 import numpy as np def find_color_region(screen_bgr, lower_hsv, upper_hsv, min_area100): hsv cv2.cvtColor(screen_bgr, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, lower_hsv, upper_hsv) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None max_c max(contours, keycv2.contourArea) if cv2.contourArea(max_c) min_area: return None x, y, w, h cv2.boundingRect(max_c) return x, y, w, h参数说明lower_hsv和upper_hsv是三元组比如识别红色血条第 1 段为(0, 120, 70)到(10, 255, 255)第 2 段为(170, 120, 70)到(180, 255, 255)。min_area用于过滤灰尘般的噪声避免把技能特效当成血条。取色的时候不要凭感觉写 HSV 值我习惯先截图用取色工具读取目标区域的颜色再转成 HSV。如果血条红色不明显也可以改成蓝色或绿色。蓝绿色的 HSV 范围相对固定像绿血条可以设(35, 40, 40)到(85, 255, 255)。这里的关键是先看看实际画面的饱和度和明度再调参数不要一开始就追求标准色环。3.3 识别后的点击节奏不急着点先稳住识别到目标后如果立刻点击经常因为画面还在滚动或弹窗尚未完全展开而点偏。常见做法是点击前加一个小随机延迟让操作更像人手工点击的节奏。这个延迟不需要很大0.1到0.3秒就够。同时每次点击之间保持至少 0.5 秒的等待这样既能减少误触也方便日志定位。import random import time def safe_click(x, y): time.sleep(random.uniform(0.1, 0.3)) pyautogui.click(x, y) time.sleep(random.uniform(0.5, 1.0))把点击包装成safe_click还有一个好处如果需要加音效提示或日志只需要改这一个函数。后面的状态机里所有操作都走这个函数避免在流程代码里散落大量的pyautogui.click。4. 状态机组织主流程从启动到重复战斗的骨架有了前两章的坐标换算和图像识别已经能完成“看到按钮就点击”这个动作。但真实场景是一条链启动游戏、等待加载、点开始、对局结束、领取奖励、再开下一局。如果把这些顺序写在一坨if else里任何一个环节出现延迟或弹窗脚本都会像喝醉一样乱走。状态机能把流程切成一个个离散状态每个状态只负责一件事切换条件由图像识别结果决定。4.1 为什么用状态机而不是长串if直接看一个反例如果按线性流程写点击“开始”后会 sleep 20 秒然后默认进入对局可实际可能因为网络卡顿还在加载页也可能 15 秒就进对局了。等 20 秒必然错。状态机的思路是不在点击后盲目等待固定时间而是循环检测画面状态一旦看到“对局开始”的特征就切换。这样不管网络快慢脚本都能对齐真实画面。状态不要定义得太细我通常分成五个核心状态初始化、找按钮、点按钮、对局中、结算。每个状态里再处理各自的重试逻辑。class BotState: INIT INIT FIND_START FIND_START CLICK_START CLICK_START IN_BATTLE IN_BATTLE REWARD REWARD ERROR ERROR这里的REWARD状态是结算后的领奖动作领完后可以直接回到FIND_START形成循环。ERROR统一接收超时和识别异常避免脚本无声退出。4.2 状态机骨架每个状态只做一件事代码的关键在于run_once每次只处理一个状态状态内部不阻塞长时间循环。以下是核心骨架class GameBot: def __init__(self, cfg): self.cfg cfg self.state BotState.INIT self.retry_count 0 self.hwnd None self.rect None def run_once(self): if self.state BotState.INIT: self.hwnd find_window_by_title(self.cfg[window_title]) self.rect get_window_rect(self.hwnd) self.state BotState.FIND_START elif self.state BotState.FIND_START: pos match_template_on_window(self.rect, self.cfg[start_btn_template]) if pos: self.start_btn_pos pos self.state BotState.CLICK_START else: self.retry_count 1 if self.retry_count self.cfg[max_retry]: self.state BotState.ERROR self.retry_count 0 elif self.state BotState.CLICK_START: x, y, w, h self.start_btn_pos safe_click(x w // 2, y h // 2) self.state BotState.IN_BATTLE elif self.state BotState.IN_BATTLE: if has_exit_reward(self.rect): self.state BotState.REWARD elif self.state BotState.REWARD: click_reward(self.rect) self.state BotState.FIND_START elif self.state BotState.ERROR: log_error(self.state) self.state BotState.INIT注意一个细节CLICK_START并不直接切换到等待画面而是切到IN_BATTLE然后在IN_BATTLE里通过检测再进入REWARD。这样即使对战结算延迟也会一直停留在IN_BATTLE等待而不是误跳到下一步。4.3 轮询节奏、超时与状态回退主循环非常简单但要注意每次run_once和下一次之间要 sleep。如果 sleep 太短CPU 占用高也不会提高识别速率如果太长可能错过结算按钮的短暂出现。我一般设0.3到0.5秒。bot GameBot(cfg) while True: try: bot.run_once() time.sleep(0.4) except Exception as e: logging.exception(主循环异常: %s, e) bot.state BotState.ERROR time.sleep(1)在FIND_START里如果连续max_retry次都找不到按钮就切到ERROR然后再回到INIT重新找窗口。这里有一个容易翻车的点INIT里调SetForegroundWindow会把游戏窗口再次置前如果你同时在电脑上做别的事会一直被顶掉。所以我只在脚本启动时执行一次窗口初始化和激活进入循环后不再手动置前除非ERROR后重新恢复。另外强烈建议加一个紧急停止键。虽然 pyautogui 本身有 FailSafe 机制把鼠标甩到屏幕左上角会触发异常但有时脚本跑起来手根本追不上鼠标。用 keyboard 库监听按键更直接import keyboard if keyboard.is_pressed(esc): break把这段放在主循环开头一旦按 Esc 就退出循环。注意要在脚本开始前 try import keyboard某些精简环境没有这个库。5. 避坑与常见问题坐标偏移、匹配失败和脚本卡死这部分是我在实际复现和修改脚本时踩得最多的四个坑每一条都按“现象→原因→解决”记录可以直接对照排查。5.1 换分辨率后模板匹配全部失效现象脚本在 1920×1080 的模拟器窗口里识别“开始战斗”很稳定换到 2560×1440 的电脑上同样的模板匹配不到偶尔还会匹配到错误区域。原因模板的像素分布是固定的分辨率提高后按钮被整体放大模板和截图的尺寸比例不一样相似度自然下降。TM_CCOEFF_NORMED对缩放没有不变性所以不能指望同一张模板通吃所有分辨率。解决不要硬扛。先在配置里保存基准分辨率比如 1920×1080启动时读取当前窗口宽度rect[width]计算缩放比例scale width / 1920然后用cv2.resize把模板等比例缩放后再去匹配。注意同时调整阈值缩放后的模板边缘会略模糊阈值可以从 0.9 降到 0.85。如果游戏支持固定窗口尺寸直接把模拟器窗口设成固定宽度再跑更省事。5.2 点击位置永远偏离按钮一点现象模板匹配到的位置看起来没问题但点击总是落在按钮边缘甚至点成旁边的按钮。原因最常见的是少加了窗口左上角偏移。模板匹配得到的(x, y)是相对截图的局部坐标而pyautogui.click需要屏幕绝对坐标漏掉rect[left]和rect[top]就会整体偏移。还有一种是点击坐标用了模板左上角没有加上模板宽度的一半。解决把坐标换算单独抽成函数比如第 2 章的local_to_screen所有点击只有这一个入口。每次修改坐标逻辑都直接改这个函数不要在其他地方重复计算。另外点击前打日志把局部坐标、窗口坐标、绝对坐标都打印出来一旦点偏能立刻看出来是哪一步漏了。5.3 跑了几分钟突然乱点现象脚本刚开始能正常循环但运行一段时间后突然点开聊天框或商店甚至点到屏幕边缘空白区域。原因游戏在长时间运行后弹出了活动、组队邀请或网络重连提示这些弹窗遮挡了本来要识别的按钮同时弹窗上某些图标和模板长得接近导致模板匹配找到一个高相似度但实际错误的位置。另一种可能是加载阶段画面变成大面积暗色但匹配算法在噪声中找了一个最大值。解决提高模板匹配阈值把 0.85 改成 0.92 以上减少误匹配。更有效的是增加前置条件识别比如在点击“开始”之前先确认主界面的某个固定图标存在确认后再找“开始”。此外可以加一个加载页判断如果截图整体平均亮度很低说明正在加载中这时不执行任何点击等待画面亮起再继续。5.4 脚本卡死鼠标完全失控现象脚本运行中鼠标自己乱动按任何按键都没反应只能打开任务管理器强制结束进程非常狼狈。原因pyautogui 的点击循环写得过密没有给系统响应时间或者是FailSafe机制被触发后没有正确处理。另一个隐蔽原因是 DPI 感知没开鼠标移动到屏幕上的真实位置和脚本计算的位置不一致导致越点越乱。解决给 pyautogui 设置全局间隔pyautogui.PAUSE 0.3保证每次操作之间至少间隔 0.3 秒。同时把FAILSAFE保持默认 True鼠标甩到左上角可以强制中断。脚本里每次点击都包在safe_click中并且主循环监听 Esc 键退出。从那次以后我再也不写裸pyautogui.click了所有点击都要经过带日志和延迟的封装。6. 进阶把脚本封装成可配置的自动化工具基础流程跑通之后你会发现每次换游戏模式或换账号都要改一堆代码。更实用的做法是把识别参数和流程步骤外置到配置文件里让脚本成为一个解释器而不是一堆写死的判断分支。我这里提供三个可以立刻用上的进阶思路。6.1 JSON配置模板路径、阈值和点击间隔不写死在代码里把窗口标题、模板路径、匹配阈值、重试次数、点击间隔都放进config.json。加载代码很直接import json with open(config.json, r, encodingutf-8) as f: cfg json.load(f)实际使用时在__init__里读取cfg[threshold]在FIND_START里读取cfg[start_btn_template]。以后切换不同游戏模式只需要复制一份 JSON不需要动 Python 代码。6.2 动作序列把流程数据化成可复用的步骤除了配置参数流程本身也可以数据化。比如定义一个steps列表每个元素是一个动作字典脚本按顺序执行steps [ {type: find_template, template: start_btn.png, threshold: 0.9, click: True}, {type: sleep, seconds: 1}, {type: find_color, lower: [0, 120, 70], upper: [10, 255, 255], click: True}, ]然后在主循环里遍历这个列表。这么做的好处是以后调整流程顺序不需要重新排版 if else只需要改 JSON 里的steps顺序。注意find_color的click字段要支持False如果只是检测状态不执行点击否则很容易因为旁边有同色物体点错。6.3 日志与回放出现问题时不用从头定位脚本出问题时最怕的是不知道刚才看到了什么。所以我在每个识别动作后都会写一条日志保留当前截图和识别结果import logging import cv2 logging.info(state%s pos(%s,%s) conf%.3f, bot.state, x, y, max_val) cv2.imwrite(flogs/{time.time()}.png, screen_bgr)截图保存在本地事后再看画面就能判断是弹窗遮挡还是阈值问题。如果需要更严谨的验证可以在自动点击之前先跑一遍“识别回放”只用截图做模板匹配把匹配框画在图上人工确认每个目标的位置正确后再放开自动点击。我从那以后每次写这类脚本都会强制先跑一遍坐标到截图的往返校验确认所有点击的绝对坐标都和识别框重合才敢把脚本交给别人用。这个习惯帮我省下了很多在游戏里鼠标失控后强杀进程的时间希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑