【ゲーム制作】DirectXでOBBの当たり判定をする方法|回転した矩形を初心者向けに解説

サムネイル画像 ゲーム制作

今回は、DirectXでの「OBBの当たり判定をする方法」について解説していきます。

「AABBは分かったけど、回転した四角形には使いづらい…」
「斜め向きの敵や弾の当たり判定を自然にしたい」
「OBBって難しそうだけど、どう考えればいいの?」

こんな疑問はありませんか?

ゲーム制作では、オブジェクトが回転し始めると、AABBだけでは見た目と判定がズレやすくなります。そんな時に役立つのが、回転した矩形そのものを扱えるOBBです。

この記事を読み終えると、あなたはOBBとは何か、AABBとの違い、2Dでの基本的な判定方法、実装の流れを理解できるようになると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

OBBとは?

OBBとは、Oriented Bounding Box の略です。

AABBが「座標軸に平行な四角形」だったのに対して、OBBは向きを持った四角形です。つまり、オブジェクトが回転していても、その回転に合わせた矩形で当たり判定ができます。

たとえば、斜めに傾いた剣、回転する障害物、向きのある敵などでは、OBBの方が見た目に近い判定を取りやすいです。

なぜOBBが必要?

AABBは軽くて扱いやすいですが、回転したオブジェクトには少し弱いです。

  • AABBだと、回転した見た目に対して判定が大きくなりやすい
  • 角で当たっていないのに当たり判定だけ触れることがある
  • 斜めオブジェクトの接触が不自然になりやすい

こういう時にOBBを使うと、回転した見た目に近い四角形で当たり判定が取れるので、プレイ感がかなり自然になります。

まずAABBの基本から整理したい方は、DirectXでAABBの当たり判定をする方法の記事を先に読むのがおすすめです。

AABBとの違い

判定方法特徴向いている場面
AABB軸に平行な四角形。軽くて実装しやすい壁、床、UI、回転しない物体
OBB回転した四角形を扱える回転する敵、斜めの障害物、向きのあるオブジェクト

つまり、回転しないならAABB、回転するならOBBを検討するという考え方でかなり分かりやすいです。

OBB判定の考え方

2DのOBB判定では、よくSAT(分離軸定理)という考え方を使います。

少し難しく聞こえますが、考え方はそこまで複雑ではありません。

  • 四角形には「向き」がある
  • その向きに沿って、お互いの形を投影する
  • どこか1本でも重なっていない軸があれば、当たっていない
  • 全部の軸で重なっていれば、当たっている

2Dの矩形同士なら、確認する軸は基本的に4本です。自分の横軸・縦軸、相手の横軸・縦軸を使って判定します。

OBBはどういう原理で当たり判定しているの?

ここが一番わかりにくいポイントだと思います。

AABBなら、上下左右で離れているかを見れば判定できました。ですが、OBBは四角形が回転しているので、単純に「x座標」「y座標」だけでは判断しにくくなります。

そこでOBBでは、「その四角形が向いている方向に沿って見た時に、重なっているか」を調べます。

少しイメージしやすく言うと、斜めに回転した四角形をそのまま見るのではなく、特定の方向から押しつぶして影のようにした時、その影が重なっているかを見る感じです。

SAT(分離軸定理)という考え方

OBBの判定でよく使うのが、SAT(分離軸定理)です。

名前は少し難しいですが、考え方はかなりシンプルです。

  • もしどこか1本でも「重なっていない向き」が見つかれば、2つの四角形は当たっていない
  • 逆に、調べたすべての向きで重なっていれば、当たっている

つまり、「離れている方向が1つでもあるか」を探しているわけです。

なぜ4本の軸を見るの?

2DのOBB同士なら、基本的に確認する軸は4本です。

  • Aの横方向
  • Aの縦方向
  • Bの横方向
  • Bの縦方向

四角形は「横」と「縦」の2つの向きを持っています。 そして相手側も同じように2つの向きを持つので、合計4本です。

この4本のどれかで重なっていなければ、当たり判定は false になります。

コードの中で何をしているの?

前回のコードでは、ざっくり次のことをしています。

  • GetAxisX() で四角形の横方向を取る
  • GetAxisY() で四角形の縦方向を取る
  • Dot() で、その方向にどれだけ伸びているかを調べる
  • 中心同士の距離と、両方の「影の長さ」を比べる

ここで大事なのは、四角形そのものを直接ぶつけているわけではなく、各軸に投影した“影”を比べているということです。

「影」で考えるとわかりやすい

たとえば、斜めの棒を机に置いて、真上からライトを当てると影ができますよね。

OBBの判定では、あの「影の重なり」を4方向ぶん確認しているイメージです。

もし1つの方向でも影が離れていたら、実物もどこかで離れているので当たっていません。 逆に、どの方向から見ても影が重なっているなら、実物も重なっていると判断できます。

