简介本资源是一款面向《命运/冠位指定》FGOAndroid玩家的自动化战斗工具FGAFate/Grand Order Auto Battle聚焦于日常任务与重复战斗场景的效率优化适合具备Android基础、对无障碍服务与图像识别原理感兴趣的中高级开发者或技术型玩家。项目基于Kotlin语言开发深度整合Android Accessibility服务实现界面交互控制并结合OpenCV进行实时屏幕元素识别如血条、技能图标辅以MediaProjection API完成画面捕获构成完整端到端自动战斗链路。压缩包为ZIP格式大小2.62MB含可执行APK及配套说明文件虽未提供详细文件列表但核心交付物明确指向即装即用的自动化能力验证与技术复现参考。目前已有2611人学习下载读者可直接部署运行、分析Kotlin工程结构、研究无障碍权限适配逻辑并借鉴OpenCV在手游UI识别中的轻量化应用方案是理解安卓自动化原理与实战落地的典型技术样本。1. FGA不是“挂机脚本”而是一套基于 Android Accessibility MediaProjection 的 FGO 战斗流程自动化框架你可能在社区里见过“FGA自动打手”“FGA一键清体力”这类说法甚至被某些打包好的APK误导——以为它是个点开就跑的黑盒工具。但真实情况是FGAFate/Grand Order Automation本质上是一个开源的、面向开发者与进阶玩家的 Android 自动化工程实践样本。它不依赖 root不调用非公开系统接口核心链路由三块拼图严丝合缝组成AccessibilityService 做 UI 层事件监听与点击下发MediaProjection ImageReader 实现无延迟屏幕帧捕获OpenCVJava/Kotlin 封装版完成战斗界面关键区域的实时图像识别如指令卡位置、宝具槽状态、敌人血条像素级变化。整个流程完全运行在用户空间所有逻辑可审计、可调试、可按需裁剪。适合两类人一是想把 FGO 战斗逻辑拆解成「状态机视觉反馈」来练手 Android 图像处理与无障碍开发的 Kotlin 工程师二是需要稳定复现特定战斗序列如高难本连打、活动礼装自动切换的硬核玩家。它不是“全自动AI”而是给你一套可验证、可打断、可回溯的自动化骨架——你填入的是战斗策略它执行的是像素到坐标的确定性映射。2. 架构拆解为什么必须同时用 AccessibilityService 和 MediaProjection单用一个会翻车2.1 AccessibilityServiceUI 状态监听的“耳朵”但听不到画面细节AccessibilityService 是 Android 官方为无障碍场景设计的系统级服务它能监听 Activity 切换、View 树变更、文本内容更新等事件。在 FGA 中它承担两个不可替代的角色状态感知当游戏进入战斗界面com.aniplex.fategrandorder/.activity.MainActivity、结算页、或弹出确认框时onAccessibilityEvent()会收到TYPE_WINDOW_STATE_CHANGED事件触发状态机跳转精准交互通过findAccessibilityNodeInfosByViewId()定位“攻击”按钮、“指令卡”区域再调用performAction(AccessibilityNodeInfo.ACTION_CLICK)下发点击规避了坐标硬编码带来的屏幕适配风险。提示AccessibilityService 无法获取屏幕图像也无法判断“当前是否真在战斗中”——它只告诉你“窗口变了”但变的是主界面还是加载动画它不知道。这就是单靠它必翻车的根本原因。2.2 MediaProjection ImageReader画面捕获的“眼睛”但看不懂 UI 语义MediaProjection 是 Android 5.0 提供的屏幕录制/截图 API配合ImageReader可以以 30~60fps 持续获取Image对象YUV_420_888 格式。FGA 在此之上做了两层关键封装YUV → RGB 快速转换避免调用耗时的BitmapFactory.decodeByteArray()直接用RenderScript或libyuvJNI 实现 YUV420 转 RGB24实测单帧转换耗时压到 8ms 内骁龙855OpenCV Mat 绑定将 RGB 数据直接拷贝进Mat对象跳过 Bitmap 中间态防止 GC 频繁触发导致帧率抖动。但 MediaProjection 有个致命短板它捕获的是“纯画面”对 UI 结构一无所知。比如“宝具槽满”这个状态在画面上表现为一个发光圆环但 AccessibilityService 却能通过contentDescriptionNP Gauge: 100%直接读取数值。单靠图像识别去判断满不满得写一堆模板匹配亮度阈值抗噪逻辑而 AccessibilityService 一行代码就拿到结构化数据。2.3 二者协同用 AccessibilityService 做“粗筛”用 OpenCV 做“精判”真实战斗流程中两者分工明确阶段判定靠 AccessibilityService检测到Activity名为BattleActivity且View树中存在id/battle_start_btn→ 进入战斗准备态动作触发靠 OpenCV在 MediaProjection 捕获的帧中用matchTemplate()扫描指令卡区域ROI当TM_CCOEFF_NORMED匹配值 0.85 时才向 AccessibilityService 提交点击请求防误触兜底若 OpenCV 连续 3 帧未识别到指令卡但 AccessibilityService 报告“战斗已开始”则主动暂停并弹出 Toast“视觉定位失败请检查屏幕遮挡”。这种混合架构让 FGA 在 Nexus 5XAndroid 8.1到 Pixel 7Android 13上均能稳定运行核心在于——AccessibilityService 解决“该不该做”OpenCV 解决“能不能做准”。任何试图砍掉其中一环的“精简版”都会在分辨率适配、系统版本升级或游戏 UI 微调后立刻崩盘。3. 核心模块实战从零搭建一个可运行的指令卡识别单元3.1 初始化 MediaProjection申请权限与建立帧流管道// Step 1: 启动 MediaProjectionManager intent 获取用户授权 val mediaProjectionManager getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_CODE_CAPTURE) // Step 2: onActivityResult 中接收 resultData创建 MediaProjection 实例 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode REQUEST_CODE_CAPTURE resultCode Activity.RESULT_OK) { mediaProjection mediaProjectionManager.getMediaProjection(resultCode, data!!) // 创建 ImageReader宽高取屏幕分辨率格式 YUV_420_888缓冲区深度 2双缓冲防丢帧 imageReader ImageReader.newInstance( displayMetrics.widthPixels, displayMetrics.heightPixels, ImageFormat.YUV_420_888, 2 ) // 设置 OnImageAvailableListener每帧到达即触发处理 imageReader.setOnImageAvailableListener({ reader - val image reader.acquireLatestImage() ?: returnsetOnImageAvailableListener processImage(image) // 关键处理函数见 3.2 image.close() }, handler) // 开始虚拟显示将屏幕内容输出到 ImageReader virtualDisplay mediaProjection.createVirtualDisplay( FGA_Display, displayMetrics.widthPixels, displayMetrics.heightPixels, displayMetrics.densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader.surface, null, handler ) } }参数说明ImageFormat.YUV_420_888是 Android 屏幕捕获的默认格式兼容性最好bufferDepth2是血泪经验设为 1 时若processImage()处理稍慢33ms下一帧会覆盖前一帧导致丢帧设为 3 以上则内存占用陡增且无收益VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR确保横竖屏自动适配避免手动旋转计算。3.2 YUV→RGB→Mat 转换绕过 Bitmap 的零拷贝路径private fun processImage(image: Image) { val planes image.planes val yBuffer planes[0].buffer val uBuffer planes[1].buffer val vBuffer planes[2].buffer // 直接从 plane buffer 读取 YUV 数据无需 copyTo() val ySize yBuffer.remaining() val uSize uBuffer.remaining() val vSize vBuffer.remaining() // 分配 Native 内存传给 JNI 函数 yuv420ToRgb() val rgbArray ByteArray(ySize uSize vSize) // YUV 总大小 width*height*3/2 yuv420ToRgb( yBuffer.address(), uBuffer.address(), vBuffer.address(), rgbArray, image.width, image.height ) // 将 rgbArray 直接载入 Mat跳过 Bitmap 构造 val mat Mat(image.height, image.width, CvType.CV_8UC3) mat.dataAddr rgbArray.toLongArray()[0] // 此处为示意实际需 JNI 返回指针 // ... 后续 OpenCV 处理 }关键点planes[0].buffer.address()获取底层内存地址是零拷贝的前提CvType.CV_8UC3表示 3 通道 8 位无符号整型对应 RGB若设备不支持address()如部分低端机降级方案是yBuffer.get(yArray)Utils.yuv420ToRgba()但帧率会跌 15%。3.3 指令卡 ROI 定位与模板匹配抗缩放、抗色偏的鲁棒实现// 预加载三张指令卡模板Attack/Skill/NP尺寸归一化为 120x120 val attackTemplate Imgcodecs.imread(assets/template_attack.png, Imgcodecs.IMREAD_COLOR) val skillTemplate Imgcodecs.imread(assets/template_skill.png, Imgcodecs.IMREAD_COLOR) val npTemplate Imgcodecs.imread(assets/template_np.png, Imgcodecs.IMREAD_COLOR) fun detectCommandCards(mat: Mat): ListCommandCard { val cards mutableListOfCommandCard() val roi Mat(mat, Rect(100, 800, 900, 200)) // 指令卡区域x100,y800,w900,h200 val grayRoi Mat() Imgproc.cvtColor(roi, grayRoi, Imgproc.COLOR_BGR2GRAY) // 多尺度匹配在 0.8~1.2 缩放范围内搜索解决不同机型 DPI 导致的尺寸差异 for (scale in listOf(0.8, 0.9, 1.0, 1.1, 1.2)) { val scaledTemplate Mat() Core.resize(attackTemplate, scaledTemplate, Size(0.0, 0.0), scale, scale, Imgproc.INTER_AREA) val result Mat() Imgproc.matchTemplate(grayRoi, scaledTemplate, result, Imgproc.TM_CCOEFF_NORMED) // 非极大值抑制过滤重叠匹配框 val locations mutableListOfPoint() Core.MinMaxLocResult mmr Core.minMaxLoc(result) while (mmr.maxVal 0.75) { // 置信度阈值 locations.add(mmr.maxLoc) // 在匹配中心画矩形防止重复匹配 Imgproc.rectangle( result, Point(mmr.maxLoc.x - 10, mmr.maxLoc.y - 10), Point(mmr.maxLoc.x 10, mmr.maxLoc.y 10), Scalar.all(0.0), -1 ) Core.minMaxLoc(result, null, null, null, mmr) } // 将 locations 映射回原始屏幕坐标加入 cards locations.forEach { pt - cards.add(CommandCard(ATTACK, pt.x 100, pt.y 800)) } } return cards }参数说明TM_CCOEFF_NORMED对亮度变化鲁棒比TM_SQDIFF更适合游戏 UIminMaxLoc循环 矩形置零是经典 NMS非极大值抑制简化版比DNN方案轻量百倍ROI 坐标Rect(100,800,900,200)需按目标机型实测调整切勿直接抄——这是新手最常踩的坑。4. 避坑指南五个让 FGA 在真机上“突然失灵”的真实问题4.1 现象AccessibilityService 无法启用开关灰显原因Android 9 强制要求 AccessibilityService 必须声明android:canRetrieveWindowContenttrue且 targetSdkVersion ≥ 28 时系统会校验serviceinfo.xml中的accessibility-service标签完整性。常见遗漏包括未设置android:packageNames应填com.aniplex.fategrandorder或android:accessibilityFlags缺少flagDefault | flagIncludeNotImportantViews。解决检查res/xml/accessibility_service_config.xml确保完整包含accessibility-service android:packageNamescom.aniplex.fategrandorder android:accessibilityFlagsflagDefault|flagIncludeNotImportantViews android:canRetrieveWindowContenttrue android:accessibilityEventTypestypeWindowStateChanged|typeViewClicked android:accessibilityFeedbackTypefeedbackGeneric android:notificationTimeout100 /4.2 现象MediaProjection 捕获黑屏或绿屏原因ImageReader的ImageFormat与VirtualDisplay的format不匹配。Android 10 默认VirtualDisplay使用PixelFormat.RGBA_8888但ImageReader若仍设为YUV_420_888会导致颜色通道错乱。解决统一使用PixelFormat.RGBA_8888并在processImage()中改用Image.getPlanes()[0].buffer直接读取 RGBA 数据此时仅一个 plane省去 YUV 转换步骤imageReader ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2) // processImage 中 val plane image.planes[0] val buffer plane.buffer val rgbaArray ByteArray(plane.rowStride * image.height) buffer.get(rgbaArray) val mat Mat(image.height, image.width, CvType.CV_8UC4) // CV_8UC4 RGBA mat.dataAddr rgbaArray.toLongArray()[0]4.3 现象OpenCV 模板匹配在新版本 FGO 中全部失效原因游戏更新后指令卡图标微调如阴影加深、边缘模糊导致TM_CCOEFF_NORMED匹配值从 0.92 降至 0.65。模板未随游戏版本更新且未开启图像预处理。解决在匹配前对 ROI 和模板做标准化处理// 对 grayRoi 和 scaledTemplate 同时执行 Imgproc.GaussianBlur(grayRoi, grayRoi, Size(3,3), 0.0) // 去噪 Core.normalize(grayRoi, grayRoi, 0.0, 255.0, Core.NORM_MINMAX) // 归一化对比度4.4 现象点击指令卡后游戏无响应但 AccessibilityService 日志显示 ACTION_CLICK 成功原因FGO 启用了「触摸防误触」机制要求连续两次点击间隔 150ms 才视为有效操作。单次performAction(ACTION_CLICK)被判定为无效触摸。解决模拟双击node.performAction(AccessibilityNodeInfo.ACTION_CLICK) Thread.sleep(50) // 间隔 50ms node.performAction(AccessibilityNodeInfo.ACTION_CLICK)4.5 现象应用在后台运行时MediaProjection 自动断开原因Android 8.0 对后台服务限制严格MediaProjection实例在 Activity 销毁后会被系统回收。若 FGA 作为 Service 运行未在onDestroy()中显式stop()并重建就会断连。解决将MediaProjection生命周期绑定到前台 Service并在onTaskRemoved()中重启override fun onTaskRemoved(rootIntent: Intent?) { startService(Intent(this, FgaService::class.java)) super.onTaskRemoved(rootIntent) }5. 进阶技巧用状态机 视觉反馈闭环让自动化真正“可中断、可验证”5.1 构建三层状态机覆盖从启动到结算的全链路FGA 的健壮性不来自“识别多准”而来自“状态跳转多稳”。我们摒弃单一线性脚本采用三层嵌套状态机顶层全局流程态IDLE/LAUNCHING/BATTLEING/SETTLING/ERROR中层阶段子态如BATTLEING下分SELECT_CARD/WAIT_ANIMATION/CHECK_ENEMY_HP底层视觉反馈态每个子态绑定一个 OpenCV 检查函数返回SUCCESS/TIMEOUT/ABORT。例如SELECT_CARD子态的闭环逻辑stateMachine.onEnter(SELECT_CARD) { // Step 1: 用 AccessibilityService 确认当前在战斗界面 if (!isInBattleActivity()) returnonEnter ABORT // Step 2: 启动 OpenCV 检查超时 5s val startTime System.currentTimeMillis() while (System.currentTimeMillis() - startTime 5000) { val cards detectCommandCards(currentFrame) if (cards.isNotEmpty()) { // Step 3: 点击第一张卡并等待 AccessibilityService 回报 Card Selected 文本出现 clickCard(cards[0]) if (waitForText(Card Selected, timeout 2000)) { stateMachine.transitionTo(WAIT_ANIMATION) returnonEnter SUCCESS } } Thread.sleep(100) } returnonEnter TIMEOUT }价值点每一层都有明确退出条件ABORT可由用户物理按键触发监听KeyEvent.KEYCODE_BACKTIMEOUT自动降级到ERROR态并弹窗提示——自动化不再是“开就不管”而是“每一步都可审计”。5.2 视觉反馈表用像素级验证替代“相信日志”光靠 AccessibilityService 的contentDescription不可靠游戏可能动态修改文案。我们为关键状态建立视觉验证表强制用 OpenCV 二次确认状态名验证目标区域ROIOpenCV 验证方法通过条件ENEMY_DEFEATED屏幕中央 300x200 区域matchTemplate()匹配“胜利”文字模板匹配值 0.82 且位置居中NP_READY宝具槽区域x50,y100,w120,h120inRange()提取红色通道计算红像素占比占比 35% 且连续 3 帧稳定HP_CRITICAL敌人血条区域x200,y150,w400,h30threshold()二值化后计算黑色像素长度长度 血条总长 × 0.25注意所有 ROI 坐标必须在onConfigurationChanged()中根据displayMetrics动态重算硬编码坐标在全面屏手机上必然失效。5.3 “后悔药”机制一键回退到上一状态当视觉验证失败时最危险的操作是“盲目重试”。FGA 内置状态快照State Snapshot功能每次成功进入新子态前保存当前Mat帧、AccessibilityNodeInfo 树快照、时间戳。当CHECK_ENEMY_HP失败时可一键触发fun rollbackToLastValidState() { // 1. 恢复上一帧画面用于 OpenCV 重识别 currentFrame lastSnapshot.frame.clone() // 2. 用 AccessibilityService 执行“返回”操作 findNodeById(back_button)?.performAction(ACTION_CLICK) // 3. 等待 AccessibilityService 报告回到上一界面 waitForActivity(BattleActivity) }这相当于给自动化装上了“CtrlZ”避免因单点识别失败导致整个流程雪崩。从那以后我每次调试新版本 FGO 的适配都强制走一遍「状态机初始化 → ROI 坐标实测 → 视觉验证表填充 → 回滚测试」四步流程哪怕多花 20 分钟也比半夜三点被用户 Issue 报告“打不了 NP”强。希望帮到你。本文还有配套的精品资源点击获取