C++で作るゲームエンジン自作シリーズ
| Part1 設計編 | Part2 ウィンドウ | Part3 OpenGL | Part4 シェーダー | Part5 テクスチャ | Part6 3Dモデル | Part7 当たり判定 |
|---|---|---|---|---|---|---|
| - | - | - | - | - | - | 👈 Now |
はじめに
これが最終回。
絵が出るだけじゃゲームにならない。当たり判定を実装して、ゲームエンジンを完成させる。
衝突検出の基本
バウンディングボリューム
複雑なメッシュ同士の衝突判定は重い。だから単純な形状で近似する。
┌─────────────┐
│ ┌───┐ │ ← AABB(軸平行境界ボックス)
│ │ ◆ │ │ 複雑な形状を箱で囲む
│ └───┘ │
└─────────────┘
主なバウンディングボリューム:
| 形状 | 判定速度 | フィット精度 |
|---|---|---|
| AABB | 最速 | 低い |
| OBB(向き付きボックス) | 速い | 中 |
| 球 | 速い | 低い |
| カプセル | 速い | 中 |
| 凸包 | 遅い | 高い |
| メッシュ | 最遅 | 最高 |
ゲームではAABBと球がよく使われる。
AABB(Axis-Aligned Bounding Box)
定義
struct AABB {
glm::vec3 min; // 最小点
glm::vec3 max; // 最大点
};
「軸に平行なボックス」という意味。回転しない。
max
┌─────┐
│ │
│ │
└─────┘
min
AABB同士の衝突判定
めちゃくちゃシンプル:
bool intersects(const AABB& a, const AABB& b) {
return a.min.x <= b.max.x && a.max.x >= b.min.x &&
a.min.y <= b.max.y && a.max.y >= b.min.y &&
a.min.z <= b.max.z && a.max.z >= b.min.z;
}
全ての軸で重なっていれば衝突。
2Dだとこう:
A B
┌─────┐ ┌─────┐
│ │ │ │ ← X軸で重なってない → 衝突なし
└─────┘ └─────┘
┌─────┐
│ ┌──┼──┐
│ │ │ │ ← X軸でもY軸でも重なってる → 衝突!
└──┼──┘ │
└─────┘
点の内包判定
bool contains(const AABB& box, glm::vec3 point) {
return point.x >= box.min.x && point.x <= box.max.x &&
point.y >= box.min.y && point.y <= box.max.y &&
point.z >= box.min.z && point.z <= box.max.z;
}
球の衝突判定
球同士
struct Sphere {
glm::vec3 center;
float radius;
};
bool intersects(const Sphere& a, const Sphere& b) {
float distance = glm::length(a.center - b.center);
return distance <= (a.radius + b.radius);
}
中心間の距離が半径の和以下なら衝突。
球とAABB
bool intersects(const Sphere& sphere, const AABB& box) {
// 球の中心から最も近いAABB上の点を求める
glm::vec3 closest;
closest.x = std::max(box.min.x, std::min(sphere.center.x, box.max.x));
closest.y = std::max(box.min.y, std::min(sphere.center.y, box.max.y));
closest.z = std::max(box.min.z, std::min(sphere.center.z, box.max.z));
// その点と球の中心の距離
float distance = glm::length(closest - sphere.center);
return distance <= sphere.radius;
}
レイキャスト
「この方向にビームを撃ったら何に当たるか」を調べる。
レイの定義
struct Ray {
glm::vec3 origin; // 始点
glm::vec3 direction; // 方向(正規化)
};
レイとAABBの交差(Slab法)
bool rayIntersectsAABB(const Ray& ray, const AABB& box, float& tMin, float& tMax) {
tMin = 0.0f;
tMax = std::numeric_limits<float>::max();
for (int i = 0; i < 3; i++) {
float minVal = box.min[i];
float maxVal = box.max[i];
float origin = ray.origin[i];
float dir = ray.direction[i];
if (std::abs(dir) < 1e-6f) {
// レイが軸に平行
if (origin < minVal || origin > maxVal) {
return false;
}
} else {
float t1 = (minVal - origin) / dir;
float t2 = (maxVal - origin) / dir;
if (t1 > t2) std::swap(t1, t2);
tMin = std::max(tMin, t1);
tMax = std::min(tMax, t2);
if (tMin > tMax) {
return false;
}
}
}
return true;
}
Slab法は、AABBを3つの「スラブ(板状の領域)」の交差として扱う。
衝突応答
衝突を検出するだけじゃ足りない。めり込みを解消する必要がある。
衝突情報の取得
struct CollisionInfo {
bool collided;
glm::vec3 normal; // 衝突面の法線
float penetration; // めり込み量
};
CollisionInfo getCollisionInfo(const AABB& a, const AABB& b) {
CollisionInfo info;
if (!intersects(a, b)) {
info.collided = false;
return info;
}
info.collided = true;
// 各軸でのオーバーラップ量を計算
float overlapX = std::min(a.max.x - b.min.x, b.max.x - a.min.x);
float overlapY = std::min(a.max.y - b.min.y, b.max.y - a.min.y);
float overlapZ = std::min(a.max.z - b.min.z, b.max.z - a.min.z);
// 最小のオーバーラップ軸を分離軸とする
if (overlapX <= overlapY && overlapX <= overlapZ) {
info.penetration = overlapX;
info.normal = (a.center().x < b.center().x)
? glm::vec3(-1, 0, 0) : glm::vec3(1, 0, 0);
} else if (overlapY <= overlapZ) {
info.penetration = overlapY;
info.normal = (a.center().y < b.center().y)
? glm::vec3(0, -1, 0) : glm::vec3(0, 1, 0);
} else {
info.penetration = overlapZ;
info.normal = (a.center().z < b.center().z)
? glm::vec3(0, 0, -1) : glm::vec3(0, 0, 1);
}
return info;
}
めり込み解消
void resolveCollision(GameObject& obj, const CollisionInfo& info) {
if (!info.collided) return;
// めり込み分だけ押し戻す
obj.position += info.normal * info.penetration;
// 衝突面に沿った速度成分だけ残す
float normalVelocity = glm::dot(obj.velocity, info.normal);
if (normalVelocity < 0) {
obj.velocity -= info.normal * normalVelocity;
}
}
実装例:完全なcollision.h
#pragma once
#include <glm/glm.hpp>
#include <algorithm>
struct AABB {
glm::vec3 min;
glm::vec3 max;
AABB() : min(0.0f), max(0.0f) {}
AABB(glm::vec3 min, glm::vec3 max) : min(min), max(max) {}
static AABB fromCenterSize(glm::vec3 center, glm::vec3 size) {
glm::vec3 halfSize = size * 0.5f;
return AABB(center - halfSize, center + halfSize);
}
glm::vec3 center() const { return (min + max) * 0.5f; }
glm::vec3 size() const { return max - min; }
bool contains(glm::vec3 point) const {
return point.x >= min.x && point.x <= max.x &&
point.y >= min.y && point.y <= max.y &&
point.z >= min.z && point.z <= max.z;
}
bool intersects(const AABB& other) const {
return min.x <= other.max.x && max.x >= other.min.x &&
min.y <= other.max.y && max.y >= other.min.y &&
min.z <= other.max.z && max.z >= other.min.z;
}
AABB offset(glm::vec3 offset) const {
return AABB(min + offset, max + offset);
}
};
struct Sphere {
glm::vec3 center;
float radius;
bool intersects(const Sphere& other) const {
return glm::length(center - other.center) <= (radius + other.radius);
}
bool intersects(const AABB& box) const {
glm::vec3 closest;
closest.x = std::max(box.min.x, std::min(center.x, box.max.x));
closest.y = std::max(box.min.y, std::min(center.y, box.max.y));
closest.z = std::max(box.min.z, std::min(center.z, box.max.z));
return glm::length(closest - center) <= radius;
}
};
struct Ray {
glm::vec3 origin;
glm::vec3 direction;
bool intersects(const AABB& box, float& t) const {
float tMin = 0.0f, tMax = FLT_MAX;
for (int i = 0; i < 3; i++) {
if (std::abs(direction[i]) < 1e-6f) {
if (origin[i] < box.min[i] || origin[i] > box.max[i]) return false;
} else {
float t1 = (box.min[i] - origin[i]) / direction[i];
float t2 = (box.max[i] - origin[i]) / direction[i];
if (t1 > t2) std::swap(t1, t2);
tMin = std::max(tMin, t1);
tMax = std::min(tMax, t2);
if (tMin > tMax) return false;
}
}
t = tMin;
return true;
}
};
ゲームでの使用例
class GameObject {
public:
glm::vec3 position;
glm::vec3 velocity;
AABB boundingBox;
void update(float deltaTime) {
position += velocity * deltaTime;
boundingBox = AABB::fromCenterSize(position, glm::vec3(1.0f));
}
};
void gameLoop() {
std::vector<GameObject> objects;
// 衝突判定
for (size_t i = 0; i < objects.size(); i++) {
for (size_t j = i + 1; j < objects.size(); j++) {
if (objects[i].boundingBox.intersects(objects[j].boundingBox)) {
// 衝突した!
auto info = getCollisionInfo(objects[i].boundingBox,
objects[j].boundingBox);
resolveCollision(objects[i], info);
}
}
}
}
最適化:空間分割
オブジェクトが100個あると、総当たりで 100×99/2 = 4950 回の判定が必要。
空間分割で減らせる:
グリッド分割
class SpatialGrid {
std::unordered_map<int, std::vector<GameObject*>> cells;
float cellSize;
int hash(glm::vec3 pos) {
int x = (int)(pos.x / cellSize);
int y = (int)(pos.y / cellSize);
int z = (int)(pos.z / cellSize);
return x + y * 1000 + z * 1000000;
}
public:
void insert(GameObject* obj) {
cells[hash(obj->position)].push_back(obj);
}
std::vector<GameObject*> getNearby(glm::vec3 pos) {
// 周囲のセルのオブジェクトだけ返す
}
};
他の手法
- Octree: 八分木。3D向け
- BVH: バウンディングボリューム階層。レイトレ向け
- Sweep and Prune: 軸ソートして重なりを検出
デバッグ表示
当たり判定は見えないので、デバッグ時は可視化すると便利:
void renderAABB(const AABB& box, glm::vec3 color) {
// ワイヤーフレームで描画
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
// キューブを描画
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
まとめ:シリーズ完結!
7回に渡って、ゲームエンジンの基礎を作ってきた。
作ったもの
- Part1: 全体設計
- Part2: GLFWでウィンドウ
- Part3: OpenGLで三角形
- Part4: シェーダー(uniform、アニメーション)
- Part5: テクスチャ(stb_image、UV座標)
- Part6: 3Dモデル(OBJパーサー、Phongライティング)
- Part7: 当たり判定(AABB、球、レイキャスト)
次のステップ
これで基礎はできた。ここから先は:
- Entity-Component-System(ECS)パターン
- 物理エンジン(重力、反発、摩擦)
- サウンド(OpenAL)
- UI(ImGui)
- シーン管理
- アセット管理
自分だけのゲームエンジンを育てていこう。
おわりに
ゲームエンジンを作ると、ゲームの仕組みがわかる。
UnityやUnrealを使うにしても、中身を理解してると強い。
最終的なコードは全部GitHubに上げてあるので、参考にどうぞ。
お疲れ様でした!