OverlapOnAxis() がやっていること

前回コードの OverlapOnAxis() は、まさにその「ある1本の軸で影が重なっているか」を調べる関数です。

中では次の2つを比べています。

  • 中心同士が、その軸方向にどれだけ離れているか
  • それぞれの四角形が、その軸方向にどれだけ広がっているか

そして、

中心同士の距離 ≤ Aの広がり + Bの広がり

なら、その軸では重なっていると判断しています。

逆にこの条件を満たさなければ、その軸で分離しているので、もうその時点で「当たっていない」と分かります。

なぜ内積(Dot)を使うの?

Dot() は内積です。

初心者向けにかなりざっくり言うと、「ある方向に対して、どれだけ成分を持っているか」を調べるために使っています。

OBBでは、「この向きに対して中心がどこにあるか」「この向きに対して横幅と縦幅がどれだけ影になるか」を知りたいので、内積がちょうど便利です。

初心者向けに超ざっくりまとめると

  • OBBは回転した四角形
  • 判定では、四角形を直接見るのではなく、いくつかの向きに押しつぶした影で考える
  • その影が1つでも離れていれば、当たっていない
  • 全部の方向で影が重なっていれば、当たっている

つまりOBBは、「回転した四角形を、向きごとの影に分解して比較する判定」と考えるとかなり理解しやすいです。

基本の実装方法

少し難しく見えるかもしれませんが、やっていることは「4本の軸で影が重なっているか」を順番に確認しているだけです。ここを意識しながらコードを見ると、だいぶ追いやすくなります。

まずは、2DのOBB同士を判定するシンプルな実装例を見てみましょう。

▼main.cpp

#include <iostream>
#include <cmath>

struct Vec2 {
    float x;
    float y;
};

struct OBB {
    Vec2 center;
    float halfWidth;
    float halfHeight;
    float angle;
};

float Dot(const Vec2& a, const Vec2& b) {
    return a.x * b.x + a.y * b.y;
}

Vec2 GetAxisX(const OBB& box) {
    return { std::cos(box.angle), std::sin(box.angle) };
}

Vec2 GetAxisY(const OBB& box) {
    return { -std::sin(box.angle), std::cos(box.angle) };
}

float DegreeToRadian(float degree) {
    return degree * 3.14159265f / 180.0f;
}

bool OverlapOnAxis(const OBB& a, const OBB& b, const Vec2& axis) {
    Vec2 axA = GetAxisX(a);
    Vec2 ayA = GetAxisY(a);
    Vec2 axB = GetAxisX(b);
    Vec2 ayB = GetAxisY(b);

    float aCenter = Dot(a.center, axis);
    float bCenter = Dot(b.center, axis);

    float aRadius =
        a.halfWidth  * std::fabs(Dot(axA, axis)) +
        a.halfHeight * std::fabs(Dot(ayA, axis));

    float bRadius =
        b.halfWidth  * std::fabs(Dot(axB, axis)) +
        b.halfHeight * std::fabs(Dot(ayB, axis));

    return std::fabs(aCenter - bCenter) <= (aRadius + bRadius);
}

bool IsHit(const OBB& a, const OBB& b) {
    Vec2 axes[4] = {
        GetAxisX(a),
        GetAxisY(a),
        GetAxisX(b),
        GetAxisY(b)
    };

    for (int i = 0; i < 4; i++) {
        if (!OverlapOnAxis(a, b, axes[i])) {
            return false;
        }
    }

    return true;
}

int main() {
    OBB player = { {100.0f, 100.0f}, 40.0f, 20.0f, DegreeToRadian(20.0f) };
    OBB enemy  = { {145.0f, 115.0f}, 35.0f, 25.0f, DegreeToRadian(-15.0f) };

    if (IsHit(player, enemy)) {
        std::cout << "当たっています\n";
    } else {
        std::cout << "当たっていません\n";
    }

    return 0;
}

実行結果

当たっています

このコードでは、中心座標、半分の幅、半分の高さ、回転角度を持つOBBを使っています。角度はラジアンで扱っているので、例では度数法から変換しています。

実践例

たとえば、回転する障害物とプレイヤーの接触判定はこんな形で使えます。

▼main.cpp

#include <iostream>
#include <cmath>

struct Vec2 {
    float x;
    float y;
};

struct OBB {
    Vec2 center;
    float halfWidth;
    float halfHeight;
    float angle;
};

float Dot(const Vec2& a, const Vec2& b) {
    return a.x * b.x + a.y * b.y;
}

Vec2 GetAxisX(const OBB& box) {
    return { std::cos(box.angle), std::sin(box.angle) };
}

Vec2 GetAxisY(const OBB& box) {
    return { -std::sin(box.angle), std::cos(box.angle) };
}

float DegreeToRadian(float degree) {
    return degree * 3.14159265f / 180.0f;
}

