写自动化脚本这件事,听起来像是在“偷懒”,但真正上手以后你会发现,它其实是在练系统设计能力。不需要去修改游戏内存、注入DLL,只用窗口查找、图像识别、键鼠模拟这三板斧,就能把大量重复操作串成一条全自动流水线。最近我用C++和大漠插件做了一套针对DNF的自动化脚本,从绑定窗口到最终跑通完整的刷图流程,前前后后折腾了两周。这篇文章不聊虚的,就从原理讲到踩坑,把代码和思路一并列出来,给那些对Windows GUI自动化、游戏辅助开发感兴趣的朋友做参考。
DNF早期的自动化大多基于固定坐标和按键精灵实现,稳定性很差;而用C++配合大漠插件,优势在于可以直接调用COM接口,做到后台绑定、找色找图、OCR识别、键鼠模拟一体化的闭环。这篇文章适合有一定C++基础、了解Windows窗口消息机制、而且想深入理解“图像识别驱动型自动化”的读者。如果你只是随便找个工具录个宏,那这篇文章不适合你;如果你想搞明白每个关键步骤背后的原理,那可以继续往下看了。
1. 项目概览与技术选型
1.1 这套脚本到底做了什么
先交代一下项目目标:模拟玩家手动操作DNF游戏客户端,自动完成“登录 → 选角色 → 进城镇 → 进副本 → 打怪 → 拾取 → 返回城镇 → 循环刷图”的整套流程。核心交互点包括:点击按钮、按技能快捷键、移动角色、与NPC对话、识别对话选项、读取血量蓝量状态等。
从产品功能角度看,这套脚本本质上是“Windows GUI 自动化程序”,只是目标程序从普通的办公软件换成了游戏客户端。换句话说,它和你平时见过的RPA(机器人流程自动化)没什么本质区别,只是操作对象更特殊、实时性要求更高、容错空间更小。如果你理解了这套方案的原理,那以后去做办公自动化、网页数据抓取、重复性UI操作,思路完全可以复用。
我选DNF作为目标,主要原因有三个:一是窗口化运行后可以后台绑定,方便脚本在游戏挂后台时继续做其他事;二是游戏内UI元素相对固定,技能栏位置、血条位置基本不变,找色找图成功率很高;三是副本流程有比较固定的地图和怪物分布,适合用状态机来管理流程。
1.2 为什么选C++而不是Python或易语言
很多刚接触自动化的朋友会问:Python不是也能调用大漠插件吗,为什么非要折腾C++?我实际对比过,Python通过comtypes或pywin32调用大漠确实可行,而且写起来更快,但有几个不太舒服的地方:
第一,Python脚本的分发需要解释器环境,哪怕用PyInstaller打包,产物也很容易被杀毒软件误报;C++编译出来的原生exe体积小、依赖少,部署成本低很多。第二,游戏自动化讲究“手速”和稳定性,C++调用COM接口几乎没有额外性能损耗,在频繁找色找图时优势明显。第三,C++对底层Windows API的操作更直接,后面如果想扩展到内存读取、Hook注入等方向,C++的路径更平滑。
至于易语言,确实有很多现成的游戏脚本是用它写的,但在我看来,易语言的生态相对封闭,代码可维护性差,而且很容易被各种加固系统标记。既然你愿意花时间研究自动化,不如直接学一门通用语言,收益长远得多。
1.3 大漠插件能做什么、不能做什么
大漠插件是一套基于COM的Windows自动化组件,核心能力可以归纳为六块:窗口操作、后台绑定、图色识别、键鼠模拟、文字识别(OCR)、内存读写。对于图片识别这块,它提供了找色、找图、多点找色、模糊找图等接口,比直接用OpenCV写模板匹配省事得多。
但大漠插件也不是万能的。首先,它对后台绑定的支持依赖目标窗口的绘制方式,不是所有游戏都能完美后台;其次,大漠的OCR能力偏基础,适合识别简单的数字、文字,复杂场景还要配合外部识别库;第三,大漠本身是一套商业组件,部分高级功能需要注册码,而且国内杀毒软件对这类工具有时会有误报,部署时要提前做好白名单。
选型的时候我心里很清楚:图像识别自动化的天花板不高,但落地速度快、通用性强,尤其适合做“流程验证”和“场景控制”。先跑通这套方案,后续再考虑要不要结合内存或模型识别,这个顺序不会错。
2. 环境准备与基础开发框架
2.1 开发工具与大漠插件版本
我用的是Visual Studio 2022,Windows 10 64位系统,大漠插件版本是3.1233。大漠老版本和新版本的接口变化不大,但注册方式基本一致。
第一步是把dm.dll放到一个固定目录,然后用管理员权限打开命令提示符,执行组件注册命令:
regsvr32 C:\dm\dm.dll执行成功后,会弹出“DllRegisterServer in C:\dm\dm.dll succeeded”的提示。这一步很关键,因为C++程序是按COM方式调用大漠的,组件没有注册到系统里,CoCreateInstance就找不到对象。
这里要说一下为什么必须用管理员权限:regsvr32注册COM组件需要写注册表的HKEY_CLASSES_ROOT节点,普通权限没有写权限。如果你注册时提示失败,九成是权限不够。另外,杀毒软件可能会拦截注册操作,建议临时关闭实时保护,或者把dm.dll加入白名单。
2.2 在C++工程中加载大漠COM接口
大漠接口在C++中有两种常用加载方式:一种是直接用Windows COM API(CoCreateInstance),另一种是用#import指令引入类型库,自动生成智能指针包装类。我推荐第二种,代码简洁,而且智能指针可以自动管理COM引用计数。
在Visual Studio里可以这样写:
#import "C:\dm\dm.dll" named_guids raw_interfaces_only using namespace DmSoft; void InitDm() { ::CoInitialize(nullptr); IDmSoftPtr dm; HRESULT hr = dm.CreateInstance(__uuidof(DmSoft)); if (SUCCEEDED(hr)) { _bstr_t ver = dm->Ver(); printf("大漠版本: %ls\n", (wchar_t*)ver); } else { printf("创建大漠对象失败: 0x%08X\n", hr); } }如果你不想用#import,也可以手动声明接口指针:
#include <windows.h> #include "dm.h" // 从idl生成的头文件 IDmSoft* dm = nullptr; HRESULT hr = CoCreateInstance(CLSID_DmSoft, nullptr, CLSCTX_INPROC_SERVER, IID_IDmSoft, (void**)&dm);第一次编译时,我踩过一个坑:如果#import路径写错或者dm.dll未注册,编译报错不明显,运行时才会在CreateInstance返回REGDB_E_CLASSNOTREG。排查问题的时候,我建议先写一个只有“获取版本号”的最小程序,跑通了再继续加功能,这样能快速区分是环境问题还是代码问题。
2.3 工程属性配置细节
C++调用大漠还需要注意工程配置:
- 字符集:建议使用“使用多字节字符集”,因为大漠接口返回的很多字符串是基于ANSI的,Unicode模式容易遇到转换问题。
- 预处理定义:如果使用
#import,不需要额外加宏。 - 平台目标:建议设置为
x64或x86统一。如果游戏是32位,而你的脚本是64位,后来绑定窗口时可能会因为权限级别不同而出问题。我自己全程用x64编译,没有遇到特殊障碍。 - MFC使用:纯控制台程序就可以,不需要MFC支持。
这些细节看起来不起眼,但都是我在实际编译中真实遇到的问题。特别是字符集,一旦切到Unicode,_bstr_t转换和字符串比较就会变得非常啰嗦,尽量规避。
3. 核心开发:窗口绑定与屏幕识别
3.1 窗口绑定:前台还是后台
自动化脚本的第一步是找到游戏窗口,然后绑定。DNF游戏窗口通常可以通过标题或者进程查找。大漠接口里最简单的查找方式是FindWindow:
HWND hwnd = (HWND)dm->FindWindow("", "地下城与勇士");但这种方式在双开或多开时会失效,因为它只返回第一个匹配的窗口。解决方法是遍历进程枚举,根据进程PID找到所有相同进程的窗口句柄。我们可以用EnumWindows配合GetWindowThreadProcessId来收集所有窗口,再按进程名过滤。这样每个窗口对应一个独立脚本实例,互不干扰。
找到窗口后,就要绑定。绑定是大漠自动化最关键、最容易出问题的一步。大漠绑定窗口有三种大方向:普通绑定、GDI绑定、DX绑定。
| 绑定模式 | 原理 | 适用场景 | 优缺点 |
|---|---|---|---|
| normal | 直接发送Windows消息 | 简单的窗口程序 | 兼容性好,但游戏内绘图可能无法响应 |
| gdi | 截取GDI绘制内容 | 老游戏、简单的DX游戏 | 后台截图有效,适合找色找图 |
| dx/dx2/dx3 | 通过DirectX接口截取画面 | 多数现代网游 | 能找到画面内容,但绑定配置复杂 |
DNF在窗口化运行状态下,后台绑定通常需要使用DX模式,具体是dx还是dx2,要看你本机显卡驱动和大漠版本。我实测下来,在NVIDIA显卡 + Windows 10环境下,使用dx2模式绑定比较稳定:
long hr = dm->BindWindow((long)hwnd, "dx2", "windows", "windows", 0); if (hr == 1) { printf("绑定成功\n"); } else { printf("绑定失败,错误码: %ld\n", hr); }绑定失败时,可以尝试更换绑定模式,比如改成dx、gdi,或者替换为“normal+windows”。大漠文档里对每种模式都有说明,但最终还是要靠测试。记得绑定之前,游戏窗口不能最小化,不能遮挡严重;绑定成功后,尽量减少对窗口的频繁拖拽,否则可能导致绑定失效。
3.2 找色找图:识别画面的底层逻辑
找到窗口并绑定之后,最重要的就是让脚本“看懂”屏幕。大漠的找色找图本质上是在绑定窗口的缓冲区里逐像素扫描,匹配颜色或图片特征。它不是一个通用的图像识别算法,更像是“模板匹配+颜色直方图”的轻量实现。
找色接口示例:
int x = 0, y = 0; // 在指定区域内找颜色0x4A2F1B,允许偏色101010 int ret = dm->FindColor(0, 0, 1920, 1080, "4A2F1B-101010", 1.0, 0, &x, &y); if (x >= 0 && y >= 0) { printf("找到颜色: (%d, %d)\n", x, y); }这里4A2F1B-101010的意思是:目标颜色是BGR格式的0x4A2F1B,偏色范围是10 10 10。偏色参数很有用,因为游戏画面会有反锯齿、光影变化,颜色会轻微抖动。不设偏色,脚本在暗场景下可能找半天也找不到目标。
找图接口类似:
dm->FindPic(0, 0, 1920, 1080, "button.bmp", "000000", 0.9, 0, &x, &y);0.9是图片相似度,新手最容易在这里翻车。相似度设得太高,窗口缩放或画面渲染稍有不同就匹配不上;设得太低,又容易把相似的UI元素误识别成目标。我的经验是,素材图尽量裁剪小图标区域,不要包含背景;制作素材时用游戏原分辨率截图,不要拉伸。如果同一张图在不同的分辨率下表现差异大,就要准备多套素材,或者干脆用多点找色代替。
3.3 分辨率适配与坐标转换
DNF有多个分辨率选项,玩家屏幕尺寸也五花八门。如果脚本里写死1920×1080坐标,到2560×1440的电脑上,点击位置就会整体偏移。最简单的适配方案是坐标换算:
struct Point { double x; double y; }; Point ConvertPos(int rawX, int rawY, int baseWidth, int baseHeight, int targetWidth, int targetHeight) { Point p; p.x = rawX * (targetWidth / (double)baseWidth); p.y = rawY * (targetHeight / (double)baseHeight); return p; }这里baseWidth、baseHeight是脚本录制素材时用的分辨率,targetWidth、targetHeight是当前游戏窗口的分辨率。所有涉及到找图、找色的坐标,都用换算后的坐标再去调用大漠接口。
大漠其实也提供了屏幕缩放接口SetScreenRatio,但那个接口设置的是整个窗口的缩放偏移,对部分游戏支持不完美。我更喜欢自己做换算,逻辑清楚,调试起来也直观。
3.4 OCR识别:读取状态信息
除了找色找图,偶尔还要读取游戏里的文字,比如角色名、金币数量、副本剩余时间。大漠自带的OCR能力比较简单,需要先用大漠综合工具制作字库,再通过Ocr接口识别:
char result[256] = {0}; dm->Ocr(0, 0, 200, 50, "ffffff-000000", 1.0, result); printf("识别结果: %s\n", result);字库制作是个体力活:要把每个字符截图、定义字符内容、生成字库文件。识别率受字体、字号、背景干扰影响很大。如果你只需要识别数字,那工作量还好;如果要识别中文,我建议别在大漠OCR上死磕,直接把截图保存下来,用外部OCR引擎离线识别,更省心。
4. 模拟操作与实战脚本设计
4.1 键盘鼠标模拟的正确姿势
识别到目标位置后,脚本需要执行点击、按键等操作。大漠提供了前台和后台两套接口。前台操作就是移动真实鼠标、发送真实键盘事件,好处是兼容性最好,坏处是你不能再碰电脑做其他事。后台操作则是在窗口绑定的基础上直接向窗口发送消息或DX虚拟键,可以做到“游戏在后台运行,脚本照常操作”。
常用操作示例:
// 移动鼠标到指定位置并点击 dm->MoveTo(x, y); dm->LeftClick(); // 按键 dm->KeyPress(49); // 数字键1,对应技能栏第一个技能 dm->KeyDown(17); // Ctrl按下 dm->KeyUp(17); // Ctrl抬起 // 后台操作模式下,可以指定给绑定窗口发送按键 dm->KeyPress(49);这里有几个容易踩的坑。第一,很多游戏会检测鼠标移动轨迹是否是“人类轨迹”,如果你每次都瞬移坐标,容易被判定为脚本。解决方法是加入随机延迟和微小偏移:
void RandomMove(int x, int y) { int offsetX = rand() % 6 - 3; int offsetY = rand() % 6 - 3; int steps = rand() % 5 + 3; int curX = 0, curY = 0; dm->GetCursorPos(&curX, &curY); for (int i = 1; i <= steps; i++) { int nx = curX + (x + offsetX - curX) * i / steps; int ny = curY + (y + offsetY - curY) * i / steps; dm->MoveTo(nx, ny); Sleep(rand() % 20 + 10); } }第二,连续操作之间必须加延迟。没有延迟的脚本就像机器人一样,操作节奏太快,不仅会触发游戏的安全检测,还可能导致游戏窗口消息堆积、操作丢失。我的经验是每次操作之间至少Sleep(100),关键操作之间可以加Sleep(300)到Sleep(800)的随机延迟。
第三,技能连招或移动时,很多时候需要“按住”而不是“点按”。比如跑步,需要按住方向键一段时间再松开;这时候用KeyDown和KeyUp组合,不能用KeyPress,因为KeyPress的按下时间太短,游戏里根本跑不起来。
4.2 自动刷图的完整流程设计
自动化脚本最容易犯的错误,是把所有逻辑堆在一个大循环里,判断条件套判断条件,最后改一个功能就崩一片。我的做法是用状态机来管理流程。所谓状态机,就是把整个刷图过程拆成若干状态,每个状态只负责一件事,完成后再跳转到下一个状态。
对于DNF刷图,我设计了这些核心状态:
S_LOGIN:输入账号密码,点击登录。S_SELECT_ROLE:选择角色,进入城镇。S_TOWN:在城镇中移动到副本入口,进入选择地图界面。S_ENTER_DUNGEON:选择副本难度,点击开始。S_FIGHT:进入副本后,移动角色、释放技能、打怪。S_PICKUP:怪物清完后,拾取掉落物。S_AFTER_DUNGEON:结算界面,点击继续/返回城镇。S_RESTART:返回城镇后,判定疲劳值,决定是否继续下一把。
每个状态之间用条件判断来切换。比如在S_FIGHT状态中,如果识别到“结算按钮”出现,说明副本结束,就切到S_AFTER_DUNGEON;如果在S_RESTART状态中识别到“疲劳值不足”的提示,就直接进入S_QUIT退出脚本。
下面是一个简化版的状态切换伪代码:
enum ScriptState { S_LOGIN, S_SELECT_ROLE, S_TOWN, S_ENTER_DUNGEON, S_FIGHT, S_PICKUP, S_AFTER_DUNGEON, S_RESTART, S_QUIT }; ScriptState state = S_LOGIN; void Update() { switch (state) { case S_LOGIN: if (DoLogin()) state = S_SELECT_ROLE; break; case S_SELECT_ROLE: if (SelectRole()) state = S_TOWN; break; case S_TOWN: if (MoveToDungeon()) state = S_ENTER_DUNGEON; break; case S_ENTER_DUNGEON: if (EnterDungeon()) state = S_FIGHT; break; case S_FIGHT: if (CheckDungeonEnd()) state = S_PICKUP; else FightLogic(); break; // 其余状态类似... } }状态机的好处是:每个状态可以独立测试,出错时能快速定位到具体环节。比如一直卡在S_TOWN,那问题就出在NPC识别或移动路径上,不需要查看整段代码。
写战斗逻辑时,我建议不要把技能循环写得太复杂。DNF的副本怪物位置是随机的,你要做的是给脚本一套可执行的规则:优先对最近怪物释放范围技能;如果血量低于30%,先吃药;如果角色被困住,用位移技能脱困。这些规则用简单的坐标识别加随机分支就能实现,不需要AI就能达到“看起来像人”的效果。
4.3 异常处理与断线重连
长时间运行脚本,最怕的是游戏掉线、网络波动、弹窗提示。一个成熟的自动化脚本必须包含异常检测和自动恢复机制。
我的做法是:每5秒检查一次窗口句柄是否有效,如果发现游戏进程退出,就自动重新启动游戏并登录;如果识别到“与服务器连接断开”的提示,就点击确定,然后重新进入游戏。同时把每次状态切换和异常事件写入日志文件,方便事后排查:
void Log(const char* msg) { FILE* fp = fopen("script.log", "a"); SYSTEMTIME st; GetLocalTime(&st); fprintf(fp, "[%02d:%02d:%02d] %s\n", st.wHour, st.wMinute, st.wSecond, msg); fclose(fp); }日志看起来不起眼,但调试自动化脚本时就是生命线。第一次长时间跑脚本挂掉,我盯着游戏窗口猜了半天,最后打开日志才发现是某个找图超时之后状态没复位,所有逻辑卡死在一个不可能跳出的循环里。
4.4 随机化与人性化处理
脚本的“人性化”程度直接决定它能不能长期稳定运行。这里说的随机化不是简单地加随机数,而是让操作间隔、操作轨迹、甚至操作顺序都呈现出一种“不规律但合理”的节奏。
我自己封装了一个简单工具函数:
void SleepRandom(int base, int range) { Sleep(base + rand() % range); }然后所有关键操作之间都用它来控制节奏。比如点击进入副本后,正常情况下需要等加载画面,这段等待时间比较长,我会设成SleepRandom(2000, 1500);释放技能后,由于技能动作有前摇后摇,操作间隔设成SleepRandom(150, 300)。
随机化的核心目的是避免“机械感”。如果你每次点击坐标完全相同,延迟完全一致,那和按键精灵时代的脚本没什么区别。稍微加一点随机偏移和间隔抖动,稳定性提升非常明显。
5. 常见问题与排查技巧
5.1 绑定窗口失败:先看权限再看模式
绑定失败是最常见的入门问题,原因主要集中在四个方面:
- 没有以管理员权限运行脚本。
- 游戏窗口已经最小化或遮挡严重。
- 绑定模式与游戏不匹配。
- 杀毒软件拦截了大漠的进程注入或消息调用。
解决步骤是:先确认脚本以管理员权限运行;然后把游戏窗口还原、不要遮挡,再尝试绑定;如果还是不行,换dx、dx2、gdi、normal几种模式轮询测试。我写了一个自动尝试绑定的小工具,启动后依次用不同模式绑定窗口,几秒钟就能测出当前环境支持哪种模式。
5.2 找图不稳定:偏色和相似度要配合调整
找图不稳定表现在两个方向:一是该找到的时候找不到,二是不该找到的时候误匹配。前者大概率是相似度设太高,或者素材图中包含了过多的背景内容;后者大概率是偏色设太大,或者素材区域太小、特征不够明显。
我的建议是:素材图尽量只截取目标特征的“核心部分”。比如识别“开始副本”按钮,不要把整个按钮截下来,而是截取按钮里的“开始”两个字区域。这样即使按钮背景有不同颜色,也不会影响匹配。另外,尽量用多点找色代替大图匹配。多点找色只取几个关键像素点的颜色关系,受分辨率影响小,速度也更快。
5.3 脚本跑一会就掉线或冻结
游戏长时间运行后,画面特效、内存占用都会变化,脚本可能因为某个识别条件一直不满足而卡在循环里。这时候不能只靠脚本自身检测,还要加上“看门狗”机制。
我设计了两种看门狗:
- 超时看门狗:每个状态设置最大执行时间。比如正常刷图最多10分钟,如果超过15分钟还在
S_FIGHT状态,就强制触发回城重开。 - 卡死看门狗:定时检测窗口画面是否长时间无变化。如果连续30秒画面和初始帧完全一样,大概率是游戏弹了无法识别的对话框或卡加载了。
遇到这类情况,不要试图在脚本里处理所有意外,更合理的做法是“放弃当前局,重新来过”。毕竟自动化的目标是长时间无人值守,偶尔浪费一把图的收益,远小于脚本逻辑复杂度上升带来的风险。
5.4 大漠插件被杀毒软件误报
大漠插件这类工具被误报几乎是必然的。如果你的脚本只是自己学习使用,可以把程序目录加入杀毒软件白名单;如果要做分发,建议对核心逻辑做签名或加壳,但这也只能降低误报概率,不能完全消除。
我自己的看法:不要因为这个原因放弃大漠。它的开发效率确实高,尤其适合快速验证自动化的思路。如果你在意杀毒误报,可以看看目前主流的RPA工具,或者直接用OpenCV配合Windows API自己写一套轻量识别框架,但那属于另一篇很长的文章了。
6. 学习建议与延伸思考
6.1 这套技术栈还能用在哪些地方
如果你把DNF替换成其他窗口程序,这套脚本框架几乎可以直接迁移。
- 办公自动化:自动填写Excel表、批量处理Word文档、模拟登录OA系统。
- 数据录入:从旧系统截图识别信息,填入新系统。
- 游戏挂机:很多老游戏、模拟器游戏都可以用同样的方式实现后台自动化。
- UI自动化测试:通过找图定位控件,比传统的坐标点击更抗版本更新。
我第一次尝试用大漠做办公自动化的时候,发现比游戏还简单,因为办公软件窗口稳定、元素位置固定、没有复杂的反作弊检测。所以别把这个项目理解成“游戏外挂开发”,它就是一套通用的Windows界面自动化方案。
6.2 技术边界与合规提醒
写到这里,必须认真说一句:自动化和外挂是有明确边界的。如果你的目标是在线网络游戏里牟利或恶意破坏游戏平衡,那不仅违反游戏用户协议,还可能触犯相关法律法规,后果不是技术能兜底的。
我对这个项目的定位是:纯粹的技术研究和个人学习用途。把目标环境设定为单机学习环境、本地测试服务器、或者明确允许脚本操作的个人场景,才能在安全的前提下研究图形识别和自动化控制流程。希望看到这篇文章的读者,也能在合规范围内使用这些技术。
6.3 下一步可以怎么进阶
跑通这套大漠方案后,如果想继续深入,我建议沿着下面几个方向:
- 用OpenCV替换部分找图逻辑,训练更精确的UI识别模型。
- 学习内存读写,理解游戏数据结构的组织方式,但注意只在完全合法合规的环境下使用。
- 引入外置决策脚本,用Lua或Python管理行为逻辑,让C++部分只负责底层执行。
- 尝试把脚本改造成一个插件化框架,不同的目标游戏只写业务逻辑,通用部分沉淀成库。
我目前正在做的是把大漠相关操作封装成一个独立的动态库,上层用Lua脚本控制流程,这样不用每次改逻辑都重新编译C++代码,调试效率明显提高。
最后再分享一个调试习惯:所有状态切换和关键节点都打印带时间戳的日志,关键截图保存到独立目录。第一次跑崩溃时,不要盯着游戏窗口猜,而是翻日志找第一个异常点。自动化脚本出问题,九成都是调用太快、参数太死、或者环境变了,一份好日志能帮你把瞎猜的时间省下来。这个项目做完之后,我自己最大的体会是:图像识别自动化并没有想象中那么难,难的是把业务状态切得足够清晰、把异常处理得足够稳。顺着这个思路去做其他GUI自动化,也能少吃很多亏。