読者です 読者をやめる 読者になる 読者になる

ゲーム業界エンジニアの独り言

クライアント、サーバ、インフラなどなど興味あることなんでも紹介

ゲームプログラマのためのデザインパターン(ビジター)

今、「C++のためのAPIデザイン」を読んでます。 APIをラポン具デザインパターンについてまとめてみました。

ゲームプログラマのためのC++

ゲームプログラマのためのC++

デザインパターンとは・・・

デザインパターンはプログラム設計でよく見られる問題の特性と、 このような問題に対して最も一般的に採用されている解決策の両方の 両方をカプセル化した抽象構造です。

自分なりに解釈すると、 オブジェクト指向で設計する際に起こりやすい問題の解決策だったり、 再利用性や可読性を上げるための設計の考え方だと思ってます。

ゲーム開発において特に有効なデザインパターン

  • シングルトン
  • ファザード
  • オブザーバ
  • ビジター

今回はビジターを紹介します。

ビジターとは・・・

ビジターとは英語で訪問者を意味します。 ビジターは機能を訪問者であるビジターに記述し処理の追加を簡単にします。

ゲームプログラマーが直面する苛立たしい問題として、 ある特定の機能を実装し、複数の場所でその機能が必要となり コード内に同じような機能の複製が作成されてしまうことがあります。

この解決策になるのがビジターパターンです。

例:オブジェクト位置に関連する処理

オブジェクト位置に関係する処理で例えばで以下のようなものがあります。

  • プレイヤーから一定範囲内に存在する敵を全て検索する
  • プレイヤーから一定範囲内に存在するアイテムを全て検索する
  • プレイヤーから一番近くにあるアイテムを検索する
  • プレイヤーから一定範囲内に存在する敵の数を取得する

このようなオブジェクトの位置に対する処理は挙げればきりがありません。 他にもイベント発生トリガーや、セーブポイントなどゲームのほとんどが 同じような性質を持っています。

ビジターを使用しない実装例

// 敵クラス
class Enemy
{
public:
    Vector3D GetPosition(); // 位置を取得
    // その他、敵に関する処理
}
// アイテムクラス
class Item
{
public:
    Vector3D GetPosition(); // 位置を取得
    // その他、アイテムに関する処理
}
// セーブポイントクラス
class SavePoint
{
public:
    Vector2D GetLocation(); // 位置を取得(これは2Dで扱っている)
    // その他、セーブポイントに関する処理
}

これらのクラスはある場所に対するパブリックなアクセサを持っています。 しかし、位置の扱いが異なる場合もあります。(Vector3DとVector2Dなど) 他にも、アクセサの名前が異なる場合もあるでしょう。

こういった違いのためか位置に対する処理は、 以下のような形で記述されることが多いと思います。

// 敵管理クラス(敵の検索など処理を記述)
class EnemyManager
{
public:
    // 指定位置から範囲内の敵を全て検索
    vector<Enemy*> FindEnemyInRange(const Vector3D& pos, const float range);
    // 指定位置から範囲内の敵の数を取得
    int GetEnemyInRangeCount(const Vector3D pos, const float range);
}

コード内を見てみると、他にも ItemManager や SavePointManager など、 似たような処理の複製が存在していることに気づくと思います。

プロジェクトを開始したばかりなら、これらのオブジェクトクラスを 位置を扱う基底クラスから派生させる事で、複製を回避できます。 しかし、プロジェクトがかなり進行してからこの問題が発生した場合は そのような大幅なインターフェイスの変更は危険です。

この解決策になるのがビジターパターンです。 ビジターを使用するとクラスのインターフェイスは変更せず拡張が可能になります。

ビジターの実装

ビジターの実装には2つの主要なコンポーネントがあります。

1.訪問する全てのクラスに対して、訪問メカニズムを提供するために  ビジタークラスを記述します。  さまざまな複数のクラスを横断するビジターを、実装したい共通機能の  カテゴリ一つにつき一つ持つことができます。