bool OverlapOnAxis(const OBB& a, const OBB& b, const Vec2& axis) {
    Vec2 axA = GetAxisX(a);
    Vec2 ayA = GetAxisY(a);
    Vec2 axB = GetAxisX(b);
    Vec2 ayB = GetAxisY(b);

    float aCenter = Dot(a.center, axis);
    float bCenter = Dot(b.center, axis);

    float aRadius =
        a.halfWidth  * std::fabs(Dot(axA, axis)) +
        a.halfHeight * std::fabs(Dot(ayA, axis));

    float bRadius =
        b.halfWidth  * std::fabs(Dot(axB, axis)) +
        b.halfHeight * std::fabs(Dot(ayB, axis));

    return std::fabs(aCenter - bCenter) <= (aRadius + bRadius);
}

bool IsHit(const OBB& a, const OBB& b) {
    Vec2 axes[4] = {
        GetAxisX(a),
        GetAxisY(a),
        GetAxisX(b),
        GetAxisY(b)
    };

    for (int i = 0; i < 4; i++) {
        if (!OverlapOnAxis(a, b, axes[i])) {
            return false;
        }
    }

    return true;
}

int main() {
    OBB player   = { {220.0f, 180.0f}, 24.0f, 24.0f, DegreeToRadian(45.0f) };
    OBB obstacle = { {250.0f, 190.0f}, 50.0f, 16.0f, DegreeToRadian(30.0f) };

    if (IsHit(player, obstacle)) {
        std::cout << "障害物に当たっています\n";
    } else {
        std::cout << "まだ当たっていません\n";
    }

    return 0;
}

実行結果

障害物に当たっています

このように、回転しているプレイヤーや障害物でも、見た目に近い形で判定を取りやすくなります。

【重要】私が実際にOBBで困った体験談

私も自主制作のアクションゲームで、回転する障害物の当たり判定をAABBのまま使っていたことがあります。

その時は見た目ではまだ当たっていないのに、AABBだけ大きく広がっていて、プレイヤーが少し手前で引っかかるような状態になっていました。逆に、斜め向きの細い障害物では、見た目よりスカスカに感じる場所もありました。

そこでOBBに切り替えたところ、見た目と判定のズレがかなり減って、プレイ感がだいぶ自然になりました。ただ、その時に今度は角度を度数法のまま cos / sin に入れてしまって、判定が壊れたことがあります。

この時にかなり実感したのは、OBBは便利だけど、角度・中心座標・半サイズの意味をきちんと揃えないとすぐズレるということでした。

OBB使用時のよくある失敗例と対処法

  • 角度を度数法のまま使ってしまう
    std::cosstd::sin はラジアン前提です。度数法なら変換関数を通しましょう。
  • width と halfWidth を混同する
    今回の実装は「半分の幅」「半分の高さ」を使っています。全幅をそのまま入れると判定が大きくなります。
  • 中心座標ではなく左上座標を使ってしまう
    OBBは中心基準で考えると整理しやすいです。描画基準とズレないように注意しましょう。

クラスや構造体で分けておくと便利

オブジェクトが増えてきたら、OBBも別ファイルで管理した方が見やすくなります。

▼OBB.h

#pragma once

struct Vec2 {
    float x;
    float y;
};

struct OBB {
    Vec2 center;
    float halfWidth;
    float halfHeight;
    float angle;
};

▼Collision.h

#pragma once
#include "OBB.h"

bool IsHit(const OBB& a, const OBB& b);

こうしておくと、敵、プレイヤー、障害物など、いろいろな場所で同じ判定処理を再利用しやすいです。

クラスや構造体で整理する考え方は、C++のクラスの記事もあわせて読むと理解しやすいです。

注意点

  • OBBはAABBより自然な判定を取りやすいですが、そのぶん実装は少し複雑です
  • 回転しないオブジェクトまで全部OBBにする必要はありません
  • まずはAABBで作って、回転物だけOBBにする方が実践的です

特に当たり判定のズレが気になる時は、ログ出力やブレークポイントで中心座標・角度・半サイズを確認すると原因を追いやすいです。デバッグ方法はC++でのデバッグのやり方の記事も参考になります。

まとめ

  • OBBは、向きを持った矩形の当たり判定
  • 回転したオブジェクトに対して、AABBより自然な判定を取りやすい
  • 2DではSATの考え方を使うと実装しやすい
  • 角度はラジアン、サイズは半分で管理するとミスを減らしやすい
  • 回転しない物はAABB、回転する物はOBBと使い分けるのが実践的

OBBは最初こそ少し難しく見えますが、回転するオブジェクトを扱うようになるとかなり便利です。

AABBでは違和感が出てきたタイミングでOBBを導入すると、ちょうど理解しやすいと思います。

ここまで読んでくださり、ありがとうございました。

関連記事