1. 为什么我会去折腾FGO的自动化脚本
玩FGO(《命运/冠位指定》)的朋友都懂,这游戏什么都好,就是刷本太精污。无限池、素材本、狗粮本,一刷就是几百把,手指头点得比上班敲键盘还累。我大概是在日服某次无限池活动期间彻底破防的——连续三个晚上刷到凌晨两点,第二天顶着黑眼圈开会,当时就下定决心:必须把这玩意儿自动化掉。
市面上确实有一些现成的辅助工具,但要么是闭源的黑盒,要么收费,要么跑在模拟器上卡得不行。后来在GitHub上翻到了FGO-py这个项目,纯Python写的,开源,逻辑清晰,可以直接跑在本地,通过截图识别+模拟点击的方式来实现自动化。说白了,它就是一个“看屏幕→判断当前状态→决定点哪里→执行点击”的循环,跟人玩游戏的逻辑一模一样,只不过它不会累。
这篇文章适合谁看?如果你有Python基础,想自己动手搭一套FGO自动化方案,或者你对“图像识别+自动化操作”这个技术组合感兴趣,想拿FGO当练手项目,那这篇内容应该能帮你省下不少踩坑的时间。我会从整体设计思路讲起,然后拆解核心细节,再给出一套可复现的实操流程,最后把我遇到过的坑和排查方法整理出来。全程不涉及任何违规工具,纯粹是技术层面的分享。
2. 整体设计思路与方案选型
2.1 为什么选Python而不是按键精灵
很多人第一反应是用按键精灵或者AutoHotkey这类工具,录制一段操作然后循环播放。我一开始也试过,但很快就发现行不通。FGO的战斗界面虽然固定,但每次进入战斗后的卡牌分布、敌人配置、技能冷却状态都是动态的,纯靠固定坐标点击,遇到稍微复杂一点的副本就会翻车。比如你录制的是“点第一个技能→点第二个技能→点宝具”,但下一把如果第一个从者的技能在冷却,这个脚本就废了。
FGO-py的思路完全不同。它不依赖固定坐标,而是通过屏幕截图+模板匹配来识别当前界面处于什么状态,然后根据预设的策略决定下一步操作。这就好比你不是在背答案,而是在教一个机器人认字,它认识“攻击”按钮长什么样,认识“宝具”卡牌的颜色,然后自己判断该点哪里。Python在这件事上有天然优势:OpenCV做图像处理、Pillow做截图、pyautogui做鼠标控制,整个技术栈非常成熟,社区资源也多。
2.2 核心架构:截图→识别→决策→执行
整个脚本的运转逻辑可以拆成四个环节。截图环节负责以固定频率抓取游戏窗口的画面,通常用pyautogui.screenshot()或者mss库来实现,后者速度更快,适合高频率截取。识别环节是核心,用OpenCV的matchTemplate函数在截图中寻找预设的模板图片,比如“攻击按钮”“宝具卡”“技能图标”等,返回匹配位置和置信度。决策环节根据识别结果和当前战斗状态(比如回合数、从者血量、技能CD)来决定下一步动作,这部分是纯逻辑代码,也是你可以自定义策略的地方。执行环节就是调用pyautogui.click()在指定位置模拟点击,或者用pyautogui.press()模拟键盘操作。
这个架构的好处是解耦。截图和识别是通用的,决策逻辑可以随时改。比如你想换一个刷本策略,只需要改决策部分的代码,不用动底层的图像识别。我后来甚至把决策逻辑抽成了一个配置文件,用JSON描述“如果看到A就点B,如果看到C就点D”,改起来非常方便。
2.3 模板匹配的精度与性能权衡
模板匹配的精度直接决定了脚本的稳定性。FGO的界面元素其实挺规整的,按钮、图标、卡牌的位置和样式都比较固定,这给模板匹配提供了很好的条件。但有几个坑需要注意:分辨率必须固定,游戏窗口大小一变,模板就对不上了;缩放比例也要一致,Windows的显示缩放如果是125%或150%,截图出来的像素尺寸会变,模板匹配的置信度会大幅下降。我建议把游戏窗口固定在1280x720或者1920x1080,系统缩放设为100%,这样最省心。
性能方面,全屏截图+全图匹配是比较耗时的。我的做法是限定搜索区域。比如“攻击按钮”只可能出现在屏幕右下角,那我就只截取右下角那块区域去匹配,搜索范围小了,速度自然就上去了。实测下来,限定区域后单次识别循环可以控制在50毫秒以内,完全跟得上游戏节奏。
3. 核心细节解析与实操要点
3.1 环境搭建:从零开始配置Python运行环境
先把基础环境搭好。我推荐用Python 3.9或3.10,太新的版本有些库的兼容性还没跟上,太老的版本又缺少一些新特性。安装的时候记得勾选“Add Python to PATH”,不然命令行里调不出来。装完之后,用pip安装几个核心依赖:
pip install opencv-python pillow pyautogui numpy mssopencv-python是图像处理的核心,pillow用来处理截图和模板图片,pyautogui负责模拟鼠标键盘,numpy是OpenCV的依赖,mss是一个高速截图库,比pyautogui自带的截图快很多。如果你打算用ADB连接安卓设备或者模拟器,还需要装pure-python-adb,不过我个人更推荐直接跑在PC端的模拟器上,省去很多连接调试的麻烦。
注意:安装opencv-python的时候,如果之前装过其他版本的OpenCV,可能会冲突。建议先
pip uninstall opencv-python opencv-contrib-python清理干净再装。
3.2 模板图片的采集与处理
模板图片的质量直接决定了识别的准确率。采集方法很简单:在游戏里截一张完整的屏幕截图,然后用画图工具或者Python脚本把需要识别的元素裁剪出来,保存成PNG格式。比如“攻击按钮”就裁那个按钮的区域,“宝具卡”就裁卡牌的区域。裁剪的时候要注意留一点边距,不要把元素裁得太紧,否则匹配的时候容错率会很低。
我一般会为同一个元素采集多个模板,比如“攻击按钮”在普通状态和高亮状态下颜色不一样,那就存两张模板,匹配的时候只要有一张命中就算成功。另外,模板图片的尺寸要和实际显示尺寸一致,不要缩放。如果你在1920x1080下截的模板,换到1280x720的窗口下就用不了了。
处理模板的时候,可以用OpenCV做一下灰度化和二值化,减少颜色干扰,提高匹配速度。但也不是所有元素都适合二值化,比如宝具卡的颜色本身就是重要的识别特征,那就保留彩色。这个需要根据具体情况来试。
3.3 战斗决策逻辑的设计
决策逻辑是整个脚本的“大脑”。FGO的战斗流程其实很固定:进入战斗→选择技能→选择指令卡→结算→下一回合。脚本需要根据当前屏幕上的信息来判断自己处于哪个阶段,然后执行对应的操作。
我设计了一个简单的状态机,每个状态对应一个识别模板和一组动作。比如:
| 当前状态 | 识别依据 | 执行动作 |
|---|---|---|
| 战斗开始 | 出现“战斗开始”文字 | 等待2秒 |
| 技能选择 | 出现技能图标 | 按预设顺序点击技能 |
| 指令卡选择 | 出现三张指令卡 | 按预设策略选择卡牌 |
| 宝具确认 | 出现宝具卡 | 点击宝具卡 |
| 结算画面 | 出现“获得奖励”文字 | 点击继续 |
这个状态机的好处是逻辑清晰,每个状态只关心自己该做什么,不会互相干扰。你可以根据不同的副本需求,调整技能释放顺序和选卡策略。比如刷狗粮本的时候,我一般设置成“第一回合放自充技能→第二回合放宝具→第三回合补刀”,整个流程非常稳定。
3.4 模拟点击的精度控制
模拟点击看起来简单,但实际操作中有几个细节要注意。点击位置要尽量选在按钮的中心区域,不要点在边缘,否则容易误触。点击间隔要留够,一般设置0.3到0.5秒,太快了游戏反应不过来,太慢了效率低。鼠标移动可以用pyautogui.moveTo()先移动再点击,模拟真实操作,有些游戏会检测瞬间跳转的点击。
还有一个容易被忽略的点:焦点问题。如果游戏窗口没有获得焦点,模拟点击可能会点到其他窗口上。我的做法是在每次点击前先用pyautogui.click()点一下游戏窗口的标题栏或者空白区域,确保焦点在游戏上。或者用win32gui库直接把游戏窗口置顶并激活。
实操心得:如果你的模拟器支持“后台点击”或者“多开同步”,可以研究一下ADB方案,通过发送触摸事件来操作,不依赖鼠标焦点,稳定性更高。但配置起来稍微麻烦一点,适合有一定基础的朋友。
4. 完整实操流程与核心环节实现
4.1 第一步:固定游戏窗口与系统缩放
在开始写代码之前,先把环境固定下来。打开模拟器,把游戏窗口调整到一个固定大小,我习惯用1280x720,这个分辨率下模板匹配的速度和精度比较平衡。然后检查Windows的显示设置,把缩放改成100%。如果你用的是笔记本,外接显示器的话,注意两个屏幕的缩放要一致,不然截图出来的尺寸会变。
固定好之后,截一张全屏截图,用画图工具打开,确认一下游戏窗口在屏幕上的位置。记下窗口左上角的坐标和窗口的宽高,后面写代码的时候要用到。我一般会把游戏窗口拖到屏幕左上角,这样坐标计算最简单。
4.2 第二步:编写截图与模板匹配的核心函数
先写一个截图函数,用mss库来实现:
import mss import cv2 import numpy as np def capture_screen(region=None): with mss.mss() as sct: if region: monitor = {"top": region[1], "left": region[0], "width": region[2], "height": region[3]} else: monitor = sct.monitors[1] img = np.array(sct.grab(monitor)) return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)然后写模板匹配函数:
def match_template(screen, template_path, threshold=0.8): template = cv2.imread(template_path) result = cv2.matchTemplate(screen, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) if max_val >= threshold: h, w = template.shape[:2] center_x = max_loc[0] + w // 2 center_y = max_loc[1] + h // 2 return (center_x, center_y, max_val) return None这个函数返回匹配到的中心坐标和置信度。threshold是阈值,一般设0.8左右,太高了容易漏检,太低了容易误检。你可以根据实际情况调整。
4.3 第三步:搭建战斗循环与状态判断
主循环的逻辑大概是这样的:
import pyautogui import time def battle_loop(): while True: screen = capture_screen(region=(0, 0, 1280, 720)) # 检查是否在指令卡选择界面 card_match = match_template(screen, "templates/card_attack.png") if card_match: x, y, conf = card_match pyautogui.click(x, y) time.sleep(0.5) continue # 检查是否在技能选择界面 skill_match = match_template(screen, "templates/skill_icon.png") if skill_match: x, y, conf = skill_match pyautogui.click(x, y) time.sleep(0.5) continue # 检查是否在结算界面 result_match = match_template(screen, "templates/result_next.png") if result_match: x, y, conf = result_match pyautogui.click(x, y) time.sleep(1) continue time.sleep(0.2)这个循环会不断截图、识别、点击,直到你手动停止。实际使用的时候,我会加一个最大循环次数或者运行时间限制,避免脚本一直跑下去。
4.4 第四步:参数调优与实测记录
参数调优是个体力活,但也是最有意思的部分。我一般会先跑几把,观察脚本在哪些环节卡住了,然后针对性地调整。比如发现“攻击按钮”经常识别不到,就把阈值从0.8降到0.7,或者重新采集一张更清晰的模板。发现点击太快导致游戏没反应,就把time.sleep的时间从0.3秒加到0.5秒。
实测下来,在1280x720分辨率、系统缩放100%的环境下,单次识别循环的平均耗时在40到60毫秒之间,整个战斗流程(从进入战斗到结算)大概需要2到3分钟,跟手动操作的速度差不多,但胜在可以一直跑,不用休息。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 截图区域 | 1280x720 | 与游戏窗口一致 |
| 匹配阈值 | 0.75-0.85 | 根据模板质量调整 |
| 点击间隔 | 0.3-0.5秒 | 太快容易漏点 |
| 循环间隔 | 0.2秒 | 平衡性能和响应速度 |
| 最大运行时间 | 自定义 | 避免无限循环 |
注意:不同模拟器的渲染方式不一样,有些模拟器截图出来是黑屏,这时候需要开启模拟器的“兼容模式”或者换用ADB截图方案。我用的模拟器默认支持后台截图,所以没遇到这个问题,但如果你遇到了,可以试试在模拟器设置里把渲染模式改成“DirectX”或者“OpenGL”。
5. 常见问题与排查技巧实录
5.1 模板匹配失败怎么办
这是最常见的问题,表现就是脚本卡在某个界面不动了。排查思路分三步:先看截图,把当前屏幕截图保存下来,用画图工具打开,看看模板应该匹配的位置实际长什么样;再对模板,确认模板图片的尺寸、颜色、内容和实际显示是否一致;最后调阈值,如果模板没问题但就是匹配不上,把阈值降到0.6试试,如果还是不行,那可能是分辨率或缩放的问题。
我遇到过一次特别诡异的情况:模板在本地测试的时候能匹配上,但跑脚本的时候死活匹配不到。后来发现是模拟器在后台运行的时候,渲染帧率会降低,截图出来的画面有轻微模糊,导致匹配置信度下降。解决办法是把模拟器设置为“始终在前台运行”,或者把模拟器的帧率限制调高。
5.2 点击位置偏移怎么解决
点击偏移通常是因为截图坐标和屏幕坐标不一致。mss截图返回的是相对于截图区域的坐标,而pyautogui.click()需要的是屏幕绝对坐标。如果你截图的时候指定了region参数,那匹配到的坐标需要加上region的左上角偏移量,才是真正的屏幕坐标。
比如你截取的是(100, 100, 1280, 720)这个区域,匹配到的坐标是(500, 300),那实际点击的坐标应该是(100+500, 100+300) = (600, 400)。这个偏移量很容易漏掉,我一开始就踩过这个坑,脚本点得乱七八糟。
5.3 脚本运行不稳定,时好时坏
这种“玄学”问题最让人头疼。我的经验是,先排除环境因素。检查一下系统缩放是不是100%,游戏窗口大小是不是固定,模拟器有没有开垂直同步,后台有没有其他程序在抢焦点。这些外部因素排除之后,再去看代码逻辑。
还有一个可能是游戏本身的动画时间不固定。比如宝具动画有时候快有时候慢,如果脚本在动画还没结束的时候就点击,就会点空。解决办法是在关键操作之后加一个等待时间,或者用模板匹配来判断动画是否结束。比如宝具动画结束后会出现“继续”按钮,那就等这个按钮出现再点击,而不是固定等待几秒。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 脚本卡住不动 | 模板匹配失败 | 检查截图、模板、阈值 |
| 点击位置不对 | 坐标偏移未计算 | 加上截图区域的偏移量 |
| 时好时坏 | 环境不稳定 | 固定分辨率、缩放、焦点 |
| 点击没反应 | 游戏未获得焦点 | 点击前先激活窗口 |
| 截图黑屏 | 模拟器渲染模式 | 切换渲染模式或改用ADB |
| 识别速度慢 | 搜索区域太大 | 限定搜索区域 |
| 误点击频繁 | 阈值太低 | 提高阈值或优化模板 |
实操心得:我习惯在脚本里加一个调试模式,开启后会把每次截图和匹配结果保存到本地,方便事后分析。这个功能在排查问题的时候特别有用,比盯着屏幕看强多了。
5.5 如何让脚本更“聪明”一点
基础的模板匹配只能做“看到A点B”这种简单决策。如果你想让脚本更智能,可以引入一些简单的状态记录。比如记录当前是第几回合,根据回合数来决定放什么技能;或者记录从者的血量,血量低于某个阈值就优先放治疗技能。这些逻辑用Python写起来很简单,一个字典或者一个类就能搞定。
我后来还加了一个随机延迟,在每次点击前随机等待0.1到0.3秒,模拟真人操作的不确定性。虽然FGO本身对脚本的检测不算严格,但加一点随机性总归更稳妥。另外,我建议不要24小时不间断地跑,设置一个合理的运行时长,比如每次跑2小时就休息一下,既保护账号也保护电脑。
6. 一些个人体会和后续可以折腾的方向
这套方案我断断续续用了大半年,从最初的“能跑就行”到后来慢慢优化,现在基本可以做到无人值守刷完一管体力。最大的感受是,图像识别+自动化这个组合的通用性很强,不只是FGO,很多重复性的GUI操作都可以用类似的思路来解决。你学会了模板匹配、坐标计算、状态机设计这些技能,换个游戏或者换个软件,稍微改改就能用。
后续我打算试试把决策逻辑做成可视化配置,用JSON或者YAML来描述战斗策略,这样不用改代码就能调整刷本方案。另外也在研究用深度学习做更鲁棒的识别,比如用YOLO训练一个目标检测模型,直接识别屏幕上的各种元素,摆脱对模板匹配的依赖。不过那个工程量就大多了,暂时还在摸索阶段。
如果你也在折腾类似的东西,我的建议是先从最简单的场景开始,比如先让脚本能自动点“攻击”按钮,跑通了再逐步加功能。不要一上来就想做全自动,那样很容易被各种细节问题劝退。一步一步来,每解决一个问题,你对整个系统的理解就会深一层。