34: DirectX Overlay
概要
DirectXというAPIを使う事で、ゲーム画面上に自分のオリジナルのグラフィックを加える事ができます。今回は、DirectXを理解し、DirectX Overlayを使ってゲーム画面上に現在の座標を表示する事を目標にやっていきます。
チート対象のゲームは、x86でチートの練習用に作られた、Pwn Adventure 3を使います。
Windows対象で、Visual Studio 2019とDirectX9のSDKを使用し
C++
で記述する。
ソースコード内で、windowsのウィンドウを作成する箇所があるので、慣れてない方は 33回
の記事を見てからこっちを読むと良いかもです。この記事は、この動画を参考にしています。完成形のソースコードは一番最後のソースコードです。
DirectXとは
Windowsで グラフィックス や ゲーム関連の処理 を高速に行うためのAPIです。よくゲームを作るのに使われてます。DirectXの中には DirectX Audio, DirectX Input, DirectX Graphics など色々ありますが、 単にDirectXと言うとグラフィック関連の高速な処理を提供するAPIである DirectX Graphics (DirectX 3D) を指す事が多いです。
DirectXにはバージョンがあり、よく出てくるのが DirectX 9,10,11,12 で、バージョン間で結構仕様が変ったりします。 一番扱いやすいと言われているのが DirectX 9 で、ネット上に記事も多いので、DirectX 9 を使っていきます。
DirectXを使ったチートの種類
DirectXを使ったチートは、主にゲームにグラフィック関連で何か付け足すのが目的です。 その方法として大きく以下の二種類があります。- DirectX Overlay:透明なウィンドウをゲームのウィンドウに重ね、そのウィンドウに描画する
- 長所: DirectXを使ってるかどうか問わず全ゲームに使える
- 欠点: フルスクリーンのゲームには使えない、一応別ウィンドウなので例えばゲーム画面のウィンドウキャプチャとかすると見えない
- DirectX Hook:DirectXを内部で使用しているゲームの描画の関数にフックをかけ、オリジナルの描画処理を入れる
- 長所: 余計なウィンドウを作る必要が無い、ゲームの画面を動かした時にちょっと描画がズレたりとかしない
- 欠点: ゲームがDirectXを使ってないといけない、ゲームのDirectXのバージョンに合わせないといけない
DirectX9 SDK のインストール
DirectXを使ったプログラムを書く場合は、まずSDKをダウンロードする必要があります。自分はこれを言語はEnglishにしてダウンロードしました。
DirectX9 SDK を Visual Studio で使うための設定
まず、今回の DirectX Overlay 用のDLLのプロジェクトをいつも通り作成しましょう。 そして、ソリューションのプロパティ画面の詳細 > 文字セット
で、マルチバイト文字を使用するように設定しましょう。Visual Studio で DirectX SDK を使うためには、DirectXを使う新しいプロジェクトを立ち上げる度に以下の3点を設定する必要があります。
#include
した時にDirectXの関数の宣言が書かれているヘッダファイルをどこから探すかのパスの登録- 実際の処理内容が入ってるライブラリファイル(
.lib
ファイル)へのパスを登録 - リンカーの追加の依存ファイルにDirectXのライブラリファイルを登録
<編集...>
の所を押し、以下のようにパスを指定しましょう。
これで、DirectXのヘッダファイルを#include
できるようになりました。続いて、ライブラリディレクトリを編集していきます。 同じく
<編集...>
の所を押し、以下のようにパスを指定しましょう。
これで、DirectXの実際の処理があるライブラリを#pragma comment
等で見つけられるようになりました。最後に、リンカーの依存ファイルにDirectXのライブラリを追加します。 同じく
<編集...>
の所を押し、以下のように入力します。
dllmain.cppに以下の用に書いて、赤線が出ず、ビルドが通ればインクルードディレクトリとライブラリディレクトリの編集は上手く行ってるはずです。 実行時に上手く行かなかった場合はリンカーの依存ファイルの設定をし忘れてるか上手く行ってないです。
透明なウィンドウの作成
では、まずは透明なウィンドウから作成していきます。 コードは以下になります。dllmain.cppに書きましょう。#include "pch.h"
#include <d3d9.h>
#include <d3dx9.h>
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")
LPCSTR windowName = "DirectX9 Overlay Window for PwnAdventure3";
HWND hWnd;
int width, height;
LRESULT CALLBACK WndProc(HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam) {
switch (mes) {
case WM_PAINT:
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, mes, wParam, lParam);
}
}
DWORD WINAPI ThreadMain(LPVOID params) {
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = (HINSTANCE)params;
wcex.hIcon = 0;
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = CreateSolidBrush(RGB(0, 0, 0));
wcex.lpszMenuName = windowName;
wcex.lpszClassName = windowName;
wcex.hIconSm = 0;
RegisterClassEx(&wcex);
hWnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_TRANSPARENT | WS_EX_LAYERED, windowName, windowName, WS_POPUP, 1, 1, width, height, NULL, NULL, (HINSTANCE)params, NULL);
SetLayeredWindowAttributes(hWnd, RGB(0, 0, 0), 0, LWA_COLORKEY);
ShowWindow(hWnd, SW_SHOWDEFAULT);
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
CreateThread(0, 0, ThreadMain, hModule, 0, 0);
}
return TRUE;
}
(このコードの意味が全体的によくわからない人は前回の33回の記事を読んでいただければ。)基本的にはウィンドウを作成するだけのコードとあんま変わらないですが、透明化で大事なのは以下の2点です。
CreateWindowEx
の第一引数である追加のウィンドウスタイルの設定WS_EX_TOPMOST
: ウィンドウが非アクティブでも他のウィンドウの前面に表示される(ゲームがフルスクリーンだと前面に持ってこれない)WS_EX_TRANSPARENT
: マウスクリック等をしたときに、このウィンドウをすり抜けて奥のウィンドウをクリックできるようになるWS_EX_LAYERED
: layered windowに設定する事で、このウィンドウとゲームのウィンドウを同時に描画できるSetLayeredWindowAttributes
で黒色(RGB(0,0,0)
)を透明にしている点
透明ウィンドウがゲームウィンドウと同じサイズ/位置になるようにする
ゲームウィンドウが移動されたり、拡大された時等のために、常にゲームウィンドウと同じサイズ/位置になるようにします。#include "pch.h"
#include <d3d9.h>
#include <d3dx9.h>
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")
LPCSTR windowName = "DirectX9 Overlay Window for PwnAdventure3";
LPCSTR gameTitle = "PwnAdventure3 (32-bit, PCD3D_SM5)";
HWND hWnd, gameHWnd;
int width, height;
LRESULT CALLBACK WndProc(HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam) {
switch (mes) {
case WM_PAINT:
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, mes, wParam, lParam);
}
}
DWORD WINAPI ThreadMain(LPVOID params) {
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = (HINSTANCE)params;
wcex.hIcon = 0;
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = CreateSolidBrush(RGB(0, 0, 0));
wcex.lpszMenuName = windowName;
wcex.lpszClassName = windowName;
wcex.hIconSm = 0;
RegisterClassEx(&wcex);
hWnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_TRANSPARENT | WS_EX_LAYERED, windowName, windowName, WS_POPUP, 1, 1, width, height, NULL, NULL, (HINSTANCE)params, NULL);
SetLayeredWindowAttributes(hWnd, RGB(0, 0, 0), 0, LWA_COLORKEY);
gameHWnd = FindWindowA(0, gameTitle);
if (gameHWnd) {
RECT rect;
GetWindowRect(gameHWnd, &rect);
width = rect.right - rect.left;
height = rect.bottom - rect.top;
}
else return FALSE;
ShowWindow(hWnd, SW_SHOWDEFAULT);
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
RECT rect;
GetWindowRect(gameHWnd, &rect);
width = rect.right - rect.left;
height = rect.bottom - rect.top;
MoveWindow(hWnd, rect.left, rect.top, width, height, true);
}
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
CreateThread(0, 0, ThreadMain, hModule, 0, 0);
}
return TRUE;
}
まず、FindWindow
を使ってウィンドウの名前でゲームのウィンドウを探し、そのハンドルを取得します。
その後、GetWindowRect
でゲームウィンドウの大きさや位置の情報を取得し、MoveWindow
で逐次ゲームウィンドウと同じになるよう座標/サイズを合わせています。
DirectX9プログラミングの基本
DirectXで透明なウィンドウに座標をここから書いていきますが、その前にDirectX9で何か書く時の流れを説明します。DirectX9のプログラムを書く場合の全体の流れは以下です。
- 初期化
- IDirect3D9を作る
- Direct3Dのあらゆる機能を使うためのインターフェースの大元
- D3DPRESENT_PARAMETERSにDirectX関連の色んな設定を詰め込む
- IDirect3DDevice9を作る
- これ作る時にD3DPRESENT_PARAMETERが必要
- ビデオカードなど描画関連のデバイスを管理するインターフェース
- ここに色んな描画に関する関数があるので、こっちを主に使う(IDirect3D9はIDirect3DDevice9を作るぐらいしか役目無い)
- 描画 (以下をメッセージループ内で繰り返します)
- 画面のクリア (Clear)
- 描画開始 (BeginScene)
- 描画処理 (ここに色々オリジナルの描画を書く)
- 描画終了 (EndScene)
- 次の描画に移動 (Present)
フロントバッファとバックバッファ
DirectXはフロントバッファとバックバッファを使用して描画してます。- フロントバッファ: 今表示されてる画面の描画情報が格納されてるバッファ
- バックバッファ: 次のフレームで表示する画面の描画情報が格納されてるバッファ
よって、一度バックバッファに次の画面に表示する描画情報を入れてから、次のフレームになった時にバックバッファの内容をフロントバッファに移動する というように描画しており、これをスワップエフェクトと呼びます。
初期化のコード例
初期化は以下みたいな感じで行います(今回使うコードまんまです)。IDirect3D9Ex* g_pD3D;
IDirect3DDevice9Ex* g_pD3DDev;
Direct3DCreate9Ex(D3D_SDK_VERSION, &g_pD3D);
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.BackBufferWidth = width; // バックバッファの幅
d3dpp.BackBufferHeight = height; // バックバッファの高さ
d3dpp.Windowed = true; // 描きこみたいウィンドウがフルスクリーンじゃない時はtrue、フルスクリーンならfalse
d3dpp.hDeviceWindow = hWnd; // 描き込み対象のウィンドウのハンドル
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; // スワップエフェクトの種類
d3dpp.MultiSampleQuality = D3DMULTISAMPLE_NONE; // マルチサンプリング(エッジを滑らかに描画するためのグラフィック技法)の質
d3dpp.BackBufferFormat = D3DFMT_A8R8G8B8; // 色のビット数など画面のフォーマットの情報
d3dpp.EnableAutoDepthStencil = TRUE; // 深度ステンシルバッファ(3Dで奥行きの情報を持たせる。手前の物で隠れてる奥のものを描画しない)の有無
d3dpp.AutoDepthStencilFormat = D3DFMT_D16; // 深度ステンシルバッファのフォーマット(16bitで奥行きを表現)
g_pD3D->CreateDeviceEx(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, 0, &g_pD3DDev);
また、CreateDeviceEx
の引数の情報は以下の通りです。
- Adapter (D3DADAPTER_DEFAULT): ディスプレイアダプタ(ビデオカード)の指定
- DeviceType (D3DDEVTYPE_HAL): デバイスの種類を指定
- hFocusWindow (hWnd): 描画対象のウィンドウのハンドル
- BehaviorFlags (D3DCREATE_HARDWARE_VERTEXPROCESSING): ハードウェアとソフトウェアどちらで頂点の処理させるか等を設定
- pPresentationParameters (&d3dpp): D3DPRESENT_PARAMETERSへのポインタ
- pFullscreenDisplayMode (0): フルスクリーン時の設定等
- ppReturnedDeviceInterface (&g_pD3DDev): ここで渡したポインタにIDirect3DDevice9が入る
描画のコード例
描画もまず初めに以下をメッセージループの中にいれるのが普通です(今回使うコードまんまです)。g_pD3DDev->Clear(0, NULL, D3DCLEAR_TARGET, 0, 1.0f, 0);
g_pD3DDev->BeginScene();
// -- オリジナルの描画処理 --
g_pD3DDev->EndScene();
g_pD3DDev->Present(NULL, NULL, NULL, NULL);
プレイヤーの現在の座標をDirectX Overlayで表示する
ラストです。まず、20回 の記事からgeneral-movement.h、18回 の記事から general-cheats.h/general-cheats.cpp/player.h/player.cpp をコピーして持ってきましょう。これらは単にPwnAdventure3のプレイヤーの座標を取得するために使うだけです。
できたら、dllmain.cppに以下のように書いて終了です。これが最終コードです。
#include "pch.h"
#include "general-cheats.h"
#include "player.h"
#include <d3d9.h>
#include <d3dx9.h>
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")
Player* player;
LPCSTR windowName = "DirectX9 Overlay Window for PwnAdventure3";
LPCSTR gameTitle = "PwnAdventure3 (32-bit, PCD3D_SM5)";
HWND hWnd, gameHWnd;
int width, height;
ID3DXFont* font = 0;
void drawText(char* string, int x, int y, int a, int r, int g, int b) {
RECT rect;
rect.top = y;
rect.left = x;
font->DrawTextA(0, string, strlen(string), &rect, DT_NOCLIP, D3DCOLOR_ARGB(a, r, g, b));
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam) {
switch (mes) {
case WM_PAINT:
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, mes, wParam, lParam);
}
}
DWORD WINAPI ThreadMain(LPVOID params) {
player = new Player();
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = (HINSTANCE)params;
wcex.hIcon = 0;
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = CreateSolidBrush(RGB(0, 0, 0));
wcex.lpszMenuName = windowName;
wcex.lpszClassName = windowName;
wcex.hIconSm = 0;
RegisterClassEx(&wcex);
hWnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_TRANSPARENT | WS_EX_LAYERED, windowName, windowName, WS_POPUP, 1, 1, width, height, NULL, NULL, (HINSTANCE)params, NULL);
SetLayeredWindowAttributes(hWnd, RGB(0, 0, 0), 0, LWA_COLORKEY);
gameHWnd = FindWindowA(0, gameTitle);
if (gameHWnd) {
RECT rect;
GetWindowRect(gameHWnd, &rect);
width = rect.right - rect.left;
height = rect.bottom - rect.top;
}
else return FALSE;
IDirect3D9Ex* g_pD3D;
IDirect3DDevice9Ex* g_pD3DDev;
Direct3DCreate9Ex(D3D_SDK_VERSION, &g_pD3D);
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.BackBufferWidth = width;
d3dpp.BackBufferHeight = height;
d3dpp.Windowed = true;
d3dpp.hDeviceWindow = hWnd;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.MultiSampleQuality = D3DMULTISAMPLE_NONE;
d3dpp.BackBufferFormat = D3DFMT_A8R8G8B8;
d3dpp.EnableAutoDepthStencil = TRUE;
d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
g_pD3D->CreateDeviceEx(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, 0, &g_pD3DDev);
D3DXCreateFont(g_pD3DDev, 20, 0, FW_BOLD, 1, false, DEFAULT_CHARSET, OUT_DEVICE_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH, "Comic Sans", &font);
ShowWindow(hWnd, SW_SHOWDEFAULT);
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
RECT rect;
GetWindowRect(gameHWnd, &rect);
width = rect.right - rect.left;
height = rect.bottom - rect.top;
MoveWindow(hWnd, rect.left, rect.top, width, height, true);
g_pD3DDev->Clear(0, NULL, D3DCLEAR_TARGET, 0, 1.0f, 0);
g_pD3DDev->BeginScene();
if (gameHWnd == GetForegroundWindow()) {
Vector3 curLoc = player->GetCurrentLocation();
char buf[200];
snprintf(buf, 200, "x:%.2f y:%.2f z:%.2f", curLoc.x, curLoc.y, curLoc.z);
drawText(buf, width / 50, height / 30, 255, 80, 220, 50);
}
g_pD3DDev->EndScene();
g_pD3DDev->Present(NULL, NULL, NULL, NULL);
}
g_pD3DDev->Release();
g_pD3D->Release();
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
CreateThread(0, 0, ThreadMain, hModule, 0, 0);
}
return TRUE;
}
今回はプレイヤーの座標の情報を文字としてゲーム内に表示したいので、D3DXCreateFont
を使ってフォントの調節をし、BeginScene
とEndScene
の間で
座標を取得し、drawText
を使って透明なウィンドウに描画しています。drawText
は自分で定義した関数で、中でID3DXFontオブジェクトの内のメソッドの一つである DrawTextA
を呼び出してます。