ゲームの当たり判定のクラス設計を考えてみる

アクション系やシューティングゲームを作っていく上でほぼ必ずといっていいほどぶち当たるのが当たり判定(コリジョン)です。

フィールドに複数のキャラクターが存在するときは、衝突し得るキャラクター同士すべてで当たり判定処理を行う必要があります。
しかしながら、この当たり判定処理はキャラクターの数が増えるほど重くなります。
したがって、不要なキャラクター同士との当たり判定はしないように工夫する必要があります。

今回はこれらの当たり判定処理を管理するためのクラス設計について考えてみました。

キャラクタには円形や四角形などさまざまな形状のキャラクタが存在すると思います。
地形も平面な形状のキャラクタと捉えることが出来るかもしれません。

形状を保持するクラスの例です。

//--------------------------------------
// 形状の種類
//--------------------------------------
enum SHAPE_TYPE {
    SHAPE_CIRCLE,
    SHAPE_RECT,

    SHAPE_TYPE_NUM
};

//--------------------------------------
// 形状の基底クラス
//--------------------------------------
class Shape {
public:
    virtual ~Shape() {}

    virtual SHAPE_TYPE GetType() const = 0;
};</pre>

//--------------------------------------
// 円形状クラス
//--------------------------------------
class CircleShape : public Shape {
public:
    CircleShape(float x, float y, float radius) :
        m_x(x), m_y(y), m_radius(radius) {}
    virtual ~CircleShape() {}

    SHAPE_TYPE GetType() const { return SHAPE_CIRCLE; }

    float GetX() const { return m_x; }
    float GetY() const { return m_y; }
    float GetRadius() const { return m_radius; }

private:
    float m_x, m_y;
    float m_radius;
};

//--------------------------------------
// 矩形形状クラス
//--------------------------------------
class RectShape : public Shape {
public:
    RectShape(float left, float bottom, float right, float top) :
        m_left(left), m_bottom(bottom), m_right(right), m_top(top) {}
    virtual ~RectShape() {}

    SHAPE_TYPE GetType() const { return SHAPE_RECT; }

    float GetLeft() const { return m_left; }
    float GetBottom() const { return m_bottom; }
    float GetRight() const { return m_right; }
    float GetBottom() const { return m_bottom; }

private:
    float m_left, m_bottom, m_right, m_top;
};

円と長方形が存在した場合は上記のように円と長方形の形状クラスを基底の形状クラスから派生させます。

形状同士の当たり判定の計算は、形状の組み合わせによって違ってきます。
上の例では、円と長方形が存在するので、円と円、円と長方形、長方形と長方形の3種類の当たり判定計算が存在します。
当たり判定計算も形状クラス同様に基底クラスから派生させます。

//--------------------------------------
// 当たり判定の基底クラス
//--------------------------------------
class Collision {
public:
    virtual ~Collision() {};

    virtual bool Test(const Shape& s1, const Shape& s2) = 0;
};

//--------------------------------------
// 円と円の当たり判定
//--------------------------------------
class CircleAndCircle : public Collision {
public:
    CircleAndCircle () {}
    virtual ~CircleAndCircle () {}

    bool Test(const Shape& s1, const Shape& s2) {
        計算あれこれ
    }
};

//--------------------------------------
// 円と長方形の当たり判定
//--------------------------------------
class CircleAndRect : public Collision {
public:
    CircleAndRect () {}
    virtual ~CircleAndRect () {}

    bool Test(const Shape& s1, const Shape& s2) {
        計算あれこれ
    }
};

//--------------------------------------
// 長方形と長方形の当たり判定
//--------------------------------------
class RectAndRect: public Collision {
public:
    RectAndRect() {}
    virtual ~RectAndRect() {}

    bool Test(const Shape& s1, const Shape& s2) {
        計算あれこれ
    }
};

そして、任意の形状のキャラクタ同士との当たり判定を行うクラスを実装します。

//--------------------------------------
// 任意の形状同士の当たり判定を行うクラス
//--------------------------------------
class FlexibleCollision : public Collision {
public:
    FlexibleCollision () {
        m_colTable[SHAPE_CIRCLE][SHAPE_CIRCLE] = new CircleAndCircle();
        m_colTable[SHAPE_CIRCLE][SHAPE_RECT]   = new CircleAndRect();
        m_colTable[SHAPE_RECT][SHAPE_CIRCLE]   = new CircleAndRect();
        m_colTable[SHAPE_RECT][SHAPE_RECT]     = new RectAndRect();
    }
    virtual ~FlexibleCollision () {
        for ( int i = 0 ; i < SHAPE_TYPE_NUM ; ++i ) {
            for ( int j = 0 ; j < SHAPE_TYPE_NUM ; ++j ) {
                delete m_colTable[i][j];
            }
        }
    }

    bool Test(const Shape& s1, const Shape& s2) {
        return m_colTable[s1.GetType()][s2.GetType()]->Test(s1, s2);
    }

private:
    // 当たり判定テーブル
    Collision* m_colTable[SHAPE_TYPE_NUM][SHAPE_TYPE_NUM];
};

もっとスマートなやり方があると思いますが(涙)、ひとまずこのような感じになります。

形状の種類に応じた当たり判定の計算は、当たり判定テーブル(m_colTable)で管理しています。
s1.GetType()、s2.GetType()の戻り値からオブジェクトの形状の種類を取得し、これをインデックスとしてm_colTableに与え、適切な当たり判定オブジェクトで判定するようにしています。

フィールドにはプレイヤーと敵と障害物が配置されているとします。
ここで、プレイヤーと敵、プレイヤーと障害物の当たり判定を行うとします。
(敵と障害物はすり抜けてもOK)
これらは、プレイヤーと敵と障害物の3種類のグループに分類して管理します。
そして、必要なグループ同士との当たり判定のみ行うようにします。

//--------------------------------------
// グループ単位の当たり判定を行う
//--------------------------------------
void RunCollisionTest(Collision& collision, list<shape*>& shapeGroup1, list<shape*>& shapeGroup2) {
    // グループ内のすべての組み合わせで当たり判定
    for ( auto it1 = shapeGroup1.begin() ; it1 != shapeGroup1.end() ; ++it1 ) {
        for ( auto it2 = shapeGroup2.begin() ; it2 != shapeGroup2.end() ; ++it2 ) {
            // 当たり判定の結果で分岐処理
            if ( collosion.Test(*it1, *it2) ) {
                当たり時の処理
            } else {
                はずれ時の処理
            }
        }
    }
}

        ・・・
{
    FlexibleCollision collosion;
    list<shape*> playerGroup;
    list<shape*> enemyGroup;
    list<shape*> obstacleGroup;

        ・・・

    // プレイヤーと敵との当たり判定
    RunCollisionTest(playerGroup, enemyGroup);

    // プレイヤーと障害物との当たり判定
    RunCollisionTest(playerGroup, obstacleGroup);

これで特定のグループ同士との当たり判定が出来るようになりました。

異なる形状同士の当たり判定には個別の計算が必要であること、キャラクタをグループで分類してグループ同士での当たり判定を行うことの2点を念頭に置いて設計していけばよいかと思います。
当たり判定の形状が増えるほど当たり判定の計算の種類も増えていくので、当たり判定クラスと当たり判定テーブルが複雑になっていきます。
形状の種類を最小限に抑えることも重要ではないかと思います。