十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

自走棋日常任务自动化脚本:图像识别与状态机实战解析

自走棋日常任务自动化脚本:图像识别与状态机实战解析 我平时写脚本有个习惯先把能自动化的环节全列出来再一个个去啃绝不会为了写脚本而写脚本。最近花了两周时间搓了一套“一将成名自走棋”的日常自动化脚本从最初的鼠标模拟乱点到后来稳定的状态识别加任务调度中间踩了不少坑也把整个思路捋顺了。今天把这套东西从需求到落地完整拆一遍想自己动手搞游戏自动化或者对脚本架构感兴趣的朋友可以拿走当参考。1. 项目背景与核心需求解析1.1 为什么需要这套脚本“一将成名”这个自走棋玩法日常任务其实相当密集。每天上线要做的固定事情包括签到、领体力、刷日常对局、完成活动任务、收取挂机奖励等等。如果纯手动一套流程跑下来少说四十分钟而且大部分时间是盯着屏幕等加载纯属浪费精力。我搭这套脚本的出发点很简单把每天重复的、规则固定的操作交给程序把时间留出来干别的事。它不是什么外挂不修改游戏数据不读取内存完全走界面自动化路线就是模拟人手的点击操作。原理上和你用按键精灵写个自动点击脚本没有本质区别只不过在逻辑架构上做了更工程化的设计。这个脚本适合谁大概三类人。第一类是和我一样日常任务繁重、想解放双手的普通玩家。第二类是想入门自动化测试或者脚本开发的人这套脚本可以当成一个完整的案例来研究。第三类是对图像识别、状态机设计感兴趣的开发爱好者可以从里面看到一套实际运行中的调度逻辑是怎么组织的。1.2 自走棋日常任务的特性分析要把自动化做好首先得摸清任务本身的规律。我观察了几天总结出几个关键特征流程固定每日任务虽然多但每项操作的顺序和路径基本不变这意味着可以用状态机来管理整个执行流程。界面元素稳定按钮位置、图标样式在版本不变的情况下是固定的这给图像匹配提供了基础。存在随机等待加载、战斗、结算等环节的耗时不可预知必须加入轮询检测机制而不是死等固定时间。反异常要求高网络波动、弹窗、活动入口变化都可能打断正常流程脚本必须有错误恢复能力。针对这些特征我确定了几条设计原则状态机驱动而不是时间线驱动图像识别优先于坐标硬编码模块化拆分以便快速调整。2. 技术方案选型与工具准备2.1 图像识别方案模板匹配为何够用游戏自动化的核心难点是“知道当前在什么界面”。最开始我考虑过直接用坐标写死毕竟自走棋界面相对固定。但实际跑起来就发现问题了不同分辨率下坐标会偏模拟器偶尔弹出对话框会把界面整体顶下去硬编码玩法必死。后来换成了图像识别方案。我选的是OpenCV的模板匹配就是先把关键界面的特征图截下来存好运行时实时截屏拿截图去和模板做匹配。匹配度超过阈值就认为处于对应界面然后执行该界面下定义的操作。这里用的是最经典的归一化相关匹配算法通过计算模板在搜索图上的相似度得分来决定是否匹配成功得分通常在0到1之间设定一个合适的阈值就能把“像与不像”的界线切得比较干净。为什么不用深度学习或者更复杂的YOLO目标检测理由很简单成本高、收益小。模板匹配对固定界面的识别又准又快CPU跑起来毫无压力深度学习需要标注数据、训练模型对于两三个按钮的识别需求完全是大炮打蚊子。2.2 输入模拟与游戏窗口控制输入模拟选型上我比较过pyautogui和pydirectinput这两个库。pyautogui用起来简单但它模拟的是系统层面的鼠标事件部分游戏会做输入过滤pydirectinput直接调用DirectInput接口兼容性更好尤其适合游戏场景。我最终选了pyautogui做基础版本因为自走棋操作不涉及高速连点对输入精度的要求并不苛刻。窗口控制方面用pygetwindow按窗口标题定位游戏窗口把脚本的所有操作都限制在窗口坐标系内这样无论窗口拖到哪里坐标计算都不会乱掉。2.3 运行环境搭建整个项目依赖不算多核心就这几样Python 3.9 及以上版本OpenCV-Python负责图像识别Pillow负责截屏处理PyAutoGUI负责鼠标操作PyGetWindow负责窗口定位NumPy配合OpenCV做矩阵运算安装直接用pip批量装就行。我习惯先建虚拟环境再装避免把系统Python搞乱。如果你用的是Anaconda一条conda create -n yjc python3.9就能起环境然后pip install opencv-python pillow pyautogui pygetwindow numpy。这里有个小建议模板匹配对分辨率敏感不同分辨率下界面元素看起来可能一样但像素数值不同导致匹配度下降。所以我做了一版多分辨率适配方法是按检测到的屏幕分辨率加载对应的模板目录各目录里放的是同一套按钮在不同分辨率下的截图。3. 脚本架构设计与核心模块拆解3.1 四层架构调度、状态、识别、操作这套脚本的整体架构我分成了四层每层只干一件事互相之间通过接口通信。最上层是调度层负责读取任务列表按照优先级和冷却时间决定当前该执行哪个任务。中间是状态层用状态机维护整个执行流程游戏的每个界面都对应一个状态。再往下是识别层封装了截图、模板匹配、OCR这几个能力。最底层是操作层负责具体的鼠标点击、拖拽和键盘输入。这种分层的设计思路来自我写后端服务时的习惯。好处很直接修改某一层的实现不影响其他层。比如识别层从模板匹配换成YOLO上层完全无感操作层从pyautogui换成pydirectinput状态层也不用动。3.2 状态机的设计思路说到状态机这是整个脚本的骨架也是最值得细聊的部分。自走棋的游戏流程可以概括为主城 → 活动页 → 开始匹配 → 准备阶段 → 战斗阶段 → 结算 → 回到主城。每一个“屏”都是状态状态之间通过条件转移连接。我定义了几个核心状态MAIN_CITY主城、ACTIVITY_PAGE活动页、MATCHING匹配中、LOADING加载中、BATTLE战斗中、SETTLEMENT结算页。状态转移的触发条件有两类一是界面特征切换由识别层上报当前界面ID状态层根据预定义的状态转移表判断下一步二是超时触发比如匹配超过90秒还没进去就判定为匹配失败切换到取消匹配状态。这里有一个很多初写自动化脚本的人容易犯的错用sleep硬等界面切换。这种写法最大的问题是把不确定性当确定性处理网络卡一下加载就超时后面所有流程全乱套。状态机的做法是把“等待”抽象成轮询每0.5秒截一次图检测到目标特征才继续下一步。虽然写起来稍麻烦但稳定性提升是质的。3.3 任务调度优先级与冷却管理日常任务虽然多但天然存在优先级差异。比如高收益的限时活动如果上线时还没结束就得优先做而体力领取这种固定产出什么时候做都一样。我的调度器维持了一个优先队列每个任务注册时携带三个属性优先级权重、冷却时间、执行函数。调度器每秒跑一次心跳扫描所有任务把当前可执行且权重最高的任务取出来执行。执行完根据任务类型决定是否进入冷却。这个设计参考了操作系统的进程调度思路。虽然对于单机脚本来说有点重但好处是后续加新任务非常方便只要按接口注册一个任务对象调度器自动管理不需要改其他代码。3.4 日志与数据记录模块这个模块看起来不起眼但实际排障的时候帮了我大忙。脚本每执行一步操作都会记录一个结构化日志包含时间戳、当前状态、识别结果、操作类型和耗时。截图也会按轮次保存到本地方便事后复盘。数据记录方面每次完成一个任务都会把结果写进SQLite数据库。这样我可以统计每个任务的平均耗时、成功率、失败原因分布从而持续优化脚本的执行策略。4. 实操过程与关键脚本实现4.1 模板采集与预处理流程模板匹配的效果好坏七成取决于模板图的质量剩下三成才看算法参数。模板图的采集有一个标准流程我一般是这么做的先把游戏窗口调整到合适大小进入目标界面把窗口置顶然后截取整屏。用截图工具框选按钮区域保存为独立的模板文件。注意模板图必须包含按钮周边的几个像素作为上下文特征纯粹的按钮本体反而容易误匹配因为很多按钮长得差不多。采集完模板要进行预处理。第一是统一尺寸所有模板要么做scale统一要么在匹配时按比例缩放。第二是灰度化颜色信息在模板匹配中用处不大还容易被色差干扰转成灰度能提升匹配鲁棒性。第三是边缘增强我用的是Canny边缘检测后再做匹配对光照变化更抗干扰。模板文件我会按功能分类存放目录结构大致是这样的templates/ ├── common/ │ ├── confirm_button.png │ ├── close_button.png │ └── back_button.png ├── main_city/ │ ├── activity_icon.png │ ├── mail_icon.png │ └── task_icon.png └── battle/ ├── start_button.png ├── ready_button.png └── settle_confirm.png这样后续加新任务的时候只需要在对应目录里补充模板文件再在任务配置表里注册一下不用动核心代码。4.2 核心代码模板匹配与点击函数我先把匹配和点击封装成两个基础函数它们是所有上层操作的基石。匹配函数负责在截图中定位模板位置点击函数负责在指定位置执行点击。import cv2 import numpy as np import pyautogui import pygetwindow as gw def find_template(screenshot, template, threshold0.8): 在截图中定位模板位置 返回匹配区域中心坐标未匹配到则返回None # 转换为灰度图 gray_screenshot cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2GRAY) gray_template cv2.cvtColor(template, cv2.COLOR_RGB2GRAY) # 执行模板匹配 result cv2.matchTemplate(gray_screenshot, gray_template, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(result) if max_val threshold: h, w gray_template.shape center_x max_loc[0] w // 2 center_y max_loc[1] h // 2 return center_x, center_y return None def click_at(coords, delay0.3): 在指定坐标执行点击 coords为(x, y)元组 x, y coords pyautogui.moveTo(x, y, duration0.15) pyautogui.click(x, y) time.sleep(delay)一个完整的操作序列就是“截图 → 匹配 → 点击”的循环。比如领取体力思路就是先截图在主城界面上匹配体力箱子的图标找到就点它。def collect_energy(): 领取体力 game_window get_game_window() screenshot capture_window(game_window) energy_icon load_template(main_city/energy_icon.png) pos find_template(screenshot, energy_icon) if pos is None: log_warning(未找到体力图标可能不在主城界面) return False click_at(pos) time.sleep(1) # 在弹出的确认框中点击领取按钮 screenshot capture_window(game_window) confirm_btn load_template(common/confirm_button.png) confirm_pos find_template(screenshot, confirm_btn) if confirm_pos: click_at(confirm_pos) return True这个函数初版就是纯手写流程线一个任务一个函数。跑通之后我就发现了明显问题代码重复率太高状态判断和点击逻辑高度耦合加一个新任务要复制粘贴一大段。后来才重构出状态机的模式。这个重构过程让我体会到写自动化脚本前期别追求完美架构先把流程跑通根据痛点再逐步优化。一上来就设计大而全的框架大概率会陷入过度设计的泥潭。4.3 状态库实现界面感知与跳转状态库的核心是一个当前状态变量加一个更新函数。每次轮询时调用更新函数内部重新截屏并识别界面特征然后判断当前状态是否发生变化。class GameState: def __init__(self): self.current UNKNOWN self.last_update 0 def update(self): 刷新当前界面状态 game_window get_game_window() screenshot capture_window(game_window) # 按优先级依次匹配各状态的特征模板 for state_name, feature_templates in STATE_TEMPLATES.items(): matched False for tmpl in feature_templates: tmpl_img load_template(tmpl) pos find_template(screenshot, tmpl_img, threshold0.75) if pos: matched True break if matched: if self.current ! state_name: log_info(f状态切换: {self.current} - {state_name}) self.current state_name break self.last_update time.time()STATE_TEMPLATES是每个状态联动的特征模板列表。一个状态用多个模板做并列匹配是有原因的游戏加载中偶尔会有遮挡一个模板没匹配上还能靠另一个兜底。我用2-3个模板投票识别稳定性大幅提升。4.4 战斗阶段的处理策略自走棋的战斗阶段比较特殊操作层面不需要怎么介入主要考验的是脚本的稳定等待能力。战斗时长不固定快则一两分钟慢则五分钟中间还有玩家掉线重连等意外。我的做法是把战斗阶段拆成两个子状态先检测“战斗正在进行”的特征图标如果识别到就持续等待同时监控异常弹窗战斗结束的特征是“结算”按钮出现。这里有一个值得讲的参数轮询间隔。我一开始设的是0.3秒CPU占用率太高电脑都发烫。后来调整到0.8秒识别效果没差别资源消耗却降了一截。具体间隔还是要看你电脑配置我最终定了0.5秒作为平衡点既能快速响应又不至于太吃资源。4.5 主循环与异常重启机制所有模块拼起来后主循环就是一个标准的“心跳”def main_loop(): scheduler TaskScheduler() state GameState() running True while running: try: state.update() current_task scheduler.get_current_task(state.current) if current_task: current_task.execute() else: time.sleep(1) except Exception as e: log_error(f执行异常: {e}) recovery_action(state) time.sleep(2)异常重启机制的关键在于recovery_action。如果识别层连续3次返回未知状态说明发生了脚本预期外的情况比如弹了一个没见过的新活动窗口。这时候如果继续执行很可能会乱点我的策略是先按“ESC”键或点击通用关闭按钮尝试回到主城界面。如果还是不行就强制重置状态机回到主城待命。这个机制让我几个晚上挂着脚本的时候能睡个安稳觉。真正跑起来遇到的最大工程挑战其实是环境差异。同一套代码在这个模拟器上跑得好好的换到另一个模拟器上就开始各种不识别。后来定位清楚了是不同模拟器对窗口的渲染方式有细微差别导致截图的色彩和尺寸有偏移。解决方案也很朴素就是在多个环境里都采集一套模板运行时根据当前模拟器类型动态切换模板目录。5. 常见问题与排查技巧实录5.1 模板匹配误识别我碰到的第一个高频问题是误识别。一个典型的场景是主城界面的“任务”按钮和活动界面的“任务”按钮长得几乎一模一样模板匹配时经常搞混导致状态机跳到了错误分支。一开始我通过降低阈值试图解决结果适得其反阈值一低不相关的东西也被识别出来了。后来改用多模板投票策略每个界面状态绑定2到3个独立特征模板必须匹配到至少两个才确认状态。误识别率显著下降。排查这个问题的思路值得借鉴先检查模板图本身是否存在歧义区域再看阈值设置是否合理。我见过不少人的模板匹配直接拿全图标截图图标里的数字、角标都会干扰匹配裁剪模板时要把这些干扰元素剔干净。5.2 死等与超时配置脚本跑着跑着就不动了这类问题排查起来最费时间。原因通常是只写了匹配代码而没写超时容错。比如等待加载完成适配代码会无限循环地截图、匹配、再截图一旦游戏卡死或者网络异常导致界面一直不变化脚本就永久挂起。针对这个坑我给每个等待状态加了一个“超时跳转”配置超过设定秒数仍未检测到目标特征就触发异常处理。具体时长参考了日常体验匹配等待最长90秒加载等待最长30秒战斗检测最长5分钟。超时后先触发一次重试重试仍失败就把状态重置回主城。5.3 多开窗口坐标偏移脚本跑单窗口时一切正常一旦我开双号就不行了。排查发现是窗口位置偏移导致的。虽然脚本是按窗口坐标计算但截屏时的坐标系和操作时的坐标系不一致窗口稍微偏移点击就点歪。解决办法是先获取窗口的真实边界再建立坐标系映射。所有识别和点击全部转换到窗口内相对坐标不直接用屏幕全局坐标。def get_game_window(): 获取游戏窗口并建立坐标映射 windows gw.getWindowsWithTitle(一将成名) if not windows: raise Exception(未找到游戏窗口) win windows[0] if win.isMinimized: win.restore() win.activate() time.sleep(0.5) return { handle: win, left: win.left, top: win.top, width: win.width, height: win.height, } def to_window_coords(win, point): 将全局坐标转换为窗口坐标 x, y point return x - win[left], y - win[top] def to_global_coords(win, point): 将窗口坐标转换为全局坐标 x, y point return x win[left], y win[top]这样改造后窗口随意移动都不影响识别的准确性。5.4 常见问题速查表现象可能原因排查方法脚本启动后无反应未找到游戏窗口检查窗口标题是否匹配确认游戏窗口未最小化频繁误识别模板图裁剪不当重新裁剪模板移除数字、角标等噪声元素界面等待永久卡住缺少超时机制为等待逻辑增加超时配置超时自动重置点击位置偏移坐标系未统一统一使用窗口相对坐标避免直接使用全局坐标CPU占用过高轮询间隔过短调大轮询间隔从0.3秒调到0.5秒以上偶发识别失败模板过小或模糊适当增大模板匹配的搜索范围重新截取高分辨率图6. 脚本的合规边界与扩展空间关于这类脚本我觉得有必要说几句。游戏自动化的合规边界一直是个灰色问题不同游戏的态度也不同。就自走棋这种PVE为主的玩法日常自动化通常不会破坏其他玩家的体验但使用第三方工具仍然可能违反游戏用户协议。我的建议是自己研究、自己用别规模化传播更不要拿去做代练或者盈利。脚本这东西当成学习自动化的项目来研究价值远比直接“用”它更大。技术上的扩展空间其实不小。当前版本用的是模板匹配对于动态内容比如活动弹出框里变化的文字识别率不高。想进一步提升可以引入OCR来做文字识别或者用目标检测模型做更通用的元素识别。这又是一个大型工程了目前的方案足够轻量没必要过度投入。另外这套状态机加调度器的架构完全可以复用到其他自走棋游戏的自动化中。界面变了模板重截一套就行逻辑变了状态转移表改一下就行。核心骨架的复用价值很高。我在跑这套脚本的过程中最大的收获不是“日常任务全自动了”而是把“怎么把不确定的界面流转变成稳定的程序逻辑”这个问题真正想明白了。自动化不只是写代码更是对业务逻辑的深度拆解。如果你也想做类似的东西我建议从小任务入手先做一个自动领取每日奖励的脚本跑通整个链路再逐步加任务。一口吃不成胖子但慢慢来就能把整个流程养成一个成熟的自动化系统。
返回列表