class ObjectVisitor
{
public:
    // 訪問されるオブジェクトで使用される明示的な訪問メソッド
    virtual void VisitEnemy(Enemy *enemy) = 0;
    virtual void VisitItem(Item*item) = 0;
    virtual void VisitSavePoint(SavePoint*savepoint) = 0;
}

2.訪問を受ける各クラスのインターフェイスには  訪問を受け入れるメソッドを追加します。  ビジターはパブリックインターフェイスにアクセスできるようになります。

// 敵クラス
class Enemy
{
public:
    Vector3D GetPosition(); // 位置を取得
    // 訪問を受け入れるメソッド
    void AcceptVisitor(ObjectVisitor *visitor)
    {
        visitor->VisitEnemy(this);
    }
}
// アイテムクラス
class Item
{
public:
    Vector3D GetPosition(); // 位置を取得
    // 訪問を受け入れるメソッド
    void AcceptVisitor(ObjectVisitor *visitor)
    {
        visitor->VisitItem(this);
    }
}
// セーブポイントクラス
class SavePoint
{
public:
    Vector2D GetLocation(); // 位置を取得
    // 訪問を受け入れるメソッド
    void AcceptVisitor(ObjectVisitor *visitor)
    {
        visitor->VisitSavePoint(this);
    }
}

実際の使用例

上で記述したインターフェイスを使用し、 範囲内のオブジェクトを検索する例を見てみます。 (ビジター関係ない実装は省きます)

// 範囲内にオブジェクトが存在するか判定するビジタークラス
class FindRangeInObjectVisitor
{
public:
    // 検索位置を設定
    void SetPosition(const Vector3D pos) { pos_= pos; }
    // 検索範囲を設定
    void SetRange(const float range) { range_ = range; }
    // 結果を取得
    bool GetResult() { retutn result_; }

    // 訪問されるオブジェクトで使用される明示的な訪問メソッド
    void VisitEnemy(Enemy *enemy);
    void VisitItem(Item*item);
    void VisitSavePoint(SavePoint*savepoint);

private:
    Vector3D pos_; // 検索位置
    float range_; // 検索範囲
    bool result_; // 判定結果

    // 範囲内に位置が存在するか判定する処理
    bool IsRangeIn(const Vector3D pos);
}
// 敵の訪問メソッド
void FindRangeInObjectVisitor::VisitEnemy(Enemy *enemy)
{
    // enemy にアクセスし範囲内か判定
    if (IsRangeIn(enemy->GetPosition()))
        result_ = true;
}

// アイテムの訪問メソッド
void FindRangeInObjectVisitor::VisitItem(Item *item)
{
    // item にアクセスし範囲内か判定
    if (IsRangeIn(enemy->GetPosition()))
        result_ = true;
}

// セーブポイントの訪問メソッド
void FindRangeInObjectVisitor::VisitItem(SavePoint *savepoint)
{
    // savepoint にアクセスし範囲内か判定
    // Vector2D → Vector3D への変換も吸収
    Vector3D pos;
    pos.x = savepoint->GetLocation().x;
    pos.y = savepoint->GetLocation().y;
    pos.z = 0.f;
    if (IsRangeIn(pos))
        result_ = true;
}
Enemy *target_enemy = GetTargetEnemy(); // 範囲内判定を行う敵のインスタンス

// 範囲内検索のビジタークラス
FindRangeInObjectVisitor visitor;
visitor.setPosition(GetPlayerPosition());
visitor.setRange(10.0f);

// ビジターが敵インスタンスを訪問し範囲内判定
target_enemy->AcceptVisitor(visitor);

if (visitor->GetResult())
{
    // 指定の敵は範囲内に存在
}

各オブジェクトに訪問メソッドを追加し、ビジターを迎えることで ビジターはオブジェクトのパブリックインターフェイスにアクセスし、 情報を収集することが出来ます。 検索機能もビジターに実装するため、機能の複製も避けられます。