☰
Python+ADB+OpenCV:手把手教你实现FGO自动战斗脚本
2026/10/5 7:48:55 网站建设 项目流程

做FGO自动战斗脚本,说穿了就是让程序替你完成三件事:识别当前画面、做出操作决策、模拟手指点击。而Python恰好把这三种能力都打包好了,配合ADB工具,一套脚本就能接管原本需要手动重复几千次的刷本流程。这篇博文我从需求梳理讲到代码落地,把环境配置、图像识别、状态机设计、坑位排雷全部过一遍,目标是让有Python入门基础的读者也能自己搓出一个可用的自动战斗脚本。

不过在动手之前我必须先把话说清楚:这类脚本本质是模拟人工操作,替代的是“盯屏幕、点按钮”这种重复劳动,并不会去修改游戏内存、破解数据包,所以它属于灰产边缘的辅助工具。它的教学价值在于帮你掌握PC与手机通信、图像识别、轮询状态机这类自动化通用技术。至于用不用、怎么用、在哪个号上用,你自己权衡,我只聊技术实现。

1. 项目概览:这个脚本到底在解决什么问题

1.1 FGO战斗循环的机械化特征

FGO作为一个回合制卡牌游戏,它的战斗循环是极度固定的:进入关卡、选助战、开场、选三张指令卡、看角色放技能、点宝具、结算掉落、再次进入。这个过程重复一百遍之后,你会发现手指的记忆比大脑还快,大脑早就放空了,但手指还在机械地划拉屏幕。

这种重复正是自动化的理想场景。它不像MOBA或FPS那样需要毫秒级反应和实时决策,FGO每回合的思考时间理论上可以无限长,就算脚本卡顿个两秒也完全不影响结果。而且游戏不强制要求3D渲染性能,对截图识别的环境非常友好。

所以项目的核心价值就两个字:解放。把玩家从“今天又要刷多少把”的体力劳动里解放出来,让你该上班上班、该睡觉睡觉,脚本在那边自己跑。顺带还能避免你手动点了几百次之后手酸眼花的体验。

1.2 脚本的自动化边界设定

我在规划这个项目时,给自己定的目标边界是:能刷主线、能刷活动本、能做到无限循环。至于高难本、开荒剧情、需要复杂策略的副本,不在脚本的第一版考虑范围内。

为什么这样设边界?因为FGO的高难本和剧情本往往需要针对不同的敌方配置做策略调整,比如什么时候开嘲讽、什么时候留宝具、什么时候该用令咒,这些逻辑如果全塞进脚本,代码复杂度会翻好几倍。而主线刷本和活动周回本的流程高度模板化,无外乎就是"进本、选卡、打、结算",脚本只需要在这个固定模板里做有限判断就够了。

这个边界设定非常重要,它决定了你后续代码的复杂度上限。如果你一开始就想全自动化到能打高难的级别,写到一半很容易被各种边界情况劝退。反过来,从小目标起步,先把周回脚本跑通,代码会清晰很多。

1.3 适合谁参考这篇博文

这个项目适合三类人。第一类是FGO玩家,受够了手动刷本,想搞个脚本但又不太清楚技术选型;第二类是Python初学者,学过基础语法但想知道这些语法能干什么实际的事,这个项目就是一个完整的应用场景;第三类是对手机自动化控制感兴趣的开发者,不管目标是游戏还是App,这套"ADB+OpenCV"的方案都是通用基础。

如果你是纯零基础、连Python都没装过,这篇博文也能跟下来。我在环境搭建部分会把Python安装、ADB配置、依赖包安装这些步骤拆得很细,你照着做就行。但如果你完全不懂代码逻辑,那后边的脚本内容会有些吃力,建议先补一补Python的基本语法。

2. 技术方案选型:为什么是Python+ADB+OpenCV

2.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按Win+R输入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'], stdout=f, check=True )

用subprocess.run的时候把stdout重定向到一个二进制文件对象,不会有换行符问题。截图完成后,用OpenCV读进来:screen = cv2.imread(save_path),内存里就有了可处理的图像。

4.2 模板匹配定位按钮的具体实现

模板匹配的思路非常简单:用一张小的模板图(比如"攻击"按钮的截图),在全屏截图上滑动比较,找出最相似的位置。OpenCV里对应函数是cv2.matchTemplate:

import cv2 import numpy as np def find_template(screen, template, threshold=0.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 None

TM_CCOEFF_NORMED是归一化相关系数匹配法,对光照差异不太敏感,在游戏截图上表现很稳。阈值threshold=0.85的意思是说,只要相似度超过85%,就认为找到了目标。阈值调太高容易漏检,调太低容易误检。我在实际项目中一般先用0.8,识别不到再逐步调低。

模板图的制作方式是手动截图:先用ADB截一张包含目标按钮的图,然后用画图工具或者Python代码把按钮区域裁出来,保存为单独的图片文件。注意模板图和运行时截图的分辨率要一致,否则匹配会失效。

4.3 模拟点击与滑动操作

定位到按钮位置之后,剩下的就是点击。ADB的点击命令是input tap x y。但有个细节要注意:ADB命令每次执行都有几百毫秒的开销,所以在循环里频繁调用时要控制节奏,不能太快。

import subprocess import time def tap(x, y, delay=0.5): subprocess.run(['adb', 'shell', 'input', 'tap', str(x), str(y)], check=True) time.sleep(delay)

delay参数的作用是给游戏一点反应时间。FGO的战斗动画比较长,在选卡后的攻击动画期间,脚本不需要频繁操作,此时可以把delay调大,比如2到3秒,节省CPU资源,也降低操作频率。

有时候需要滑动屏幕,比如FGO的助战列表要往下滚动才能看到想要的助战。ADB滑动命令是input swipe x1 y1 x2 y2 duration,duration是滑动耗时,单位毫秒:

def swipe(x1, y1, x2, y2, duration=300): subprocess.run(['adb', 'shell', 'input', 'swipe', str(x1), str(y1), str(x2), str(y2), str(duration)], check=True)

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, timeout=30, interval=1): 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'], stdout=f, check=True) def find_template(screen, template_path, threshold=0.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_y+50, card_x-35:card_x+35] # 判断颜色(简化版:用卡面主色调判断) if match_color(card_img, 'red'): cards.append(('red', card_x, card_y)) # ... 其他颜色 cards.sort(key=lambda 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'], timeout=15) if pos: tap(*pos) time.sleep(3) # 等待选卡界面 cards = wait_for_cards(timeout=20) if cards: # 选卡、攻击 ... completed += 1

这个循环的退出条件很关键:如果脚本卡在战斗里一直出不去,completed不会增加,很快会因为超时进入异常处理。我的空制条件是每等一轮都要检查completed有没有变化,连续三轮无变化就判定异常,先截图保存再重启游戏。

5.5 日志记录与健康监控

长时间挂机最怕的是什么?是脚本半夜死掉你早上醒来才发现。所以日志记录很重要,我建议每个关键节点都打一条日志,写入文件:

import logging logging.basicConfig( filename='fgo_auto.log', level=logging.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, threshold=0.7) if pos: tap(*pos) time.sleep(1) break

这样就算偶尔弹出新公告,脚本也能自行处理。还有一种情况是游戏完全卡死、画面不动,这时候只能靠超时保护来重启游戏,没有更好的办法。

6.5 关于封号风险的坦诚说明

我必须诚实地说,任何自动化脚本都有被检测的风险。FGO的运营方在用户协议里对手动/外挂行为有明确的限制条款,脚本虽然模拟的是人工操作,但操作频率、行为模式如果过于机械,理论上存在被风控系统识别的可能性。

如果决定使用脚本,我的建议是:控制使用频率,不要24小时全天候挂机;不要在公告或社区里宣扬自己用脚本;优先使用小号测试,确认稳妥后再考虑是否用于主号;同时明确这是个人学习用途,后果自负。我写这篇博文的目的,是分享Pytho自动化、图像识别、状态机这些通用技术,至于游戏内怎么用,每个人都要为自己的行为负责。

一个收尾的实际建议

在我自己的实战体验里,最值得留意的其实是脚本循环的"呼吸感"。所谓呼吸感,是指操作之间要有适当的停顿,不要像发了疯一样一秒点十下。真实玩家的操作是有节奏的,进本后停顿两秒看看助战,选卡时停顿半秒,攻击动画结束后再停顿一下。把这些停顿编进脚本,既降低了对服务端的请求频率,也让整个脚本看起来更自然。

从技术角度来看,用Python做FGO自动战斗脚本这个项目,麻雀虽小五脏俱全。你会在里面用到进程调用、图像处理、状态机设计、异常恢复、日志监控,这些能力放到任何自动化领域都通用。跑通第一版之后,你可以很自然地扩展:换成别的游戏、加上OCR文字识别、接入微信通知推送战斗结果,方向完全由你自己掌握。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询