36: Aimbot
概要
シューティングゲームで定番のAimbotは、自動で一番近くにいる敵を狙ってくれるチートです。今回は、最も簡単なAimbotを作る事を目標にやっていきます。
チート対象のゲームは、x86でオープンソースのAssault Cube v1.2.0.2を使います。
Windows対象で、Visual Studio 2019を使用し
C++
で記述する。ソースコードはまとめてページの一番下にある。Aimbotの作成にはEntityListが必要なので、見ていない方は 35回 から見ていただければ。
オイラー角
Aimbotは「敵の方へ向くように自分の角度を常に調整する」ものなので、角度が大事です。ゲーム内で角度を表現する方法は大きく分けて二種類あり、クォータニオンとオイラー角があります。
クォータニオンの方は 17回 の記事で説明しましたが、 AssaultCubeではオイラー角が使用されており、一般にオイラー角の方が直観的でわかりやすいです。
オイラー角は yaw, pitch, roll という三つの値で表され、人間に例えるとそれぞれ以下の回転方向の回転です。 ( このサイトのgif見た方が早いです )
- yaw: 左向くときとか右向く時とかの左右の回転方向
- pitch: 前転する時などの縦の回転方向
- roll: 側転する時の回転方向
ラジアン か 度 か
角度は、90度
のように度で表す場合と、π/2
のようにラジアンで表す二通りのパターンがあります。今回、角度を求めるのにC++のcmathライブラリの三角関数を使いますが、戻り値はラジアンで返ってきます。
ラジアンと度を変換する際の公式は以下です。
度 = ラジアン * (180/π)
ラジアン = 度 * (π/180)
Aimbotの仕組み
最低限のAimbotの機能を作る上で、必要な情報は以下の二つだけです。- 自分の座標と向き
- 全敵の座標(EntityList)
自分 と (自分に一番近い)相手 の座標は分かっているので、上図の灰色の立方体の全辺の長さが分かっています。
角度と三角形の辺の長さを関連付ける物は三角関数です。 特に、二つの長さから角度を求めるには、
asin,acos,atan
が使えます。全辺の長さが分かっているので、一見
asin,acos,atan
のどれを使って角度を求めても良さそうに見えますが、
例えばゲーム上でpitch
を求めたい場合にacos
を使った場合は、以下のケースで両方とも同じ30度になってしまいます。
しかし、下図の緑の辺を使うasin,atan
なら、上下で正負が違うので角度も30,-30のように区別する事ができます。
このように、使える三角関数と使えない三角関数を判断する必要があります。どの三角関数が使えるかの判断は上記の理屈で考えるのが確実ですが、実は関数の戻り値を見れば簡単に判断できます。
C++のcmathライブラリの
asin,acos,atan,atan2
の戻り値は以下のようになっています。(度で書いてますが、実際は全部ラジアンで値が返ってきます)
- asin: [-90, 90]
- acos: [0, 180]
- atan: [-90, 90]
- atan2: [-180, 180]
acos
は使えないなみたいに判断できると思います。asin, acos, atan
は、sin, cos, tanの値を引数に取り角度を返すので、引数の個数は一つです。
しかし、それだと例えばatan
は以下の結果が同じになってしまいます。
atan
に限らず、上の戻り値を見てみれば、asin, acos, atan
のどれも360度全部を表現する事はできません。
しかし、ゲームではyaw
は360度あるので、360度の範囲で返してくれる三角関数が欲しいです。そこで使えるのが、atan2
です。
上図の例の場合、引数を一つでは無く、atan2(-10, -20), atan2(10, 20)
のように取った場合、atan2
の内部の実装は知りませんが、
少なくとも引数が両方正の場合は[0,90]の角度で返す、引数が両方負の場合は[-90,-180]の角度で返すみたいに上記を区別する事ができます。
よって、atan2
は360度全ての角度を返せます。長くなりましたが、要はAimbotの仕組みは角度を求める事で、その角度を求めるのに
asin,acos,atan,atan2
を使いますという事です。
AssaultCubeで向いてる方向の角度を取得
AssaultCubeでの自分の 横の角度(yaw) と 縦の角度(pitch) を特定します。前回の35回の記事 でやったように、ReClass.NETでプレイヤークラスを見ながら、ゲーム上で視点だけ動かすと、以下のようにオフセット
0x40
がyaw、0x44
がpitchっぽい事がわかります。
まとめると、Assault Cubeでの角度は以下のようになっている事が確認できると思います。
- yaw: float, オフセット
0x40
, 0<=度<360 - pitch: float, オフセット
0x44
, -90<=度<=90
atan2
を使うしか無いです (-180~0の戻り値は360足して正に直す)。
pitchに関しては、上記の説明で出した例と同じなので、asin
を使えば良さげです。
なので、以下の4辺を使う事になります。
Aimbotのコードの作成
早速VisualStudioでDLLプロジェクトを開き、ソリューションのプロパティ画面からマルチバイト文字を使用する設定にしましょう。また、x86でのビルドになっていることを確認しましょう。今回は、dllmain.cppだけにコードを書きます。まずは、アドレスやVector3型の定義等のコードを以下のように書きます。 次に以下のように、二つの座標の差分
Vector3 delta
から距離を求める関数を用意し、無限ループの中でEntityの総数と自分のプレイヤーの座標を取得します。
GetDistance
で使われてるsqrt
はルートで、pow(x,2)
はxの2乗
と言う意味です。"Entityの総数" が格納されているアドレスは記事内では紹介していませんが、前回のEntityListみたいな感じで見つけられるので説明は省略します。
後やる事は二つで、無限ループの中で、「一番近い敵のアドレスを取得」と「その敵と自分の座標からaimYaw,aimPitchを算出」の二点をやればよいだけです。
一番近い敵のアドレスを取得するコードを以下のように追加しましょう。
EntityList内を プレイヤー数(maxPlayer
)-自分 分for文で回し、各Entityの座標をentPos
に入れて、
自分とのx,y,zにおける差分delta
を算出します。delta
から自分と各Entityとの距離をd
に取得し、
最後のif文で最短距離のEntityのアドレスをclosestEntityAddr
に更新していきます。これで一番近くにいる敵(Entity)のアドレスが取得できたので、aimYaw,aimPitchを求めます。
ここで、pitchはわかると思いますが、yawに
+90
が付いているのが不思議だと思います。(ちなみに、
if(aimYaw<0) aimYaw+=360
をしてるのは、[-180,180]で戻り値が返ってくるのに対し、ゲームでは[0,360]で表現されているため、
表記方法を変えるためです。)上図を見るとわかりますが、
atan2
が返す角度は、x軸正の向きを0度とした場合の角度です。
つまり、ゲーム上でz軸方向から見た場合、以下の角度になります。
しかし、ゲーム上でどの方向を yaw=0度
としているかは不明です。
AssaultCubeでは画面右上にコンパスがあり、そこにNとかSとか書いてあるので、yaw=0
の時、真北(N)を見ている事は分かっていますが、
それがy軸の方向なのか、x軸の方向なのかはわかりません。なので、yawに関しては、Aimbotが上手く動くまで
+90
していったり -
を付けたりして何回か試行錯誤しないといけません。
今回自分が試した所、atan2(y,x)+90
で上手くいきました。90度反時計回りにずらしたら上手く行ったという事は、つまり、このゲームではy軸正の方向をyaw=0
にしているという事になります。
もっと言えば、yaw=0
の時に真北(N)を向いていたので、y軸の正の方向が(N)であることがわかります。
また、実際確かめてみるとわかりますが、ゲーム上で右(Nを向いてる状態からEを向く状態へ)へ回転させるとyawが 0 > 90 > 180 > ...
のように上がっていくので、
まとめるとAssaultCubeの 座標/向き/東西南北 の関係は、こうなっていると判明します。
試してみる
話が長くなりましたが、コードは以上で完成なのでビルドして、ゲームにDLL Injectして確かめてみましょう。以下のように、動いても敵の方を常時狙い続けていれば成功です。
Aimbotの改良について
今回実装したのは、Aimbotの一番基本的な実装方法で、実はAimbotはかなり奥深い物になっています。 例えば、現状のAimbotの問題点として、以下のような点があります。- 敵が倒れても次の敵を狙わず、倒れてる敵を狙ってしまう
- 壁の向こう側/天井よりも上 にいる敵も狙ってしまう
- ヘッドショットだと一撃で倒せるが、常に頭を狙っているわけではない
このように、Aimbotには色んな改良項目があります。
ソースコード
dllmain.cpp
#include "pch.h"
#include <cmath>
float PI = 3.14159265359;
typedef struct Vector3 {
float x;
float y;
float z;
Vector3(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {}
} Vector3;
float GetDistance(Vector3 delta) {
return (float)sqrt(pow(delta.x, 2) + pow(delta.y, 2) + pow(delta.z, 2));
}
DWORD WINAPI ThreadMain(LPVOID params) {
DWORD baseAddr = (DWORD)GetModuleHandle("ac_client.exe");
DWORD entityListAddr = *(DWORD*)(baseAddr + 0x10f4f8);
DWORD playerAddr = *(DWORD*)(baseAddr + 0x10f4f4);
DWORD yawAddr = playerAddr + 0x40;
DWORD pitchAddr = playerAddr + 0x44;
while (1) {
int maxPlayer = *(int*)(baseAddr + 0x10F500);
Vector3 myPos(
*(float*)(playerAddr + 0x34),
*(float*)(playerAddr + 0x38),
*(float*)(playerAddr + 0x3c)
);
// Get closest entity
DWORD closestEntityAddr = 0;
float closestDist = -1;
for (int i = 1; i < maxPlayer; i++) {
DWORD entityAddr = *(DWORD*)(entityListAddr + 4 * i);
Vector3 entPos(
*(float*)(entityAddr + 0x34),
*(float*)(entityAddr + 0x38),
*(float*)(entityAddr + 0x3C)
);
Vector3 delta(
entPos.x - myPos.x,
entPos.y - myPos.y,
entPos.z - myPos.z
);
float d = GetDistance(delta);
if (closestDist == -1 || d < closestDist) {
closestDist = d;
closestEntityAddr = entityAddr;
}
}
// Aim closest entity
if (closestDist != -1 && closestEntityAddr != 0) {
Vector3 entPos(
*(float*)(closestEntityAddr + 0x34),
*(float*)(closestEntityAddr + 0x38),
*(float*)(closestEntityAddr + 0x3C)
);
Vector3 delta(
entPos.x - myPos.x,
entPos.y - myPos.y,
entPos.z - myPos.z
);
float aimPitch = asin(delta.z / GetDistance(delta)) * (180 / PI);
float aimYaw = atan2(delta.y, delta.x) * (180 / PI) + 90;
if (aimYaw < 0) aimYaw += 360;
*(float*)yawAddr = aimYaw;
*(float*)pitchAddr = aimPitch;
}
}
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;
}