1. 从手动刷本到脚本托管:FGO-py 到底解决了什么问题
玩《命运/冠位指定》的朋友大概都有过这样的体验:活动期间为了搬空商店,同一个副本要重复刷上百次,每次操作都是点技能、选卡、等结算、再点下一把。一管体力刷完,手指酸了,眼睛也花了,关键是这个过程毫无乐趣可言,纯粹是机械劳动。FGO-py 就是冲着这个痛点来的——它是一个用 Python 写的自动化脚本,能够接管游戏中的重复性操作,让你把刷本这件事交给程序去跑。
我第一次接触这类工具的时候,心里其实是有顾虑的。市面上不少所谓的"游戏辅助"要么需要修改游戏文件,要么需要注入进程,风险高不说,还容易把账号搭进去。FGO-py 的思路不太一样,它走的是图像识别 + 模拟点击的路线,本质上就是"看屏幕、动鼠标",跟你自己坐在电脑前操作没有本质区别。它通过截取游戏窗口的画面,用 OpenCV 做模板匹配和图像处理,判断当前处于哪个界面,然后决定下一步该点哪里。整个过程不碰游戏内存,不改游戏数据,从技术路线上就规避了很多风险。
这个项目适合什么人用?我觉得有三类人特别合适。第一类是活动期间想省时间的上班族,每天体力有限,手动刷太累,交给脚本挂着就行。第二类是想学 Python 自动化但找不到练手项目的人,FGO-py 的代码结构清晰,涉及图像识别、窗口操作、配置管理等多个知识点,拿来当学习材料非常合适。第三类是对自动化测试感兴趣的技术爱好者,它用到的很多思路跟 UI 自动化测试是相通的,比如元素定位、状态判断、异常重试这些。
需要提前说明的是,这类工具的使用需要你自己权衡。游戏官方对自动化的态度各时期可能不同,使用前建议了解清楚相关规则。我写这篇东西的目的是分享技术实现和实操经验,不是鼓励大家去违反任何规定。技术本身是中性的,怎么用取决于你自己。
从技术栈来看,FGO-py 主要依赖这么几块:Python作为主语言,OpenCV负责图像处理,pywin32或类似库负责窗口操作和鼠标模拟,NumPy处理数组运算,配置文件用JSON或YAML管理。如果你打算用 Docker 来跑,还需要了解容器化的基本操作。下面我会把这些东西拆开来讲,从环境搭建到核心原理,再到实际使用中的各种坑,尽量让你看完就能上手。
2. 环境搭建:Python、OpenCV 和那些让人抓狂的依赖问题
2.1 Python 版本选择与虚拟环境隔离
装 Python 这件事听起来简单,但实际踩坑的人非常多。FGO-py 对 Python 版本有一定要求,太老的版本(比如 3.6 以下)可能不支持某些语法特性,太新的版本(比如刚发布的 3.13)又可能遇到第三方库还没适配的问题。我实测下来,Python 3.9 到 3.11这个区间是最稳的,兼容性最好,各种依赖库都有预编译的 wheel 包,不需要自己折腾编译。
安装的时候有个细节要注意:Windows 上装 Python 一定要勾选"Add Python to PATH",否则后面在命令行里敲python会提示找不到命令。如果你已经装完了才发现没勾,也不用重装,手动把 Python 安装目录和 Scripts 目录加到系统环境变量里就行。
虚拟环境这块我强烈建议用上。很多人图省事直接全局装依赖,结果不同项目之间版本冲突,搞得一团糟。用venv创建独立环境,每个项目一套依赖,互不干扰:
python -m venv fgo-env fgo-env\Scripts\activate激活之后命令行前面会出现(fgo-env)的标识,说明你已经在虚拟环境里了。后面所有 pip 安装操作都在这个环境下进行,不会污染全局。
2.2 OpenCV 安装:为什么你总是遇到 ModuleNotFoundError
ModuleNotFoundError: No module named 'opencv'这个报错我见过太多次了。很多人以为是没装,其实装了但装错了包名。OpenCV 在 pip 里的包名是opencv-python,不是opencv,也不是cv2。正确的安装命令是:
pip install opencv-python如果你需要额外的 contrib 模块(一些高级图像处理算法),可以装opencv-contrib-python。但 FGO-py 用到的功能基本都是基础模块,装普通的就够了。
安装过程中如果卡在下载或者编译,大概率是网络问题或者缺少编译工具。Windows 上一般直接下载预编译的 wheel 就行,不需要本地编译。如果 pip 下载慢,可以换国内镜像源:
pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后验证一下,在 Python 里跑import cv2然后print(cv2.__version__),能打印出版本号就说明成功了。如果还是报错,检查一下是不是在正确的虚拟环境里,或者是不是有多个 Python 版本导致装到了别的地方。
2.3 窗口操作依赖与屏幕分辨率适配
FGO-py 需要跟游戏窗口交互,这就涉及到窗口句柄获取、窗口位置计算、鼠标点击模拟这些操作。Windows 平台上通常用pywin32这个库,安装命令是pip install pywin32。装完之后可能还需要跑一下python Scripts/pywin32_postinstall.py -install来注册一些组件,具体路径根据你的 Python 安装位置调整。
屏幕分辨率这块是个大坑。FGO-py 的图像识别是基于模板匹配的,也就是说它预先截取了一些界面元素的图片作为模板,然后在游戏画面上找这些模板。如果你的游戏窗口大小跟模板截取时的分辨率不一致,匹配就会失败。所以游戏窗口的分辨率必须固定,不能随便拉伸。一般建议用窗口模式运行游戏,把窗口调整到脚本要求的分辨率,然后不要再动它。
我自己的做法是先把游戏窗口调好,然后用工具(比如 Windows 自带的截图工具或者 Python 的 mss 库)截一张全屏图,看看游戏区域在屏幕上的坐标范围,把这个信息填到配置文件里。这样脚本就知道该去哪里截图、该在哪里点击了。
2.4 Docker 方案:值得折腾但门槛不低
有人会问能不能用 Docker 跑 FGO-py。技术上可行,但实际体验取决于你的使用场景。Docker 的优势是环境隔离、部署方便,特别适合在服务器上长期挂机。但问题是,Docker 容器里要访问宿主机的游戏窗口,需要把显示设备或者 X11 转发配置好,这在 Windows 上比较麻烦,在 Linux 上相对容易。
如果你打算用 Docker,基本思路是:基础镜像选一个带 Python 的 Linux 镜像,把 FGO-py 的代码和依赖装进去,然后通过挂载的方式让容器能访问到宿主机的显示服务。具体配置涉及DISPLAY环境变量、X11 socket 挂载这些,网上有相关教程可以参考。不过说实话,如果你只是在自己电脑上跑,直接用本机 Python 环境更简单,没必要为了 Docker 而 Docker。
提示:Docker Desktop 在 Windows 上安装时可能提示 "Virtualization support not detected",这是因为 BIOS 里的虚拟化功能没开。重启进 BIOS,找到 Intel VT-x 或 AMD-V 选项,设为 Enabled 就行。
3. 图像识别是怎么"看懂"游戏界面的
3.1 模板匹配:找图定位的核心逻辑
FGO-py 判断当前界面状态,靠的是模板匹配。原理说起来不复杂:你事先准备好一张小图(比如"攻击"按钮的截图),然后让 OpenCV 在大图(当前游戏画面)里滑动这个模板,计算每个位置的相似度,相似度最高的位置就是模板出现的位置。
OpenCV 提供了cv2.matchTemplate函数来做这件事,常用的匹配算法有TM_CCOEFF_NORMED、TM_CCORR_NORMED等。FGO-py 一般用归一化相关系数匹配,因为它对亮度变化不那么敏感,结果也更直观——返回值在 0 到 1 之间,越接近 1 表示越相似。
实际使用中,你需要设定一个阈值,比如 0.8,只有相似度超过这个值才认为匹配成功。阈值设太高容易漏检,设太低容易误检。我自己的经验是,对于按钮类元素,0.85 左右比较合适;对于文字类元素,因为字体渲染可能有细微差异,可以适当降到 0.75 到 0.8。
import cv2 import numpy as np def find_template(screen, template, threshold=0.8): 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] return (max_loc[0], max_loc[1], w, h) return None这段代码就是模板匹配的最小实现。max_loc是匹配位置左上角的坐标,加上模板的宽高就能得到完整的矩形区域,后续点击就点这个区域的中心。
3.2 多尺度匹配与分辨率兼容
前面说了分辨率要固定,但有时候你换了电脑或者改了窗口大小,模板就对不上了。这时候可以用多尺度匹配来救急:把模板缩放到不同尺寸,分别去匹配,取最好的结果。代价是计算量成倍增加,速度会慢下来。
def multi_scale_match(screen, template, scales=[0.8, 0.9, 1.0, 1.1, 1.2]): best_match = None best_score = 0 for scale in scales: resized = cv2.resize(template, None, fx=scale, fy=scale) if resized.shape[0] > screen.shape[0] or resized.shape[1] > screen.shape[1]: continue result = cv2.matchTemplate(screen, resized, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(result) if max_val > best_score: best_score = max_val h, w = resized.shape[:2] best_match = (max_loc[0], max_loc[1], w, h) return best_match if best_score >= 0.8 else None这个方法在分辨率变化不大的时候管用,但如果缩放比例超过 20%,匹配精度会明显下降。所以最好的办法还是固定分辨率,多尺度匹配只作为临时方案。
3.3 颜色检测与状态判断
除了找图,FGO-py 还会用颜色检测来判断一些状态。比如判断某个技能是否可用,可以通过检测技能图标周围的颜色——可用时是亮色,冷却时是灰色。OpenCV 可以把图像从 BGR 转到 HSV 色彩空间,然后在特定色相范围内做掩膜,统计像素数量来判断颜色占比。
def check_color_ratio(image, lower_hsv, upper_hsv): hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, lower_hsv, upper_hsv) ratio = np.count_nonzero(mask) / mask.size return ratio这个方法比模板匹配更轻量,适合做快速的状态预判。比如先判断当前是不是在战斗界面,如果是再去匹配具体的按钮,能省不少计算。
3.4 图像预处理:让识别更稳的几个技巧
原始截图直接拿去做匹配,效果往往不够好。FGO-py 在匹配之前会做一些预处理,常见的有灰度化、二值化、边缘检测、降噪这些。
灰度化就是把彩色图转成灰度图,减少计算量,同时消除颜色波动的影响。二值化是把灰度图转成黑白图,突出轮廓。边缘检测(比如 Canny)可以提取图像中的线条信息,对于形状明显的元素效果很好。降噪(比如高斯模糊)可以消除截图中的随机噪点,提高匹配稳定性。
def preprocess(image): gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (3, 3), 0) edges = cv2.Canny(blurred, 50, 150) return edges预处理的选择要看具体场景。对于按钮类元素,灰度 + 高斯模糊通常就够了;对于文字类元素,可能还需要做二值化来增强对比度。我建议你先用原始图跑一遍,看看哪些地方识别不稳,再针对性地加预处理。
4. 脚本主循环:从截图到点击的完整链路
4.1 主循环的基本结构
FGO-py 的核心是一个不断循环的状态机。每一轮循环做这么几件事:截取当前游戏画面、识别当前处于哪个界面、根据界面状态决定下一步操作、执行操作(点击或滑动)、等待一段时间让游戏响应、进入下一轮。
这个循环的频率不能太高也不能太低。太快了游戏还没响应完就去截下一张图,容易误判;太慢了效率低下,刷一把要等半天。我实测下来,每轮循环间隔 0.5 到 1 秒比较合适,具体看你的电脑性能和游戏加载速度。
import time def main_loop(): while True: screen = capture_screen() state = recognize_state(screen) if state == "battle": handle_battle(screen) elif state == "result": handle_result(screen) elif state == "menu": handle_menu(screen) else: handle_unknown(screen) time.sleep(0.8)这个结构看起来简单,但实际写起来要考虑的东西很多。比如状态识别错了怎么办?点击没生效怎么办?游戏卡住了怎么办?这些异常情况都需要处理。
4.2 状态识别与优先级设计
游戏界面有时候会有重叠,比如弹窗盖在战斗界面上,或者加载动画还没结束。这时候如果只按单一条件判断,很容易误判。FGO-py 的做法是给每个状态识别设一个优先级,先检查高优先级的特征,匹配上了就按那个状态处理,不再往下查。
比如弹窗的优先级最高,因为弹窗不处理掉,后面的操作都会被挡住。其次是加载动画,检测到加载中就等待,不做任何操作。然后是战斗界面、结算界面、菜单界面这些。
def recognize_state(screen): if find_template(screen, templates["popup_close"]): return "popup" if find_template(screen, templates["loading"]): return "loading" if find_template(screen, templates["attack_button"]): return "battle" if find_template(screen, templates["result_ok"]): return "result" return "unknown"优先级的设计要根据实际游戏流程来定。我建议你把常见的界面都截一遍,然后按出现频率和阻塞程度排序,阻塞性强的放前面。
4.3 点击操作与随机化处理
点击看起来简单,调个 API 就行,但实际有不少讲究。首先是点击位置,不能总是点正中心,因为有些按钮的边缘区域可能不响应。一般点中心偏下一点比较稳。其次是点击速度,太快了游戏可能来不及响应,太慢了效率低。还有就是随机化——如果每次点击的坐标和时间间隔都完全一样,行为特征太规律,不太自然。
import random import pyautogui def click_at(x, y): offset_x = random.randint(-3, 3) offset_y = random.randint(-3, 3) pyautogui.click(x + offset_x, y + offset_y) time.sleep(random.uniform(0.1, 0.3))这个随机偏移的范围不要太大,3 到 5 个像素就够了,太大了可能点到按钮外面去。时间间隔的随机范围也要控制好,既要保证游戏能响应,又不能拖慢整体节奏。
4.4 异常处理与自动恢复
脚本跑久了总会遇到各种意外:网络波动导致加载失败、游戏弹了个没见过的公告、鼠标被别的窗口抢走了、电脑休眠了等等。这些情况如果不管,脚本就会卡在那里一动不动。
FGO-py 的异常处理策略是:检测到未知状态时,先尝试一些通用的恢复操作,比如点屏幕中央、按 ESC 键、点返回按钮。如果连续多次都无法恢复,就记录日志并暂停,等人工介入。
def handle_unknown(screen): global unknown_count unknown_count += 1 if unknown_count > 10: log("连续未知状态过多,暂停脚本") pause() return pyautogui.click(screen_center) time.sleep(1)这个unknown_count在识别到已知状态时要重置,否则正常流程中偶尔几次未知也会累积到阈值。
5. 配置管理与多账号适配的实操细节
5.1 配置文件的结构设计
FGO-py 的行为很大程度上由配置文件决定。配置文件里通常包含这些内容:游戏窗口的位置和大小、各种模板图片的路径和匹配阈值、点击的坐标和延迟参数、要刷的副本编号和次数、体力恢复道具的使用策略等等。
用 JSON 还是 YAML 看个人喜好。JSON 的好处是 Python 原生支持,不需要额外装库;YAML 的好处是可读性更好,支持注释。我一般用 JSON,因为解析速度快,而且不容易出现缩进错误。
{ "window": { "left": 0, "top": 0, "width": 1280, "height": 720 }, "thresholds": { "button": 0.85, "text": 0.75 }, "battle": { "max_turns": 3, "skill_delay": 0.5, "card_delay": 0.3 } }配置文件的结构要清晰,不同功能的参数分开放,方便查找和修改。我建议给每个参数加个注释说明(JSON 不支持注释的话,可以单独写个说明文档),不然过段时间自己都忘了某个参数是干嘛的。
5.2 多账号切换的实现思路
如果你有多个账号要刷,手动切换太麻烦。FGO-py 可以通过配置文件区分不同账号的参数,比如不同的窗口位置、不同的队伍配置、不同的刷本策略。切换账号的时候,改一下配置文件的路径或者内容就行。
更高级的做法是写一个调度脚本,按顺序启动多个 FGO-py 实例,每个实例用不同的配置。但要注意,多个实例同时跑可能会互相抢鼠标焦点,导致点击错位。解决办法是让每个实例操作不同的窗口区域,或者串行执行——一个跑完再跑下一个。
import subprocess accounts = ["account1.json", "account2.json", "account3.json"] for config in accounts: subprocess.run(["python", "fgo.py", "--config", config]) time.sleep(5)串行执行的好处是稳定,坏处是总时间长。如果你电脑性能好,可以试试并行,但一定要做好窗口隔离。
5.3 参数调优:延迟、阈值和重试次数
这三个参数是影响脚本稳定性和效率的关键。延迟太短,游戏响应不过来,操作丢失;延迟太长,刷本速度慢。阈值太高,识别不到元素;阈值太低,误识别。重试次数太少,遇到偶发问题就卡住;重试次数太多,真出问题了还在那死循环。
我的调优方法是:先用保守参数跑一遍,观察日志里哪些地方报错多,然后针对性地调整。比如某个按钮经常识别不到,就把它的阈值降一点;某个操作后游戏加载慢,就把那一步的延迟加长。
| 参数 | 保守值 | 激进值 | 建议 |
|---|---|---|---|
| 循环间隔 | 1.0s | 0.3s | 0.5-0.8s |
| 按钮阈值 | 0.9 | 0.7 | 0.8-0.85 |
| 文字阈值 | 0.85 | 0.65 | 0.75-0.8 |
| 重试次数 | 10 | 3 | 5-8 |
这张表是我自己总结的经验值,你可以根据实际情况微调。关键是每次只改一个参数,改完跑一段时间看效果,不要一次改一堆,不然出了问题都不知道是哪个参数导致的。
5.4 日志记录与问题回溯
日志是排查问题的命根子。FGO-py 应该记录每一轮循环的状态识别结果、执行的操作、耗时、异常信息。日志级别分 DEBUG、INFO、WARNING、ERROR,平时跑用 INFO 就行,出问题了临时开到 DEBUG 看细节。
日志文件要定期清理,不然跑几天就几百兆了。可以按天分割,只保留最近一周的。日志里最好带上时间戳和截图文件名,这样出问题的时候可以直接看当时的画面,比对着文字猜快多了。
import logging from datetime import datetime logging.basicConfig( filename=f"fgo_{datetime.now().strftime('%Y%m%d')}.log", level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s" )截图存档也很重要。我一般让脚本在识别失败的时候自动截一张图存下来,文件名带上时间戳和状态。这样回头分析的时候,能直观看到当时是什么界面、为什么识别错了。
6. 那些文档里不会写的踩坑经验
6.1 游戏更新导致模板失效
游戏每次更新,界面多多少少会有些变化。可能是按钮位置挪了几个像素,可能是颜色调整了,也可能是加了新的动画效果。这些变化对人是无感的,但对模板匹配来说是致命的——原来能匹配上的模板,更新后可能就匹配不上了。
应对方法是:每次游戏大版本更新后,重新截取一遍模板图片。如果只是小更新,可以先跑一下看看有没有报错,报错了再针对性更新。我习惯在游戏更新后先手动跑几把,确认脚本正常再挂机,不然挂一晚上发现啥也没刷就亏大了。
6.2 鼠标焦点被抢与窗口置顶
Windows 上有个烦人的问题:如果有别的窗口弹出来抢了焦点,你的鼠标点击就会点到那个窗口上去。脚本不知道这件事,还以为自己点的是游戏,结果操作全部落空。
解决办法有几个:一是把游戏窗口设为置顶,用pywin32的SetWindowPos函数可以做到;二是脚本每次点击前先激活游戏窗口,确保焦点在游戏上;三是跑脚本的时候别开其他会弹窗的软件,比如聊天工具、邮件客户端。
import win32gui import win32con def bring_to_front(hwnd): win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)置顶之后游戏窗口会一直显示在最前面,别的窗口盖不住它。缺点是你要干别的事的时候会被游戏挡住,所以最好用双屏或者把游戏放到副屏上。
6.3 体力耗尽与道具使用的判断逻辑
刷本是要消耗体力的,体力没了就得用道具恢复,或者等自然恢复。脚本需要判断当前体力够不够,不够的话是用道具还是停下来等。
判断体力值可以通过识别界面上的数字,但数字识别比较麻烦,容易出错。更简单的办法是看"开始战斗"按钮是不是亮的——体力够的时候按钮是彩色的,不够的时候是灰色的。用颜色检测就能判断。
道具使用策略要在配置文件里写清楚:优先用哪种道具、用多少个、用完了怎么办。我一般设置成优先用快要过期的道具,然后是用存量多的,最后才用稀有的。这个逻辑用简单的 if-else 就能实现,关键是把优先级排好。
6.4 长时间运行的稳定性问题
脚本连续跑几个小时甚至几天,可能会遇到内存泄漏、句柄耗尽、截图失败这些问题。Python 的垃圾回收机制大部分时候能处理好,但如果你在循环里不断创建大对象(比如全屏截图),内存还是会涨。
我的做法是:截图用mss库而不是pyautogui,因为mss更快而且内存管理更好;每跑几百轮就主动gc.collect()一次;定期重启脚本,比如每 4 小时重启一次。这些措施能显著提高长时间运行的稳定性。
还有一个容易忽略的问题是电脑休眠。如果电源设置里设了 30 分钟无操作就休眠,脚本跑着跑着电脑睡了,那就全白费了。记得把电源计划改成"从不休眠",屏幕可以关,但系统不能睡。
6.5 关于风险控制的个人建议
最后说几句掏心窝子的话。这类工具用起来确实省事,但风险是客观存在的。我的建议是:不要在主账号上跑,用小号先试试水;不要 24 小时不间断地跑,设置合理的休息时间,模拟正常人的游戏节奏;不要同时跑太多账号,行为特征太明显;关注游戏官方的公告和社区动态,有风吹草动及时停手。
技术本身没有对错,关键在于怎么用。我分享这些是因为我自己对自动化技术感兴趣,在这个过程中学到了很多图像处理和程序设计的知识。如果你也是抱着学习的心态来折腾,那这些经验应该对你有帮助。如果你只是想省事,那也要做好承担相应后果的准备。
我在实际使用中最大的体会是:稳定性比效率重要得多。一个跑得慢但稳定的脚本,比一个跑得快但经常卡死的脚本有价值得多。宁可每把多等两秒,也不要因为抢那一秒导致操作丢失、状态错乱。调参的时候也是这个原则,先保证稳定,再逐步优化速度。踩过几次坑之后你就会明白,挂机刷本最怕的不是慢,而是你早上起来发现脚本半夜就卡住了,体力满了一整晚都没用出去。