Python+ADB+OpenCV:手把手教你实现FGO自动战斗脚本
做FGO自动战斗脚本说穿了就是让程序替你完成三件事识别当前画面、做出操作决策、模拟手指点击。而Python恰好把这三种能力都打包好了配合ADB工具一套脚本就能接管原本需要手动重复几千次的刷本流程。这篇博文我从需求梳理讲到代码落地把环境配置、图像识别、状态机设计、坑位排雷全部过一遍目标是让有Python入门基础的读者也能自己搓出一个可用的自动战斗脚本。不过在动手之前我必须先把话说清楚这类脚本本质是模拟人工操作替代的是“盯屏幕、点按钮”这种重复劳动并不会去修改游戏内存、破解数据包所以它属于灰产边缘的辅助工具。它的教学价值在于帮你掌握PC与手机通信、图像识别、轮询状态机这类自动化通用技术。至于用不用、怎么用、在哪个号上用你自己权衡我只聊技术实现。1. 项目概览这个脚本到底在解决什么问题1.1 FGO战斗循环的机械化特征FGO作为一个回合制卡牌游戏它的战斗循环是极度固定的进入关卡、选助战、开场、选三张指令卡、看角色放技能、点宝具、结算掉落、再次进入。这个过程重复一百遍之后你会发现手指的记忆比大脑还快大脑早就放空了但手指还在机械地划拉屏幕。这种重复正是自动化的理想场景。它不像MOBA或FPS那样需要毫秒级反应和实时决策FGO每回合的思考时间理论上可以无限长就算脚本卡顿个两秒也完全不影响结果。而且游戏不强制要求3D渲染性能对截图识别的环境非常友好。所以项目的核心价值就两个字解放。把玩家从“今天又要刷多少把”的体力劳动里解放出来让你该上班上班、该睡觉睡觉脚本在那边自己跑。顺带还能避免你手动点了几百次之后手酸眼花的体验。1.2 脚本的自动化边界设定我在规划这个项目时给自己定的目标边界是能刷主线、能刷活动本、能做到无限循环。至于高难本、开荒剧情、需要复杂策略的副本不在脚本的第一版考虑范围内。为什么这样设边界因为FGO的高难本和剧情本往往需要针对不同的敌方配置做策略调整比如什么时候开嘲讽、什么时候留宝具、什么时候该用令咒这些逻辑如果全塞进脚本代码复杂度会翻好几倍。而主线刷本和活动周回本的流程高度模板化无外乎就是进本、选卡、打、结算脚本只需要在这个固定模板里做有限判断就够了。这个边界设定非常重要它决定了你后续代码的复杂度上限。如果你一开始就想全自动化到能打高难的级别写到一半很容易被各种边界情况劝退。反过来从小目标起步先把周回脚本跑通代码会清晰很多。1.3 适合谁参考这篇博文这个项目适合三类人。第一类是FGO玩家受够了手动刷本想搞个脚本但又不太清楚技术选型第二类是Python初学者学过基础语法但想知道这些语法能干什么实际的事这个项目就是一个完整的应用场景第三类是对手机自动化控制感兴趣的开发者不管目标是游戏还是App这套ADBOpenCV的方案都是通用基础。如果你是纯零基础、连Python都没装过这篇博文也能跟下来。我在环境搭建部分会把Python安装、ADB配置、依赖包安装这些步骤拆得很细你照着做就行。但如果你完全不懂代码逻辑那后边的脚本内容会有些吃力建议先补一补Python的基本语法。2. 技术方案选型为什么是PythonADBOpenCV2.1 主流自动化脚本方案对比做手机自动化控制业界有几种常见路线我分别评估过这里用表格直接对比方案开发难度稳定性依赖条件适用场景ADB命令模拟点击中高电脑或调试模式原生控制、通用性强Appium/Selenium高高需要安装较多组件App功能测试手机端自动化App如按键精灵低中手机端需开无障碍服务轻量录制回放PC模拟器自带脚本低中限定某款模拟器快速上手但绑定平台对于FGO这种需要截图判断画面的游戏ADB方案最合适。原因有三个第一ADB是安卓官方调试工具不依赖任何第三方软件权限层稳定第二电脑端Python写脚本非常灵活OpenCV处理图像识别比手机端方便太多第三ADB模拟的是系统层的触摸事件和人工点击在系统层面几乎无差别。2.2 为什么选OpenCV做图像识别图像识别是这个项目的关键一环。你可能会问直接用ADB获取控件树、按控件ID点击不行吗这是个好问题但FGO的UI是游戏引擎自绘的根本不在系统控件树里。ADB的dump命令拿不到游戏内部按钮的任何信息所以只能依靠看屏幕像素来定位。图像识别的主流方案有几种纯像素比对、特征匹配、模板匹配、深度学习目标检测。考虑到FGO场景固定、按钮样式不变模板匹配的性价比最高。OpenCV里的matchTemplate函数只要几十行代码就能实现在小图里找大图的任务不需要训练模型对CPU也友好。有想过用CNN或者YOLO这类深度学习方法但说实话杀鸡用牛刀了。模板匹配在环境光照不变的游戏截图上表现已经足够好而且深色背景、亮色按钮这种高对比场景匹配精度非常高。2.3 ADB内部原理入门ADB全称Android Debug Bridge直译过来是安卓调试桥。它就是一个客户端-服务端-守护进程的三层结构电脑上的adb客户端发起命令电脑后台的adb server服务转发手机里的adbd守护进程接收并执行把触摸事件注入到系统。这套机制意味着你在电脑上执行adb shell input tap 500 1200手机那头就能模拟一次真实的手指点击。速度上虽然比人类手指快不了太多但胜在稳定、可编程、不会累。还有一点需要说明ADB并不需要手机连接WiFi或者数据线只要满足两个条件就是手机开启了USB调试并且电脑能通过USB或局域网与手机的ADB服务通信。用模拟器开发时直接连模拟器的ADB端口就行了。3. 环境配置装好Python与ADB调试环境3.1 Python版本选择与安装Python的版本我记得是3.8到3.12都在活跃维护期这里建议直接装3.10或3.11。太老的版本没必要太新的版本如3.13刚出时可能会有第三方库还没适配的风险。下载地址可以直接去python.org选对应系统的安装包。安装时务必记得勾选Add Python to PATH这个选项否则你打开命令行输入python会提示找不到命令。这一步卡住了很多人后续什么代码都跑不了。装完之后验证一下打开命令行Windows按WinR输入cmd回车输入python --version如果能显示类似Python 3.11.5的字样说明安装成功。输入pip --version如果能显示pip版本说明包管理工具也没问题。3.2 ADB工具安装与模拟器连接ADB工具本身不用单独下载整个Android Studio只需要platform-tools这一个压缩包。下载后解压到任意目录把该目录加到系统PATH环境变量里命令行里就能用adb命令了。连接方式看你用真机还是模拟器。真机的话需要把手机的开发者选项打开一般在设置里连续点击版本号七次然后开启USB调试插上数据线。模拟器更方便每个主流模拟器都自带ADB接口像MuMu模拟器通常监听127.0.0.1:7555夜神是127.0.0.1:62001雷电是127.0.0.1:5555。连接命令是adb connect 127.0.0.1:7555这种格式。验证连接是否成功的命令是adb devices如果列表里出现device状态而不是offline或unauthorized说明一切正常。3.3 Python依赖库清单与国内源加速这个项目需要用的Python库其实很少核心只有两个pip install opencv-python pip install numpy如果你刚好还会用到一些额外功能比如日志记录、配置文件解析可以再加loguru和pyyaml但第一版完全不需要。遇到下载慢或者超时的情况可以换国内源。比如用清华源pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple速度会快不少。另外安装opencv-python包比较大如果等得太久可以中途观察一下是卡在下载还是真死掉了必要时重新执行一遍。提示OpenCV在Python里的导入写法是import cv2如果你的代码提示找不到模块先确认是否装的是opencv-python而不是opencv-contrib-python后者虽然功能更多但体积也更大。4. 核心细节实现识别、点击、状态机4.1 手机屏幕截图的正确姿势脚本要看游戏画面第一步永远是截图。ADB截图命令的坑不少我踩过好几次。标准的截图命令是adb shell screencap -p /sdcard/screen.png然后adb pull /sdcard/screen.png ./screen.png两步完成但速度慢因为需要先写入手机存储再拉取。推荐直接用adb exec-out screencap -p screen.png这个命令把截图二进制流直接写到本地省去了中间步骤。不过在Windows下有个坑控制台会把\n转成\r\n导致PNG文件损坏。解决方法是让Python的子进程以二进制模式读取import subprocess def take_screenshot(save_path): with open(save_path, wb) as f: subprocess.run( [adb, exec-out, screencap, -p], stdoutf, checkTrue )用subprocess.run的时候把stdout重定向到一个二进制文件对象不会有换行符问题。截图完成后用OpenCV读进来screen cv2.imread(save_path)内存里就有了可处理的图像。4.2 模板匹配定位按钮的具体实现模板匹配的思路非常简单用一张小的模板图比如攻击按钮的截图在全屏截图上滑动比较找出最相似的位置。OpenCV里对应函数是cv2.matchTemplateimport cv2 import numpy as np def find_template(screen, template, threshold0.85): 在截图中查找模板位置 返回中心点坐标找不到返回None 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] cx max_loc[0] w // 2 cy max_loc[1] h // 2 return cx, cy return NoneTM_CCOEFF_NORMED是归一化相关系数匹配法对光照差异不太敏感在游戏截图上表现很稳。阈值threshold0.85的意思是说只要相似度超过85%就认为找到了目标。阈值调太高容易漏检调太低容易误检。我在实际项目中一般先用0.8识别不到再逐步调低。模板图的制作方式是手动截图先用ADB截一张包含目标按钮的图然后用画图工具或者Python代码把按钮区域裁出来保存为单独的图片文件。注意模板图和运行时截图的分辨率要一致否则匹配会失效。4.3 模拟点击与滑动操作定位到按钮位置之后剩下的就是点击。ADB的点击命令是input tap x y。但有个细节要注意ADB命令每次执行都有几百毫秒的开销所以在循环里频繁调用时要控制节奏不能太快。import subprocess import time def tap(x, y, delay0.5): subprocess.run([adb, shell, input, tap, str(x), str(y)], checkTrue) time.sleep(delay)delay参数的作用是给游戏一点反应时间。FGO的战斗动画比较长在选卡后的攻击动画期间脚本不需要频繁操作此时可以把delay调大比如2到3秒节省CPU资源也降低操作频率。有时候需要滑动屏幕比如FGO的助战列表要往下滚动才能看到想要的助战。ADB滑动命令是input swipe x1 y1 x2 y2 durationduration是滑动耗时单位毫秒def swipe(x1, y1, x2, y2, duration300): subprocess.run([adb, shell, input, swipe, str(x1), str(y1), str(x2), str(y2), str(duration)], checkTrue)4.4 战斗状态机的核心设计逻辑自动脚本的骨架是一个状态机。我把它分成六个状态待机、进本、选卡、战斗动画、结算、重复。每一个状态对应一个检查函数和一个操作函数。脚本不断循环每次循环先检查当前处于什么状态然后执行对应的操作跳转到下一个状态。拿选卡举例。选卡状态首先要识别指令卡的位置。FGO的指令卡在屏幕下方一共五张每张有固定的初始位置。但为了保证识别准确我不直接用固定坐标而是先模板匹配一张蓝色指令卡模板找到第一张卡的位置然后根据每张卡的间隔推算出其他四张卡的位置。状态机的核心代码可以简化成class BattleState: IDLE 0 ENTER 1 SELECT 2 BATTLE 3 RESULT 4 RETRY 5 state BattleState.IDLE while True: if state BattleState.IDLE: state handle_idle() elif state BattleState.ENTER: state handle_enter() # 其他状态类似 time.sleep(0.5)每个handle_*函数返回下一个状态。这个模式的好处是逻辑清晰后续想加新的处理逻辑比如队伍HP低时吃药只需要新加一个状态就好不用改动其他状态的代码。4.5 超时保护与异常处理脚本跑久了总会有意外发生。比如网络卡顿、游戏弹出公告、遇到没见过的特殊剧情。如果没有超时保护脚本会卡在某一步一直死循环。我的经验是每个状态都要设置一个最大等待时间比如等待战斗结算界面最多等30秒。如果超时还没等到就截图保存到日志目录然后强制重启游戏。def wait_for_template(template, timeout30, interval1): start time.time() while time.time() - start timeout: screen take_screenshot(temp.png) pos find_template(screen, template) if pos: return pos time.sleep(interval) return None这样即使遇到未知情况脚本也不会彻底卡死最多是重启游戏后重新来过。对长时间挂机来说失败后自动重启远比失败了就停住靠谱。5. 完整实操从环境验证到跑通第一个脚本5.1 最小化Demo先让脚本看得见在写完整状态机之前我建议先跑一个最小化Demo验证三个核心环节都通截图、识别、点击。这个Demo做的事情很简单截一张屏在里面找攻击按钮找到了就点击它。import cv2 import subprocess import time def take_screenshot(save_path): with open(save_path, wb) as f: subprocess.run([adb, exec-out, screencap, -p], stdoutf, checkTrue) def find_template(screen, template_path, threshold0.85): 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] return max_loc[0] w // 2, max_loc[1] h // 2 return None # 运行一次 take_screenshot(screen.png) screen cv2.imread(screen.png) pos find_template(screen, attack.png) print(攻击按钮位置:, pos) if pos: subprocess.run([adb, shell, input, tap, str(pos[0]), str(pos[1])])把这个脚本跑起来如果控制台输出了坐标并且游戏里真的点击了攻击按钮说明三个核心环节全部打通后续写完整机器人就只剩状态逻辑的体力活了。5.2 模板图制作与目录结构建议模板图是整个识别准确率的关键。我的建议是打开游戏对应的界面用ADB截图然后用Python提取ROI区域import cv2 screen cv2.imread(screen.png) # 假设攻击按钮在屏幕右下角大致区域 roi screen[1400:1550, 850:1050] # 注意OpenCV是(y1:y2, x1:x2)顺序 cv2.imwrite(template/attack.png, roi)这样一个精确的模板就做好了。注意裁出来的区域不要太靠近边缘因为matchTemplate在边缘处的计算会受到边界效应的轻微干扰。项目目录结构我推荐这样组织fgo_auto/ ├── main.py # 主入口 ├── adb_helper.py # ADB操作封装 ├── vision.py # 图像识别封装 ├── battle_state.py # 状态机 ├── templates/ # 模板图片 │ ├── attack.png │ ├── continue.png │ ├── start_battle.png │ └── card_blue.png └── logs/ # 截图日志分文件的好处是方便调试。你在vision.py里单独测识别效果不需要把整个脚本跑起来才知道哪里出了问题。5.3 指令卡选择策略的简单实现FGO的指令卡有红、蓝、绿三种颜色颜色背后对应不同的战斗效果。脚本里的选卡逻辑可以非常简单优先选红卡因为红卡伤害最高如果红卡不够选蓝卡因为蓝卡攒NP宝具值效率高绿卡最后。识别指令卡颜色的方式还是模板匹配只不过准备三张模板红卡模板、蓝卡模板、绿卡模板。当前回合五张卡识别完之后按优先级排序选出前三张def select_cards(screen): card_priority {red: 0, blue: 1, green: 2} cards [] for i in range(5): # 假设每张卡的位置是等距的先算出中心点 card_x CARD_START_X i * CARD_STEP_X card_y CARD_Y # 提取卡片区域 card_img screen[card_y-50:card_y50, card_x-35:card_x35] # 判断颜色简化版用卡面主色调判断 if match_color(card_img, red): cards.append((red, card_x, card_y)) # ... 其他颜色 cards.sort(keylambda c: card_priority[c[0]]) return cards[:3]如果是真的想玩得细可以用颜色直方图或者HSV色彩空间分析来做颜色判断比多套模板更灵活。FGO指令卡的红蓝绿配色区分度很高HSV的H通道会分别落在不同区间判断很稳定。5.4 战斗结算与循环流程的实现打完一场战斗之后游戏会弹出结算界面上面有Continue或者下一步按钮。这之后通常会回到地图界面或者直接进入下一场战斗。你的脚本需要识别战斗结束的提示然后判断是继续进本还是停。我实现的方案是这样战斗开始前记录一个目标次数每完成一场减一跑完了直接退出没跑完就继续点出击进入下一轮。target_rounds 10 completed 0 while completed target_rounds: # 找出击按钮 pos wait_for_template(templates[start_battle.png], timeout15) if pos: tap(*pos) time.sleep(3) # 等待选卡界面 cards wait_for_cards(timeout20) if cards: # 选卡、攻击 ... completed 1这个循环的退出条件很关键如果脚本卡在战斗里一直出不去completed不会增加很快会因为超时进入异常处理。我的空制条件是每等一轮都要检查completed有没有变化连续三轮无变化就判定异常先截图保存再重启游戏。5.5 日志记录与健康监控长时间挂机最怕的是什么是脚本半夜死掉你早上醒来才发现。所以日志记录很重要我建议每个关键节点都打一条日志写入文件import logging logging.basicConfig( filenamefgo_auto.log, levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s ) logging.info(第 %d 场战斗开始, completed 1)日志里只需要记录状态变化、识别结果、异常事件这些关键信息。这样第二天起来打开日志文件一眼就能看出半夜几点发生了什么。如果想更进一步可以加一个看门狗机制用一个独立线程检查主循环的心跳如果主循环卡住超过X分钟没有心跳直接杀掉主进程重启。这个属于锦上添花第一版可以不做。6. 常见问题与排查技巧实录6.1 ADB连接不稳定、设备掉线挂机脚本跑久了ADB设备偶尔会掉线这基本是不可避免的。掉线原因通常是USB供电不稳或者模拟器服务崩溃。解决方法有两个层面。第一层是软件层面在脚本里每执行一段操作后检查adb devices的返回结果如果设备不在线自动尝试重新连接。对模拟器来说重新执行adb connect基本都能恢复。第二层是物理层面真机用户换一根质量好、线材粗的数据线最好直接插电脑主板后置USB口供电更稳定。还有一个容易被忽略的坑adb devices显示unauthorized状态。这是因为手机的调试授权弹窗没被接受过或者授权被撤销了。解决办法是在手机上重新插拔数据线并把USB调试的授权记录清掉重新授权。6.2 模板匹配识别率低、找不到按钮模板匹配找不到目标最常见的原因是分辨率不一致。模板图是用某个分辨率截的脚本运行时模拟器换成了另一个分辨率匹配相似度会断崖式下跌。解决办法是统一分辨率。要么固定模拟器分辨率要么让脚本启动时先动态缩放模板到当前分辨率。后者实现起来稍复杂我自己是直接用固定分辨率方案把模拟器分辨率锁死在初始化配置里。另外阈值也值得调试。如果你在界面上肉眼都能看到按钮但脚本匹配不到大概率是阈值设太高了。把threshold从0.9降到0.75试试。反过来如果频繁把背景误识别成按钮说明阈值低了。6.3 点击坐标偏移、点了没反应点击坐标不准的情况排查思路是先确认截图尺寸和真实屏幕分辨率是否一致。用adb shell wm size查看设备实际分辨率在代码里打印截图分辨率两相对比就知道有没有缩放。有的模拟器默认自带屏幕缩放功能UI显示尺寸和逻辑尺寸不一致。这时候ADB的input tap用的坐标系是物理像素坐标而你的截图分辨率可能是逻辑像素两者差了一个缩放系数。解决方法是把缩放系数算进去或者在模拟器设置里关闭自动缩放。6.4 脚本卡在某个界面无法继续状态机挂起的最大元凶是预期之外的弹窗。活动公告、好友请求、体力不足弹窗、版本更新提示这些都是FGO每隔一段时间就会出的意外界面。我的处理思路是建立兜底模板库把所有可能遇到的弹窗关闭按钮都截成模板放在templates/misc/目录下。状态机每个循环都会先检查一遍兜底模板发现就点击关闭保证主流程不被卡住for close_btn in misc_templates: pos find_template(screen, close_btn, threshold0.7) if pos: tap(*pos) time.sleep(1) break这样就算偶尔弹出新公告脚本也能自行处理。还有一种情况是游戏完全卡死、画面不动这时候只能靠超时保护来重启游戏没有更好的办法。6.5 关于封号风险的坦诚说明我必须诚实地说任何自动化脚本都有被检测的风险。FGO的运营方在用户协议里对手动/外挂行为有明确的限制条款脚本虽然模拟的是人工操作但操作频率、行为模式如果过于机械理论上存在被风控系统识别的可能性。如果决定使用脚本我的建议是控制使用频率不要24小时全天候挂机不要在公告或社区里宣扬自己用脚本优先使用小号测试确认稳妥后再考虑是否用于主号同时明确这是个人学习用途后果自负。我写这篇博文的目的是分享Pytho自动化、图像识别、状态机这些通用技术至于游戏内怎么用每个人都要为自己的行为负责。一个收尾的实际建议在我自己的实战体验里最值得留意的其实是脚本循环的呼吸感。所谓呼吸感是指操作之间要有适当的停顿不要像发了疯一样一秒点十下。真实玩家的操作是有节奏的进本后停顿两秒看看助战选卡时停顿半秒攻击动画结束后再停顿一下。把这些停顿编进脚本既降低了对服务端的请求频率也让整个脚本看起来更自然。从技术角度来看用Python做FGO自动战斗脚本这个项目麻雀虽小五脏俱全。你会在里面用到进程调用、图像处理、状态机设计、异常恢复、日志监控这些能力放到任何自动化领域都通用。跑通第一版之后你可以很自然地扩展换成别的游戏、加上OCR文字识别、接入微信通知推送战斗结果方向完全由你自己掌握。