
玩FGO玩到后期最累的其实不是打不过而是重复刷同一个本。活动期间每天要清体力、刷素材、打周回操作千篇一律进本、选好友支援、选卡、放宝具、结算、继续下一把。打到后面完全是肌肉记忆眼睛盯着屏幕手上点点点脑子里早就放空了。这个项目的出发点就是把这套机械操作交给代码。用Python写一个FGO自动战斗脚本核心思路不复杂截屏、看图、判断当前界面、模拟点击。本质上就是把看屏幕→做决策→点屏幕这个过程自动化。它不修改游戏内存数据不注入什么外挂模块就是用OpenCV做图像匹配用ADB把触摸事件发到设备上跟人手动操作的路径是一致的。这篇文章把整个项目的实现过程完整记录下来包括方案选型、环境搭建、核心代码、状态机设计、常见报错和稳定性处理经验。顺便说清楚边界脚本只做界面识别和模拟点击属于个人便利工具自己挂着清体力没问题但别拿去碰排行榜、竞速这类影响他人体验的场景风险心里有数就行。适合两类人看一是FGO玩家想省点重复劳动二是学Python的朋友这个项目作为OpenCV和自动化方向的练手非常合适麻雀虽小该有的点都有。1. 整体思路与方案选型1.1 自动战斗脚本到底要解决什么问题FGO的战斗循环说穿了就三步进图、打完、拿奖励。手动刷一把周回本出的指令无非是点击开始、选择支援、等待读条、放技能、选三张卡、等待战斗动画、点确认结算。整个过程没有任何智力含量纯粹是重复劳动但它占据了活动期间每天大量的时间。自动战斗脚本要做的就是把这套重复劳动拆成一串识别界面→执行动作的小任务。难点在于FGO的界面状态非常多有主线关卡选择、好友支援选择、战斗指令、技能确认、结算界面、材料掉落、AP不足的提示……每个状态对应的按钮位置和操作都不同。脚本得像人一样每时每刻先判断现在屏幕上是什么界面再决定下一步点什么。这里面有个边界要提前说清楚脚本只做界面识别和模拟点击不做任何数据篡改、加速、倍攻之类的行为。后者属于完全不同的技术路线风险和性质也完全不同。用图像识别加自动操作的方式从行为逻辑上和手动刷本是一致的这也是这个项目能作为技术练习对象的原因。1.2 技术选型为什么是Python OpenCV ADB这个方案不是唯一选项我一开始也对比过其他路线。Airtest和Appium这类UI自动化框架也能做但问题是它们主要为App测试设计让你写各种查找控件的逻辑。然而FGO这类游戏的界面绝大多数是自绘渲染根本拿不到控件的层级树绕到最后还是得靠截图识别框架反而成了负担。uiautomator2在部分安卓设备上能注入触摸事件但很多国产ROM权限管控严格容易失灵而且对模拟器的支持也不稳定。Python OpenCV ADB就简单直接得多。ADB是安卓官方调试工具只要开了USB调试几乎所有设备都能发触摸事件OpenCV负责图像匹配游戏界面不管是不是自绘渲染只要是显示出来的画面都能截屏识别Python负责把它们串起来写起来快调试也方便。这套技术栈在各类自动化脚本里算是最轻量、最通用的组合学会了不只在FGO上能用其他游戏的简单自动化甚至一些桌面工具的自动化也能迁移。具体的工具清单就是这些Python 3.9以上opencv-python图像匹配的核心库numpy处理图像数组运算Pillow偶尔做图像格式转换和预处理platform-tools里的adb.exe设备交互工具1.3 状态机把复杂流程拆成独立的小状态战斗流程看着长但拆开之后本质是一串互相独立的状态。状态机的概念听起来高大上其实说穿了就是给每个界面场景定义一个状态脚本主循环里先看当前处于什么状态再根据识别结果决定要不要切换到下一个状态。我最初写的版本是一长串if-else从进本点到结算一路写下来结果运行了十分钟就乱套。原因很简单战斗流程里分支太多了。可能没AP了、可能遇到好友支援刷新、可能队伍强度不够打成了一场长线战斗、可能网络波动多了一个加载界面。任何一个意外情况都会让线性的if-else彻底跑飞。后来改成状态机每个状态只关心我在这个界面应该做什么做什么之后能走到下一个状态。比如支援选择状态只做一件事在画面里找第一个支援角色并点击找到就切到战斗指令状态找不到就继续等。各状态之间互不干扰出问题的时候也能很快定位是哪个环节卡住了。这个改造是整个项目稳定性提升的关键一步。2. 环境准备与基础配置2.1 Python安装与环境变量这部分看起来基础但真的有很多人卡在这里。我见过同事在Windows下装完Python命令行输python还是提示找不到十有八九是安装时没勾选Add Python to PATH。安装时注意安装包里那个Add Python to PATH复选框一定要勾上。没勾的话装完之后手动把Python安装目录和Scripts目录加到环境变量里也行。装完在命令行执行python --version和pip --version能正常输出版本号就说明基础环境OK。这一步也是这个项目里最没有技术含量、但最不能跳过的部分。国内环境下装第三方库pip默认从官方源下载可能非常慢。我习惯在用户配置里把源切换成国内镜像一劳永逸。在用户目录下建一个pip.iniWindows或者pip.confLinux/macOS写入index-url指向国内镜像地址。临时用的话也可以每次用pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 包名。我当时用的清华的PyPI镜像速度和稳定性都不错。2.2 安装依赖库基础依赖就三个库命令行执行pip install opencv-python numpy pillowopencv-python是图像识别的核心后面所有模板匹配都靠它numpy是OpenCV依赖的数组运算库装OpenCV的时候一般会自动带上但显式装一遍能避免后续乱七八糟的版本问题pillow做图像格式转换时用得上比如把截图转成可以交给OpenCV处理的numpy数组。想确认装好没有进Python跑一下import cv2不报错就说明环境没问题。如果后面想调试图像匹配效果我建议再加一个matplotlib。它可以把匹配结果直接框出来画在图上比只看匹配数值直观太多。这个不是必须的但是调试阶段非常提升效率尤其是调整阈值和裁剪模板的时候。2.3 ADB连接与设备调试ADB是安卓官方的调试工具脚本和手机之间的所有交互都是通过它完成的。安装方式最简单的是直接下载官方platform-tools里面自带adb.exe解压后把路径加到PATH里。手机端需要开启开发者选项和USB调试。不同品牌的开启方式略有差异但基本都是设置、关于手机、连点版本号七次、进入开发者模式、打开USB调试。插上数据线之后手机会弹一个是否允许USB调试的授权框要手动点一下允许。命令行先跑adb devices能看到设备编号后面带device状态就说明连接成功。如果是用模拟器则先在模拟器设置里开启ADB调试然后adb connect 127.0.0.1:端口号连接。连接成功后第一件事是确认分辨率和DPI。FGO的UI是固定按照分辨率缩放的不同分辨率下按钮位置完全不同。为了保证后续模板匹配的准确率最好固定用一个分辨率我实测720x1280、1080x1920、1440x2560都可行但选定之后就不要中途改。用adb shell wm size和adb shell wm density可以查看当前设备的屏幕尺寸和DPI。3. 核心实现截图、识别与模拟点击3.1 截屏方案用adb exec-out而不是screencap自动战斗的第一步是让脚本看到屏幕。Android上通过ADB截屏有两种常见姿势adb shell screencap -p /sdcard/screen.png后再adb pull拉回本地或者直接adb exec-out screencap -p screen.png把截图字节流输出到本地。我强烈推荐第二种少了一次pull操作速度快很多对脚本的循环频率也有帮助。有个细节值得提醒早期版本的adb在Windows下用screencap重定向输出会因为平台差异在PNG文件头部多出几个字节导致图片无法正常解码。用exec-out的方式没有这个问题这也是我坚持用它原因。Python里用subprocess调用ADB把stdout读成bytes再交给OpenCV解码import subprocess import cv2 import numpy as np def capture_screen(): raw subprocess.run( [adb, exec-out, screencap, -p], capture_outputTrue ).stdout img np.frombuffer(raw, dtypenp.uint8) return cv2.imdecode(img, cv2.IMREAD_COLOR)这段代码是整个脚本的地基。返回的img是一个BGR格式的numpy数组宽高对应屏幕分辨率后面所有模板匹配都在这张图上进行。3.2 模板匹配的原理与关键代码OpenCV模板匹配的原理可以理解为拿着小图片在大图片上滑动每到一个位置计算一次相似度相似度最高的位置就是小图片出现的坐标。放到这个项目里大图片是当前屏幕截图小图片是从游戏里截出来保存好的按钮、界面元素。我用的是cv2.TM_CCOEFF_NORMED这个匹配方法它会输出一个从-1到1的值越接近1说明匹配度越高。一般来说干净界面上按钮的匹配值都在0.9以上所以初始阈值可以设在0.85左右。核心函数写成这样def find_template(screen, template_path, threshold0.85): template cv2.imread(template_path) h, w template.shape[:2] res cv2.matchTemplate(screen, template, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(res) if max_val threshold: return max_loc[0] w // 2, max_loc[1] h // 2, max_val return None, None, 0返回值是模板中心点的坐标。中心点坐标就是点击要用的坐标因为人点按钮也是点在中心。匹配不上就返回空调用方根据空值决定是继续等待还是做别的处理。写代码的过程中我发现一个很实际的问题同一台设备白天和晚上截屏因为游戏内背景亮度、界面加载动效不同同一个按钮的显示效果会有细微差异。所以模板最好是直接从自己设备上截并且截完做一次裁剪只保留按钮主体不要带太多背景。背景部分越少匹配的干扰就越少。3.3 模拟点击、滑动与按键的ADB封装ADB模拟点击的命令是adb shell input tap x y。为了让脚本代码简洁我把它封装成一个函数并且允许传入点击延迟避免动作太连贯触发游戏的反自动化检测逻辑import time import subprocess def adb_tap(x, y): subprocess.run([adb, shell, input, tap, str(x), str(y)]) time.sleep(0.3)滑动命令用adb shell input swipe格式是起点坐标、终点坐标、持续时间。游戏里基本只有两个地方用到滑动一个好友支援列表的滚动选择一个是要往下拉才能点到继续按钮的结算界面。按键命令用adb shell input keyevent最常用的是返回键keyevent编码是4。把截图、找模板、点击组合起来就得到一个高级函数这也是整个脚本里最常用的一个动作def click_if_found(screen, template_path, threshold0.85): x, y, score find_template(screen, template_path, threshold) if x is not None: adb_tap(x, y) return True return False这个函数的意思是屏幕上出现目标就点不出现就返回False。后面所有状态流转都建立在类似的函数上逻辑简洁也好排查。4. 战斗流程的状态机实现4.1 战斗流程拆解与状态定义在1.3节我说了用状态机控制整个流程这里给出具体落地。整套流程被我拆成这几个状态START战斗开始前的界面找开始战斗或出击按钮SUPPORT_SELECT进入好友支援选择界面选第一个支援LOADING等待剧情和资源加载这个状态不点击只检测加载是否结束BATTLE_INSTRUCTION战斗指令阶段处理选卡、技能、攻击BATTLE_ENEMY敌人行动阶段等待敌方动画结束BATTLE_RESULT结算界面点继续或收集奖励CHECK_AP检查AP是否足够不足则停止并记录日志每个状态对应一个处理函数统一返回下一个状态名主循环就是一张状态转移表state START while True: screen capture_screen() if state START: if click_if_found(screen, imgs/start_button.png): state SUPPORT_SELECT elif state SUPPORT_SELECT: if click_if_found(screen, imgs/support_first.png): state LOADING elif state LOADING: if click_if_found(screen, imgs/battle_ui.png): state BATTLE_INSTRUCTION elif state BATTLE_INSTRUCTION: state process_battle(screen) # ... 其他状态处理 time.sleep(0.5)这里最关键的设计是两个原则每个状态循环里都重新截屏保证决策基于最新画面每次只做一步动作动作之后马上重新截屏。状态与状态之间靠界面元素是否存在来迁移而不是靠计时器强行切换。这样即使某一步动作没生效也能在下一轮循环里再次尝试。4.2 选卡策略与随机化战斗指令阶段是脚本的核心。FGO战斗界面底部有5张指令卡每张卡需要选中然后点攻击按钮。识别指令卡的类型当然可以但非常繁琐更实用的方法是按位置划分5张卡均匀分布在屏幕底部固定区域我把这块区域分成5个等宽的分区每个分区中心点就是一张卡的位置。选哪3张是个策略问题。最省事的做法是从5张卡里随机抽3个不重复的位置点击。虽然不看卡面颜色输出不是最优但周回本本身难度低随机打基本都能过而且代码量小很多import random def select_cards(screen): h, w screen.shape[:2] card_width w // 5 cards_y int(h * 0.82) selected random.sample(range(5), 3) for idx in selected: x int((idx 0.5) * card_width) adb_tap(x, cards_y) time.sleep(0.2) click_if_found(screen, imgs/attack_button.png)后续如果想优化可以根据颜色判断卡的类型红卡整体偏红、蓝卡偏蓝、绿卡偏绿用OpenCV的HSV通道做颜色筛选然后优先选某一种颜色。但我实测下来普通周回本纯随机选卡的成功率已经有95%以上颜色优化的边际收益不高除非你要打高难本。4.3 技能和宝具的简化处理技能和宝具的判断是脚本复杂度的主要来源。FGO的技能按钮在战斗界面右侧宝具和攻击按钮在右下角。每个从者有三个技能状态可能是已释放或未释放用同一套模板匹配来处理就可以。我第一版实现完全没做技能判断只点攻击按钮和选卡已经能让一个中配队伍稳定刷大部分周回本。后来为了刷得更快加了一个优先放宝具的逻辑战斗指令阶段先检测右侧宝具按钮如果存在说明宝具值满了点一下再回到选卡逻辑。放技能的操作顺序容易踩坑必须先点技能按钮弹出确认再点确认按钮最后回到选卡。每个确认按钮都得有对应的模板否则脚本会把确认释放界面当成普通画面导致技能没放出去白白浪费一个回合。我的建议是第一版脚本不要做技能和宝具先把选卡打通的成功率跑到90%以上再考虑加这些复杂操作。4.4 超时兜底与异常恢复写到第四版的时候脚本最大的问题已经不是识别不到而是某个状态卡住了。卡住的原因五花八门网络波动带来的额外加载、好友支援列表多弹了一个公告、FGO出了个新的临时弹窗。为此我加了两层保护。第一层是状态超时。每个状态在进入时记录时间如果在当前状态停留超过预设上限比如LOADING最多等30秒、BATTLE_INSTRUCTION最多等60秒就强制做一次兜底操作优先尝试点屏幕中部的确定位置关掉弹窗再不行就按返回键超时控制逻辑里重置状态到START。这个策略虽然粗暴但覆盖率很高因为FGO绝大多数异常界面只有这两条出路。第二层是全局看门狗。脚本记录最近一次状态迁移时间如果超过3分钟没有任何状态变更直接重新执行一遍完整的回主页、进本流程。这两层机制配合起来能让脚本在无人值守的情况下撑过一整晚的连续刷本而不卡死。日志里也会记录每次兜底触发的位置和原因方便第二天复盘。5. 常见问题与排查实录5.1 ADB设备识别不到或者显示未授权这是新手遇到最多的一个问题我的排查顺序一般是这样现象原因解决办法adb devices没有输出adb不在PATH或驱动没装好确认adb路径确认USB已连接且手机处于解锁状态设备状态显示unauthorized手机上的USB调试授权弹窗没点允许重新插拔数据线在手机上点允许USB调试模拟器连不上端口不对或者模拟器ADB功能没开在模拟器设置里开启ADB调试查看对应端口后用adb connect连接连接后频繁掉线USB线质量差或者接口松动换一根短的好线或者改用模拟器加本地连接还有个经常踩的坑adb devices能看到设备但跑screencap一直超时。这个多半是设备端截图时CPU占用过高游戏本身很吃性能。解决方法是给模拟器分配更多核心和内存真机的话要保证游戏运行不卡顿。5.2 模板匹配不上或者误匹配匹配不上的原因七成出在模板和当前画面不一致分辨率变了、游戏UI版本更新了、按钮位置被其他元素挡住了。我的习惯是给脚本加日志输出某个模板频繁失效时直接把匹配分数打印出来低于0.7基本就是模板需要重新制作了。误匹配是另一个常见问题。比如开始战斗按钮和继续战斗按钮长得很像模板匹配会混淆。解决办法有两条一是提高阈值比如从0.85提到0.92二是给关键按钮准备多张模板一张截按钮本身一张截按钮加上一截独特背景的模板。匹配的时候只要任何一个模板匹配成功就算找到误判率会明显下降。5.3 脚本越跑越乱状态错乱这种情况多半是识别到但执行时机不对造成的。举例结算界面有一点延迟脚本以为已经回到主页点了开始结果点到的是刚才的战斗结束弹窗。解决思路是加防抖连续多次截屏都识别到同一元素才认为状态稳定再执行动作。我在代码里把识别一次改成连续识别三次间隔0.5秒三次都匹配才动作稳定性提升非常明显。日志也特别重要。我给脚本加了一个简单的log记录器每一轮循环记录当前状态、识别到的模板、匹配分数和点击坐标。后期排查的时候打开日志一眼就能看到脚本是在哪个状态、因为哪个模板没有匹配上而卡住。这个习惯建议所有做自动化脚本的朋友都养成。5.4 设备层面的稳定性细节几个没写进代码但实测非常影响稳定性的点手机或模拟器屏幕亮度要固定最好关掉自动亮度。亮度变化会直接影响模板匹配的分数。关掉通知栏的弹窗推送。游戏结算时如果上方弹出微信或系统通知会抬高模板匹配的干扰值容易误判。模拟器环境下尽量锁定一个分辨率和DPI不要跟着窗口大小自适应变化。脚本开始前手动把游戏放到主页让START状态从开始战斗按钮找起能跳过队伍编成等复杂前置。保证设备和电脑都不休眠。长时间运行的话USB供电和屏幕常亮都要处理好。6. 实测效果与扩展方向6.1 实测效果我自己在MuMu模拟器上720x1280分辨率用一支中配主线队伍做测试稳定运行了4个小时刷了大概120把周回本人工核对了结果成功119把唯一失败的一次是遇到网络重连弹窗看门狗没能正确恢复。纯随机选卡的效率比手动操作慢10%到15%因为手动玩会刻意选红卡和攒宝具脚本是随机点。但如果是上班挂着、通宵刷素材的场景脚本不用人管这个优势远超那点效率差距。AP用完之后的处理我暂时做成检测到体力不足弹窗就停止运行并记录日志没有接自动吃苹果的逻辑主要担心误操作把攒的珍贵道具吃了。6.2 后续还能怎么扩展这个脚本的框架其实很通用有兴趣可以继续往里加东西把模板匹配换成YOLO这类目标检测模型直接检测卡面颜色和按钮文字识别准确率更高对分辨率变化也更有鲁棒性。加一个OCR模块读取当前AP数值和素材掉落数量精确控制停止刷本的时机。用APScheduler做定时任务比如每隔几小时自动启动一轮脚本实现体力满了自己刷的全自动循环。多个模拟器实例同时跑每个实例独立运行一份脚本效率直接翻倍。最后提一句个人体会脚本这类东西自己用来省点事是好事但别去碰游戏里竞速、排行榜这些会明显影响其他玩家体验的玩法图个方便就好。刷出来的材料是虚拟的但通过这个项目学到的图像识别加自动控制这套方法是真的能迁移到别的自动化场景里的硬功夫。