
简介这是一份面向明日方舟玩家的自动化辅助工具源码基于C与图像识别技术实现日常任务的一键执行适合具备一定C基础、对计算机视觉与游戏自动化感兴趣的开发者研究学习。资源包共2000个文件以1487个json配置数据、190个h头文件、162个cpp源文件为主辅以hpp、py脚本及少量go、dart、js等压缩包约124.87MB涵盖任务调度、战斗识别、基建生产、自动招募等模块。已有345人学习下载。读者可从中获取完整的图像识别落地思路包括灰度化、二值化、降噪等预处理流程边缘与角点特征提取以及OpenCV在C中的实际调用方式同时能参考多线程并行计算优化、任务触发与结果验证逻辑的工程组织并了解应对光照角度变化、反作弊检测等实际挑战的排错与适配策略目录结构清晰便于按模块检索研读。1. 明日方舟助手拆包C 图像识别怎么把日常任务压成一条流水线明日方舟的日常有多折磨玩过的都懂——基建换班、刷理智、公招识别、肉鸽挂层一套下来十几分钟就没了。这个项目就是冲着这件事来的用 C 写一套图像识别驱动的自动化助手把「看屏幕 → 判断状态 → 点哪里 → 等结果」这条链路固化下来一键跑完日常。它适合两类人一类是想直接拿来省时间的玩家另一类是拿它当 C 图像识别 ADB 控制实战案例的开发者。源码里能看到StageDropsImageAnalyzer.cpp、CombatRecordRecognitionTask.cpp这种按业务拆分的识别模块也有AdbController.cpp这种设备控制层结构比想象中干净。下面按「它怎么跑起来 → 识别层怎么设计 → 任务调度怎么串 → 坑在哪」的顺序拆一遍。2. 环境搭建与 ADB 控制层从零把助手跑起来2.1 编译前必须确认的三件事这个项目是 C 工程依赖 OpenCV 做图像处理依赖 ADB 跟模拟器或真机通信。编译之前先把三样东西对齐否则后面报错会很难定位。第一是编译器。Windows 上建议用 MSVC 2019 及以上Linux 上用 GCC 9。项目里有dart_api_dl.c和callback.c这两个 C 文件说明它可能通过 Dart FFI 暴露接口给上层 UIC 和 C 混编时要注意extern C的链接约定不然会出现符号找不到。第二是 OpenCV。版本建议 4.x编译时勾选core、imgproc、imgcodecs、highgui这几个模块就够用。识别逻辑主要用到模板匹配、颜色阈值和轮廓分析不需要dnn模块除非你想自己换深度学习模型。第三是 ADB。把 platform-tools 解压后把路径加进系统环境变量命令行敲adb devices能列出设备才算通。模拟器推荐分辨率固定 1280x720 或 1920x1080横屏。分辨率不固定后面所有坐标和模板都会错位。# 确认 ADB 可用设备列表里要有你的模拟器或真机 adb devices # 预期输出类似 # List of devices attached # 127.0.0.1:5555 device # 拉一帧当前屏幕用来验证截图链路 adb exec-out screencap -p screen.png这两条命令是排错的地基。adb devices没设备后面AdbController.cpp里所有操作都会超时screencap拉出来的图如果是黑的或者花屏说明模拟器渲染模式跟 ADB 截图不兼容常见做法是换成兼容性更好的渲染模式再试。2.2 AdbController 的封装逻辑AdbController.cpp这个文件承担的是「把 ADB 命令翻译成 C 函数」的职责。核心就四类操作截图、点击、滑动、按键。截图用exec-out screencap -p拿 PNG 字节流点击用input tap x y滑动用input swipe x1 y1 x2 y2 duration。// 简化后的 AdbController 核心接口示意 class AdbController { public: // 截图并解码成 OpenCV Mat失败返回空 Mat cv::Mat captureScreen() { std::string cmd adbPath -s deviceSerial exec-out screencap -p; std::vectoruchar buf execAndReadBinary(cmd); if (buf.empty()) return cv::Mat(); return cv::imdecode(buf, cv::IMREAD_COLOR); } // 点击坐标基于截图分辨率 bool tap(int x, int y) { std::string cmd adbPath -s deviceSerial shell input tap std::to_string(x) std::to_string(y); return execAndCheck(cmd); } // 滑动duration 单位毫秒太短会被游戏忽略 bool swipe(int x1, int y1, int x2, int y2, int duration) { std::string cmd adbPath -s deviceSerial shell input swipe std::to_string(x1) std::to_string(y1) std::to_string(x2) std::to_string(y2) std::to_string(duration); return execAndCheck(cmd); } private: std::string adbPath; // adb 可执行文件路径 std::string deviceSerial; // 设备序列号多设备时必须指定 };逻辑说明captureScreen把 ADB 输出的二进制 PNG 直接喂给cv::imdecode省去落盘再读的 IO 开销这是高频截图场景下的常见优化。tap和swipe的坐标必须跟截图分辨率一致所以项目里通常会先截一帧、量出基准分辨率再把所有坐标按比例缩放。参数上swipe的 duration 建议不低于 300ms太快游戏会判定为无效滑动这是血泪经验。提示多设备同时连接时每条 ADB 命令都要带-s 设备序列号否则命令会随机打到某台设备上表现为「点了没反应」或「点到别的窗口」。2.3 分辨率适配的坐标换算不同模拟器默认分辨率不一样直接写死坐标的代码换台机器就废。常见做法是定义一个基准分辨率比如 1280x720所有坐标按基准写运行时按实际截图尺寸做线性缩放。// 基准分辨率下的坐标 - 实际分辨率坐标 struct Point { int x; int y; }; Point scalePoint(const Point base, int baseW, int baseH, int realW, int realH) { Point p; p.x static_castint(base.x * (realW * 1.0 / baseW)); p.y static_castint(base.y * (realH * 1.0 / baseH)); return p; }这段换算看着简单但它是整个助手能不能跨设备跑的关键。参数baseW/baseH是模板截图时的分辨率realW/realH是当前设备截图分辨率。如果游戏有黑边或者非 16:9 比例缩放会引入偏移这时候要么强制模拟器用 16:9要么在换算后加一个手动偏移量校正。3. 图像识别层模板匹配、颜色阈值与多尺度策略3.1 StageDropsImageAnalyzer 的识别流程StageDropsImageAnalyzer.cpp负责识别关卡掉落物这是典型的「固定 UI 元素 有限类别」识别场景。流程分三步先裁出掉落物列表区域再对每个格子做模板匹配最后把匹配结果映射成物品 ID。// 掉落物识别核心裁剪 ROI - 逐格模板匹配 struct DropResult { std::string itemId; int count; float confidence; }; std::vectorDropResult analyzeDrops(const cv::Mat screen, const cv::Mat itemTemplate, const cv::Rect listRoi) { std::vectorDropResult results; cv::Mat roi screen(listRoi); // 裁出掉落列表区域 cv::Mat gray; cv::cvtColor(roi, gray, cv::COLOR_BGR2GRAY); // 灰度化降计算量 cv::Mat matchResult; cv::matchTemplate(gray, itemTemplate, matchResult, cv::TM_CCOEFF_NORMED); // 归一化相关系数匹配 // 阈值 0.85 是经验值低于它误匹配明显增多 cv::threshold(matchResult, matchResult, 0.85, 1.0, cv::THRESH_BINARY); // 非极大值抑制避免同一个物品被重复计数 std::vectorcv::Point locations; cv::findNonZero(matchResult, locations); for (const auto loc : locations) { DropResult r; r.itemId item_ std::to_string(loc.x); // 实际项目按模板索引映射 r.confidence matchResult.atfloat(loc.y, loc.x); results.push_back(r); } return results; }逻辑说明matchTemplate用TM_CCOEFF_NORMED是因为它对亮度变化不敏感游戏内 UI 亮度偶尔波动时更稳。阈值 0.85 不是拍脑袋是拿几十张不同关卡截图跑出来的平衡点——调高到 0.9 会漏识别调到 0.8 会把背景纹理误判成物品。findNonZero之后通常还要做一次距离过滤把挨得太近的匹配点合并否则一个物品会被数成两三个。3.2 颜色阈值在基建换班里的用法InfrastProductionTask.cpp处理基建换班这个场景不适合模板匹配因为干员头像多、状态图标小。常见做法是用颜色阈值判断「哪个设施需要换人」——比如设施图标变红表示干员疲劳变绿表示正常。// 用 HSV 颜色空间判断设施状态比 RGB 更抗光照变化 int judgeFacilityStatus(const cv::Mat screen, const cv::Rect iconRoi) { cv::Mat roi screen(iconRoi); cv::Mat hsv; cv::cvtColor(roi, hsv, cv::COLOR_BGR2HSV); // 红色疲劳标记H 在 0-10 或 170-180S 和 V 都要够高 cv::Mat mask1, mask2, mask; cv::inRange(hsv, cv::Scalar(0, 120, 120), cv::Scalar(10, 255, 255), mask1); cv::inRange(hsv, cv::Scalar(170, 120, 120), cv::Scalar(180, 255, 255), mask2); cv::bitwise_or(mask1, mask2, mask); // 红色像素占比超过 30% 判定为疲劳 double redRatio cv::countNonZero(mask) * 1.0 / (iconRoi.width * iconRoi.height); return redRatio 0.3 ? 1 : 0; // 1 疲劳0 正常 }逻辑说明HSV 的 H 通道对光照不敏感S/V 控制饱和度和亮度把 S/V 下限设到 120 能过滤掉灰色背景。红色在 H 通道跨了 0 和 180 两端所以要两个inRange再合并。30% 这个占比阈值是实测出来的太低会把带红色装饰的正常图标误判太高会漏掉部分被遮挡的疲劳标记。3.3 多尺度匹配应对分辨率漂移同一个模拟器窗口大小拖一下截图分辨率就变了。如果模板是固定尺寸匹配率会断崖式下跌。常见做法是在几个候选尺度上各跑一次匹配取最高分。// 多尺度模板匹配在 0.9 / 1.0 / 1.1 三个尺度上找最佳匹配 cv::Point multiScaleMatch(const cv::Mat screen, const cv::Mat tmpl, double bestScore) { std::vectordouble scales {0.9, 1.0, 1.1}; cv::Point bestLoc(-1, -1); bestScore -1.0; for (double s : scales) { cv::Mat scaled; cv::resize(tmpl, scaled, cv::Size(), s, s, cv::INTER_AREA); if (scaled.cols screen.cols || scaled.rows screen.rows) continue; cv::Mat result; cv::matchTemplate(screen, scaled, result, cv::TM_CCOEFF_NORMED); double minVal, maxVal; cv::Point minLoc, maxLoc; cv::minMaxLoc(result, minVal, maxVal, minLoc, maxLoc); if (maxVal bestScore) { bestScore maxVal; bestLoc maxLoc; } } return bestLoc; }逻辑说明尺度列表不用铺太密0.9/1.0/1.1 三档能覆盖大部分窗口缩放场景再密就是拿 CPU 换那一点点精度。INTER_AREA在缩小模板时比默认插值更干净能减少高频噪声导致的误匹配。bestScore返回后仍要用 0.85 做一次门槛判断低于门槛就当没识别到宁可漏也不要错点。4. 任务调度与状态机把一键日常串成可恢复的流程4.1 TaskData 与任务注册机制TaskData.cpp定义的是任务的数据结构每个任务有名字、前置条件、执行体、超时时间。整个助手启动后调度器按注册顺序依次尝试执行每个任务自己判断「当前屏幕是不是我该处理的界面」。// 任务基类与注册表示意 struct TaskContext { cv::Mat screen; // 当前帧 AdbController* adb; // 设备控制 int retryCount; // 当前任务已重试次数 }; class BaseTask { public: virtual ~BaseTask() default; virtual std::string name() const 0; // 返回 true 表示任务完成或已推进到下一状态 virtual bool run(TaskContext ctx) 0; virtual int maxRetry() const { return 3; } }; // 注册表按优先级顺序存放任务 std::vectorstd::shared_ptrBaseTask taskRegistry { std::make_sharedAutoRecruitTask(), // 公招识别 std::make_sharedInfrastProductionTask(), // 基建换班 std::make_sharedRoguelikeBattleTaskPlugin(), // 肉鸽 std::make_sharedCombatRecordRecognitionTask() // 战斗记录 };逻辑说明每个任务只关心「当前帧里有没有我认识的界面」有就处理没有就跳过。这种设计的好处是任务之间解耦加新任务不用改调度器。maxRetry控制单个任务最多重试几次防止某个界面卡住导致整个流程死循环。retryCount在每次重试后递增超过上限就跳过当前任务继续下一个。4.2 AutoRecruitTask 的公招识别逻辑公招是识别要求最细的场景要读出五个标签、判断星级、决定选哪几个标签。AutoRecruitTask.cpp的做法是先把标签区域裁出来逐个做 OCR 或模板匹配再按预设规则组合。// 公招标签识别与选择 struct RecruitTag { std::string text; cv::Rect box; float confidence; }; bool AutoRecruitTask::run(TaskContext ctx) { // 1. 定位公招界面找不到就返回 false 让调度器试下一个任务 cv::Rect tagArea; if (!locateRecruitPanel(ctx.screen, tagArea)) return false; // 2. 识别五个标签 std::vectorRecruitTag tags recognizeTags(ctx.screen, tagArea); if (tags.size() 5) { ctx.retryCount; return false; // 识别不全交给调度器重试 } // 3. 按规则选标签优先「高级资深干员」其次「资深干员」 std::vectorint selected; for (size_t i 0; i tags.size(); i) { if (tags[i].text 高级资深干员) { selected {static_castint(i)}; break; } } if (selected.empty()) { // 没有高级标签选前两个凑数 selected {0, 1}; } // 4. 点击选中并确认 for (int idx : selected) { cv::Point center(tags[idx].box.x tags[idx].box.width / 2, tags[idx].box.y tags[idx].box.height / 2); ctx.adb-tap(center.x, center.y); sleepMs(200); // 点击间隔太快会丢点击 } return true; }逻辑说明locateRecruitPanel用模板匹配找公招面板的固定标题栏找不到说明当前不在公招界面直接返回 false 让调度器继续。recognizeTags内部可以用模板匹配标签文字固定或轻量 OCR模板匹配更快但需要为每个标签存一张图。点击间隔 200ms 是下限实测低于 150ms 时游戏偶尔会吞掉点击这是玄学但确实存在。4.3 RoguelikeBattleTaskPlugin 的状态推进肉鸽模式状态多、分支杂RoguelikeBattleTaskPlugin.cpp用的是状态机思路每个状态对应一个可识别的界面特征识别到就执行对应操作并推进到下一状态。状态识别特征操作下一状态选人界面底部有「开始行动」按钮选预设干员点开始战斗中战斗中有暂停按钮和费用数字按费用放干员结算界面结算界面有「行动结束」文字点确认选人界面或退出事件界面有选项按钮按预设策略选选人界面这张表是状态机的核心每个状态的识别特征要足够独特否则会误判。比如「战斗中」和「结算界面」都有按钮靠按钮上的文字模板区分。状态推进失败时retryCount递增连续失败三次就退出肉鸽任务避免卡死。5. 避坑与排查识别率上不去、点击没反应怎么查5.1 截图是黑的或花屏现象captureScreen返回的 Mat 全黑或者颜色错乱后续所有识别都失败。原因模拟器的渲染模式跟 ADB 截图不兼容常见于某些模拟器的 DirectX 模式。另外截图命令执行时游戏正在做全屏动画也可能截到过渡帧。解决把模拟器渲染模式改成兼容性更好的模式如 OpenGL再重新截图验证。如果只是偶发在截图后加一个「非黑帧校验」黑帧就重截一次最多重试两次。5.2 模板匹配分数普遍偏低现象所有模板的匹配分数都在 0.6 到 0.7 之间达不到 0.85 阈值识别全挂。原因模板截图的分辨率跟当前截图分辨率不一致或者游戏 UI 缩放比例变了。也可能是模板截图时带了背景而当前背景变了。解决先确认当前截图分辨率跟模板基准分辨率对齐。如果分辨率一致但分数还是低重新截模板截的时候只框 UI 元素本身不要带周围背景。多尺度匹配能缓解一部分但根治还是要模板干净。5.3 点击位置偏移现象识别到了按钮但点击后没反应或者点到了旁边的元素。原因坐标换算时基准分辨率跟实际分辨率比例算错或者游戏有黑边导致有效区域偏移。解决在换算函数里打印基准坐标和实际坐标手动核对几个关键点。如果有黑边先检测黑边宽度把有效区域裁出来再做换算。常见做法是截一帧纯色背景的图用cv::findContours找有效区域边界。5.4 任务卡在某个界面反复重试现象日志显示同一个任务重试次数一直涨但界面没变化。原因识别逻辑把当前界面误判成了目标任务界面执行了操作但操作无效于是又回到同一状态。解决把该任务的识别阈值调高或者增加一个「操作后界面是否变化」的校验。如果操作后截图跟操作前几乎一样用帧差判断说明操作没生效直接跳过该任务而不是重试。5.5 多设备时命令打到错误设备现象明明指定了设备但操作打到了另一台设备上。原因ADB 命令没带-s参数或者设备序列号写错。解决所有 ADB 调用统一走AdbController序列号在构造时传入禁止在业务代码里裸写 ADB 命令。加一条启动校验adb devices列出的设备数跟配置里的设备数一致才继续。6. 进阶技巧用帧差和操作回执把稳定性再提一档跑通基础流程之后真正拉开稳定性差距的是两个东西帧差校验和操作回执。帧差校验是在每次操作前后各截一帧算一下两帧的差异度差异太小说明操作没生效直接判定失败而不是傻等。操作回执是给每个点击配一个「预期界面变化」点完等 500ms 再截一帧确认界面确实变了才继续。// 帧差校验操作前后差异低于阈值判定为无效操作 bool isScreenChanged(const cv::Mat before, const cv::Mat after, double threshold 0.02) { if (before.size() ! after.size()) return true; cv::Mat diff; cv::absdiff(before, after, diff); cv::Mat gray; cv::cvtColor(diff, gray, cv::COLOR_BGR2GRAY); double changedRatio cv::countNonZero(gray 30) * 1.0 / (gray.rows * gray.cols); return changedRatio threshold; }threshold设 0.02 意味着 2% 的像素发生明显变化才算界面变了。这个值太低会把动画噪声当成变化太高会漏掉小范围 UI 更新。我一般会针对不同任务调这个值基建换班界面变化大用 0.05公招标签选中只是小勾出现用 0.01。另一个技巧是给每个任务加「最大执行时间」。比如基建换班正常 30 秒内完成超过 90 秒还没结束就强制中断记录日志后跳到下一个任务。没有这个上限一个卡住的界面能把整晚的挂机时间全吃掉。// 任务超时保护 auto start std::chrono::steady_clock::now(); while (!task-run(ctx)) { auto elapsed std::chrono::duration_caststd::chrono::seconds( std::chrono::steady_clock::now() - start).count(); if (elapsed task-maxDurationSeconds()) { log(task %s timeout, skip, task-name().c_str()); break; } sleepMs(500); }maxDurationSeconds按任务类型给不同值识别类任务给 60 秒战斗类给 300 秒。这个习惯是从一次挂机翻车里养成的——当时肉鸽卡在一个没见过的界面整晚都在原地重试第二天起来日常一个没做。从那以后我每次加新任务都强制走一遍超时保护宁可跳过也不卡死。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。