资讯动态

FGA源码解析:Android自动战斗的截图识别与无障碍点击实战

发布时间:2026/10/30 3:04:32 来源:尧图企业网站定制
简介这份资源是面向Android开发者与FGO玩家的自动战斗应用FGAFate/Grand Order Auto Battle配套资料聚焦手游日常任务与重复战斗的自动化实现适合想研究无障碍服务、屏幕捕获与图像识别落地的中高级Android学习者。压缩包为zip格式整体约2.62MB上游未提供文件明细故不展开具体文件构成。目前已有2613人学习下载具备一定参考热度。内容围绕Kotlin语言构建应用架构结合OpenCV对游戏画面中的敌人位置、血条与技能图标进行识别并借助Accessibility无障碍服务完成自动点击与滑动通过MediaProjection获取屏幕画面以支撑识别流程形成从画面采集到策略执行的完整链路。读者可从中理解多技术框架协同的工程组织方式掌握图像识别与自动化操作的排错思路并了解此类工具在游戏条款与账号风险方面的边界。1. 拿到 FGA 源码包先别急着编译它到底替你干了哪几件事FGA 是一个面向 Android 端的自动战斗应用技术栈是 Kotlin OpenCV AccessibilityService MediaProjection。说白了它把「看屏幕」和「点屏幕」这两件事拆开做MediaProjection 负责抓实时画面OpenCV 做模板匹配和颜色判定AccessibilityService 负责把识别结果翻译成点击、滑动这类无障碍动作。你手上这份资源就是这套链路的完整工程适合两类人一是想研究 Android 端图像识别 自动化落地的开发者二是想把这套骨架迁移到自己项目里的从业者。它解决的核心痛点是——不用 root、不用改游戏包纯靠系统级权限把「截图→识别→操作」闭环跑通。但先说清楚这不是装完就能无脑挂机的成品参数、分辨率、模板图都得自己调玄学的地方不少。2. 工程结构与三条核心链路先看懂再动手拿到源码包第一件事不是点 Run而是把目录和依赖关系捋清楚。FGA 这类项目的工程结构通常围绕「采集层 / 识别层 / 执行层」三段式组织Kotlin 负责调度和 UIOpenCV 的 native so 负责图像运算AccessibilityService 和 MediaProjection 是两个系统级入口。你要是上来就改业务逻辑很容易在权限和生命周期上翻车。2.1 模块划分与依赖关系一个合格的 FGA 工程模块大致是这样切的模块职责关键类/文件依赖app主界面、配置、战斗流程调度MainActivity、BattleTask全部capture屏幕采集ScreenCaptureServiceMediaProjectionvision图像识别Matcher、ColorDetectorOpenCVaction无障碍操作AutoAccessibilityServiceAccessibilityServicedata模板图、配置持久化TemplateRepository本地存储依赖方向是 app → capture/vision/actionvision 不直接依赖 action中间靠 BattleTask 做编排。这个分层很重要识别层只输出「坐标 置信度」执行层只认坐标两边解耦之后你换识别算法不用动点击逻辑。常见做法是把 OpenCV 的初始化放在 Application 里做一次OpenCVLoader.initLocal()别在每个 Service 里重复 init否则 so 加载会出问题。// Application 中统一初始化 OpenCV避免多 Service 重复加载 native 库 class FgaApp : Application() { override fun onCreate() { super.onCreate() val ok OpenCVLoader.initLocal() // 4.x 用 initLocal老版本是 initDebug if (!ok) { Log.e(FGA, OpenCV 初始化失败检查 abiFilters 是否包含当前设备架构) } } }这段代码的逻辑是OpenCV 的 native 库按 ABI 分包initLocal()会去加载libopencv_java4.so。参数上要注意abiFilters如果你只打了arm64-v8a在 32 位模拟器上就会加载失败日志里表现为UnsatisfiedLinkError。我一般会在build.gradle里显式写ndk { abiFilters arm64-v8a, armeabi-v7a }多打一个包换兼容性值。2.2 MediaProjection 采集链路采集这块是整个项目最容易出问题的地方。MediaProjection 从 Android 10 开始要求前台服务配合foregroundServiceTypemediaProjectionAndroid 14 之后还要求每次投屏授权都重新申请不能复用 token。FGA 的采集服务典型写法是// 启动前台服务并申请投屏注意 Android 14 必须每次重新请求 private fun startCapture(resultCode: Int, data: Intent) { val manager getSystemService(MediaProjectionManager::class.java) val projection manager.getMediaProjection(resultCode, data) // 注册回调否则部分机型拿到 projection 后立即失效 projection.registerCallback(object : MediaProjection.Callback() { override fun onStop() { Log.w(FGA, 投屏被系统回收需要重新申请) } }, Handler(Looper.getMainLooper())) val metrics resources.displayMetrics val density metrics.densityDpi val width metrics.widthPixels val height metrics.heightPixels // 用 ImageReader 拿 RGBA_8888后续转 Mat 做识别 val reader ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2) projection.createVirtualDisplay( fga-capture, width, height, density, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, reader.surface, null, null ) }逻辑说明getMediaProjection拿到的对象必须注册Callback否则某些 ROM 会在几秒后静默回收表现为「截图全是黑屏」。ImageReader的maxImages设 2 是为了双缓冲设 1 容易在识别耗时的时候丢帧。参数上PixelFormat.RGBA_8888是必须的OpenCV 转 Mat 时按 4 通道处理用 RGB_565 会颜色失真导致模板匹配失败。这里有个血泪经验createVirtualDisplay的宽高一定要用真实屏幕分辨率缩放会导致模板图对不上后面识别全废。2.3 AccessibilityService 执行链路识别出坐标之后点击动作走 AccessibilityService。FGA 里通常封装一个click(x, y)方法底层是dispatchGesture// 通过无障碍手势执行点击注意 API 24 才支持 dispatchGesture fun click(x: Float, y: Float) { val path Path().apply { moveTo(x, y) } val gesture GestureDescription.Builder() .addStroke(GestureDescription.StrokeDescription(path, 0, 50)) // 50ms 按压 .build() dispatchGesture(gesture, object : GestureResultCallback() { override fun onCompleted(desc: GestureDescription?) { /* 点击完成 */ } override fun onCancelled(desc: GestureDescription?) { /* 被系统打断常见于弹窗 */ } }, null) }参数上StrokeDescription的 duration 设 50ms 是经验值太短20ms部分游戏识别不到太长200ms会被判定成长按。onCancelled一定要处理游戏里弹个公告、来个系统弹窗手势就被打断如果你不重试战斗流程就卡死在那一步。常见做法是在onCancelled里做一次延迟重试重试两次还失败就上报异常、暂停任务。3. 从零跑通一次自动战斗配置、模板与识别参数工程看懂了接下来是让它真正跑起来。这一章按「装 → 配 → 识别 → 执行」的顺序走每一步都给出可抄的配置和参数。新手照着做能出结果熟手重点看参数边界。3.1 权限清单与前台服务声明FGA 需要的权限不多但都敏感AndroidManifest.xml里至少要声明这些!-- 无障碍服务需要在系统设置里手动开启 -- service android:name.action.AutoAccessibilityService android:permissionandroid.permission.BIND_ACCESSIBILITY_SERVICE android:exportedtrue intent-filter action android:nameandroid.accessibilityservice.AccessibilityService / /intent-filter meta-data android:nameandroid.accessibilityservice android:resourcexml/accessibility_config / /service !-- 投屏前台服务Android 10 必须声明类型 -- service android:name.capture.ScreenCaptureService android:foregroundServiceTypemediaProjection android:exportedfalse / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION /accessibility_config.xml里要开canPerformGesturestrue否则dispatchGesture直接无效。foregroundServiceType漏写是新手最常见的翻车点Android 14 上会直接抛SecurityException。另外FOREGROUND_SERVICE_MEDIA_PROJECTION这个权限是 Android 14 新增的老工程迁移过来必须补。3.2 模板图制作与匹配参数识别层的核心是模板匹配。FGA 里通常用Imgproc.matchTemplate配TM_CCOEFF_NORMED阈值一般卡在 0.8 左右。模板图怎么截有讲究必须用和目标设备同分辨率的截图裁缩放过的模板匹配分数会掉到 0.6 以下直接失效。// 模板匹配返回最佳匹配位置和置信度 fun match(screen: Mat, template: Mat, threshold: Double 0.8): MatchResult? { val result Mat() // TM_CCOEFF_NORMED 对亮度变化不敏感适合游戏 UI Imgproc.matchTemplate(screen, template, result, Imgproc.TM_CCOEFF_NORMED) val mmr Core.minMaxLoc(result) return if (mmr.maxVal threshold) { // maxLoc 是模板左上角加半宽高得到中心点 MatchResult( x mmr.maxLoc.x template.cols() / 2.0, y mmr.maxLoc.y template.rows() / 2.0, score mmr.maxVal ) } else null }逻辑说明TM_CCOEFF_NORMED输出的是归一化相关系数范围 -1 到 1越接近 1 越像。阈值 0.8 是通用起点但游戏里半透明按钮、动态特效会让分数波动我一般会把关键按钮比如「开始战斗」阈值降到 0.75 并加二次校验非关键按钮保持 0.85 减少误点。maxLoc拿到的是左上角坐标点击要的是中心点所以加cols/2和rows/2这个偏移忘了加点击就会偏到按钮左上角表现为「点了没反应」。颜色判定是另一条路适合血条、技能 CD 这种纯色区域// 判断指定区域是否以某颜色为主用于血条/技能可用性检测 fun isColorDominant(screen: Mat, roi: Rect, target: Scalar, tolerance: Double 30.0): Boolean { val region screen.submat(roi) val hsv Mat() Imgproc.cvtColor(region, hsv, Imgproc.COLOR_RGBA2HSV) // 在 HSV 空间比较比 RGB 抗光照 val mean Core.mean(hsv) return Math.abs(mean.val[0] - target.val[0]) tolerance }HSV 空间做颜色判定比 RGB 稳因为游戏里技能高亮、血条渐变主要影响明度V色相H相对稳定。tolerance设 30 是经验值设太小10稍微有点特效就误判设太大50会把相邻颜色也算进去。3.3 战斗循环的调度与节流识别 点击串起来就是战斗循环。这里最容易犯的错是不节流每帧都截图识别CPU 直接拉满、手机发烫、游戏掉帧。FGA 里一般用固定间隔轮询// 战斗主循环固定间隔采样避免高频截图拖垮设备 private val loopInterval 500L // 毫秒按设备性能调 fun battleLoop() { while (isRunning) { val start System.currentTimeMillis() val screen captureOnce() // 从 ImageReader 取最新帧转 Mat if (screen ! null) { // 优先级先判技能可用再判攻击按钮 val skillReady isColorDominant(screen, skillRoi, skillColor) if (skillReady) { click(skillX, skillY) } else { match(screen, attackTemplate)?.let { click(it.x.toFloat(), it.y.toFloat()) } } screen.release() // Mat 必须释放否则 native 内存泄漏 } val cost System.currentTimeMillis() - start val sleep (loopInterval - cost).coerceAtLeast(50L) Thread.sleep(sleep) // 保证单轮不低于 50ms给系统喘息 } }参数说明loopInterval500ms 是保守值低端机可以放到 800ms旗舰机 300ms 也扛得住但别低于 200ms否则截图和识别抢 CPU。screen.release()这行千万别漏OpenCV 的 Mat 是 native 内存不释放跑几分钟就 OOM日志里是OutOfMemoryError但堆内存看着正常这就是黑匣子所在。coerceAtLeast(50L)是兜底防止单轮耗时超过间隔导致死循环空转。4. 避坑与排查那些跑不通的真实原因这套东西跑不起来九成不是代码逻辑错而是环境、权限、分辨率这些外围问题。下面几条是我踩过的按「现象 → 原因 → 解决」记下来。4.1 截图全黑或全透明现象投屏授权成功但ImageReader取出来的帧全是黑色或透明。原因通常是createVirtualDisplay的宽高和ImageReader不一致或者maxImages设太小导致 buffer 被复用。解决确保ImageReader.newInstance和createVirtualDisplay用同一组宽高maxImages至少 2取帧时用acquireLatestImage()而不是acquireNextImage()后者在慢消费时会阻塞。4.2 模板匹配分数永远偏低现象明明截图和模板肉眼一样maxVal只有 0.5 左右。原因多半是模板图被缩放或压缩过或者屏幕截图带了状态栏/导航栏导致整体偏移。解决模板图必须从同分辨率原始截图裁别用微信传过的图会被压缩识别前把状态栏区域裁掉或者用Rect限定搜索范围减少误匹配。4.3 点击位置偏移现象识别坐标对了但点下去偏了几十像素。原因是dispatchGesture用的是屏幕绝对坐标而你的 Mat 可能被缩放处理过。解决如果识别前对 Mat 做了resize点击坐标要按比例还原回原始分辨率另外检查是否漏加了模板中心偏移见 3.2 的cols/2。4.4 无障碍服务被系统杀掉现象跑一段时间后点击失效日志里没有异常。原因是部分 ROM 对无障碍服务有省电限制后台一段时间就回收。解决在系统设置里给应用加白名单、关掉电池优化代码里在onServiceConnected和onUnbind打日志配合前台服务保活别指望纯后台常驻。4.5 Android 14 上投屏授权复用失败现象第一次授权成功第二次启动直接抛SecurityException。原因是 Android 14 收紧了 MediaProjection token 的复用每次都要重新走createScreenCaptureIntent。解决把授权流程做成每次启动都请求别缓存resultCode和dataregisterCallback里onStop触发后主动清理并提示重新授权。5. 进阶把识别准确率从「能用」拉到「稳」跑通只是及格线真正难的是稳定性。我一般会做两件事一是给识别加多尺度兜底二是把关键判定做成可回放的日志。多尺度兜底的做法是模板匹配失败时把屏幕按 0.9、1.0、1.1 三个尺度各试一次取最高分。代价是耗时翻三倍所以只对关键按钮用普通按钮单尺度就够。代码上就是在match外面包一层循环Imgproc.resize改 screen 的尺寸坐标再按比例还原。// 多尺度匹配关键按钮专用容忍轻微分辨率差异 fun matchMultiScale(screen: Mat, template: Mat, scales: ListDouble listOf(0.9, 1.0, 1.1)): MatchResult? { var best: MatchResult? null for (s in scales) { val resized Mat() Imgproc.resize(screen, resized, Size(screen.cols() * s, screen.rows() * s)) val r match(resized, template) if (r ! null (best null || r.score best!!.score)) { // 坐标要除以缩放系数还原 best r.copy(x r.x / s, y r.y / s) } resized.release() } return best }日志回放这块我的习惯是每次识别失败时把当前帧存成 PNG文件名带上时间戳和判定类型。跑一晚上攒几十张失败样本第二天一看就知道是模板过期了还是阈值太严。这个习惯救过我很多次——有次某个按钮改版模板匹配分数从 0.9 掉到 0.7就是靠失败样本一眼看出来的不然还在那调阈值调到怀疑人生。参数上再补一个经验值TM_CCOEFF_NORMED在纯色背景上容易给出虚高分所以匹配前最好先做一次边缘检测或者限定 ROI别全屏搜。全屏搜还有个问题是耗时1080p 全屏匹配一张 100x100 的模板大概 30-50ms限定到按钮区域能降到 5ms 以内对循环频率影响很大。从那以后我每次改识别逻辑都强制先跑一遍失败样本回放确认新参数在历史失败帧上也能过再上真机。这套流程看着笨但比在真机上盲调省太多时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