一句话总结:本系列用 C++ 从零实现一个外部绘制的最小可用框架,第一篇先解决地基问题——用一个置顶的透明分层窗口盖在游戏画面上,直接穿透鼠标操作,再用 Direct2D 在上面画东西,全文代码可以直接编译运行。
这是系列的开工篇。整个系列四篇的路线是:透明窗口(本篇)→ 读内存拿数据 → 世界坐标投影到屏幕(WorldToScreen)→ ImGui 菜单。目标读者是会 C++ 基础语法、想搞明白「游戏里的方框和线条到底是怎么画出来的」的人。
先把丑话说在前面:本系列的演示对象是虚构的《星陨 Online》(进程名 StarFall.exe),所有内存偏移都是编的。练习请用自己的单机程序或专门的教学靶场,比如 Pwn Adventure 这种官方欢迎你倒腾的 open 靶场游戏。在线游戏上玩这套,轻则封号,重则碰法律红线,这不在本系列的讨论范围里。
OveRLay 的原理:其实是三个窗口技巧
游戏辅助绘制本质上是「往游戏画面上叠一层玻璃」。Windows 给了三样工具,拼起来就是全部原理:
- 置顶窗口(WS_EX_TOPMOST):让自己的窗口永远压在游戏窗口上面
- 分层窗口(WS_EX_LAYERED):支持逐像素透明,画布背景全透明,只显示画出来的线条
- 点击穿透(WS_EX_TRANSPARENT):鼠标事件直接穿过去落到游戏窗口,不然你的透明窗口会挡住操作
再加一个无边框样式(WS_POPUP),去掉标题栏,就是一块「透明的玻璃」。
图 1:三层窗口叠出绘制层,点击穿透和数据供给是两条独立通道
环境搭建
Visual Studio 2022 Community,新建「空项目(C++)」,平台工具集默认。需要在链接器里附加:d2d1.lib、dwrite.lib、user32.lib(一般默认已链)。字符集建议直接用 Unicode,省得宽窄字符来回转。
窗口尺寸先写死 1280×720,跟靶场窗口对齐。系列收尾篇会加自动跟窗。
窗口创建:参数一个一个抠
#include <windows.h>
#include <d2d1.h>
#pRAGma comment(lib, "d2d1.lib")
ID2D1Factory* g_factory = nullptr;
ID2D1HwndRenderTarget* g_rt = nullptr;
ID2D1SolidColorBrush* g_br = nullptr;
LRESULT CALLBACK WndProc(HWND h, UINT m, WPARAM w, LPARAM l) {
switch (m) {
case WM_DESTROY: PostQuitMessage(0); return 0;
}
return DefWindowProcW(h, m, w, l);
}
int APIENTRY wWinMain(HINSTANCE hi, HINSTANCE, PWSTR, int show) {
WNDCLASSW wc = {};
wc.lpfnWndProc = WndProc;
wc.hInstance = hi;
wc.lpszClassName = L"OverlayClass";
RegisterClassW(&wc);
HWND hwnd = CreateWindowExW(
WS_EX_TOPMOST | WS_EX_LAYERED | WS_EX_TRANSPARENT, // 三个扩展样式是核心
L"OverlayClass", L"overlay",
WS_POPUP, // 无边框
100, 100, 1280, 720,
nullptr, nullptr, hi, nullptr);
ShowWindow(hwnd, show);
三个扩展样式各司其职:TOPMOST 保置顶,LAYERED 允许透明,TRANSPARENT 让鼠标点击穿透。少任何一个都会出问题——少 TRANSPARENT,你会得到一块点不动的玻璃;少 LAYERED,背景是黑的。
注意 CreateWindowExW 第一个参数才是扩展样式,容易跟第三个参数 dwStyle 搞混,这是新手最常犯的错。
D2D 初始化与渲染循环
Direct2D 是这套方案里最省心的 2D 渲染选择:API 简单、抗锯齿默认开启、不需要懂交换链。
D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &g_factory);
g_factory->CreateHwndRenderTarget(
D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(hwnd,
D2D1::SizeU(1280, 720)),
&g_rt);
g_rt->CreateSolidColorBrush(
D2D1::ColorF(D2D1::ColorF::Red), &g_br);
MSG msg = {};
while (msg.message != WM_QUIT) {
if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg); DispatchMessageW(&msg);
continue;
}
g_rt->BeginDraw();
g_rt->Clear(D2D1::ColorF(0, 0.0f)); // 全透明清屏
// ---- 每帧绘制内容,下一篇开始在这里画 ----
g_rt->DrawRectangle(D2D1::RectF(100, 100, 300, 260), g_br, 2.0f);
// ----------------------------------------------
g_rt->EndDraw();
Sleep(1);
}
return 0;
}
编译运行,你会看到屏幕上有个红色方框悬在桌面上,鼠标点它会直接点到下面的窗口——地基就成了。这个红色方框就是将来「透视方框」的雏形,区别只是它画在哪由下一篇的内存数据决定。
三个必踩的坑
DPI 缩放。Windows 缩放不是 100% 时,你的窗口坐标和物理像素对不上,绘制会偏移。最省事的解法是在程序入口加一句 SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2),让程序自己管 DPI。
全屏独占模式。这套方案只在窗口化和无边框窗口下有效。游戏切全屏独占时,置顶窗口会被直接压到画面下面。所以实际用的时候,游戏一律开「无边框窗口」模式。这也是为什么市面上所有外部绘制工具都要求无边框。
CPU 占用。渲染循环里 Sleep(1) 只是个占位,实际会让一帧的间隔抖动很大。想省 CPU 可以把渲染帧率限到 60,或者干脆只在数据变化时重绘。系列后面统一改成基于 WaitableTimer 的稳定节拍。
下一篇预告
窗口有了,画笔有了,但画在哪还不知道。下一篇读内存:打开进程、拿模块基址、定义玩家结构体,把「敌人在哪」这个问题变成几个 ReadProcessMemory 调用。到时候这套偏移依然全部是编的,方法是真的。

发表评论
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。