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

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

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

ゲームプログラマのためのデザインパターン(オブザーバ)

今、「ゲームプログラマのためのC++」を読んでます。 ゲームプログラムで使えるデザインパターンについてまとめてみました。

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

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

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

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

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

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

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

今回はオブザーバを紹介します。

オブザーバとは・・・

あるオブジェクトに何らかの変更が生じた場合に、 それに関係する別のオブジェクトに通知を行うメカニズムを提供します。

例:オブジェクトの参照保持

ゲーム中開発中に多く見られる問題の一つに、 オブジェクト(ObjectA)を保持している別のオブジェクト(ObjectB)が存在するのに オブジェクト(ObjectA)の削除を行ってしまう事が上げられます。

// 小さなクラス
class ObjectA
{
public:
    void ObjectA(void) {}
    ~ObjectA(void) {}

    void Update(void) {}
}

// ObjectAの参照を持つクラス
class ObjectB
{
public:
    void ObjectB(ObjectA *pObjectA) : m_pObjectA(pObjectA)
    {
    }
    ~ObjectB(void) {}

    void Update(void)
    {
        if (m_pObjectA != NULL)
            m_pObjectA->Update();
    }

private:
    ObjectA* m_pObjectA;
}
ObjectA *pObjectA = new ObjectA();
ObjectB *pObjectB = new ObjectB(pObjectA);
pObjectB->Update(); // pObjectB を更新、問題なし
delete pObjectA;    // pObject Aを削除
pObjectB->Update(); // pObjectB を更新、失敗

2つの目の pObjectB->Update() が失敗する理由は pObjectA が削除され、 pObjectB が持つ pObjectA への参照が無効になりアクセスに失敗するからです。

問題のある解決策

解決策の一つに、ObjectA が ObjectB を知っているというものがあります。

// 小さなクラス
class ObjectA
{
public:
    ~ObjectA(void)
    {
        m_pObjectB->NotifyObjectADestruction();
    }

private:
    ObjectB* m_pObjectB;
}

// ObjectAの参照を持つクラス
class ObjectB
{
public:
    void NotifyObjectADestruction(void)
    {
        // m_pObjectA が削除された時に通知を貰い参照を無効化
        m_pObjectA = NULL;
    }
}

しかし、この解決策は問題点があります。

  • 参照を無効化だけでなく、他にも通知を行いたい
  • ObjectA だけではなく 他のオブジェクトの参照も保持したい

などの変更が起こった場合に、ソースは冗長になり複雑さを増します。

オブザーバの実装

この解決策はオブザーバです。 オブザーバはサブジェクトと呼ばれる別のオブジェクトの変化を観察します。 1つのサブジェクトは複数のオブザーバを持つことが出来ます。 そして、全てのオブザーバに自分の変化を通知します。

// 基本のオブザーバクラス
class Observer
{
public:
    virtual ~Observer(void);
    virtual void Update(void) = 0;

    void SetObject(Subject* pSubject)
    {
        m_pSubject = pSubject;
    }

protected:
    Subject* m_pSubject; 
}

// 基本のサブジェクトクラス
class Subject
{
public:
    virtual ~Subject(void)
    {
        std::list<Observer *>::iterator it;
        for (it = m_observers.begin(); it != m_observers.end(); it++)
        {
            (*it)->SetObject(NULL);
        }
    }

    virtual void Update(void)
    {
        std::list<Observer *>::iterator it;
        for (it = m_observers.begin(); it != m_observers.end(); it++)
        {
            (*it)->Update();
        }
    }

    virtual  void AddObserver(Observer* pObserver)
    {
        m_observers.push_back(pObserver);
        pObserver->SetObject(this);
    }

protected:
    std::list<Observer *> m_observers;
}

Observer クラスは単純で監視するサブジェクトを知っているだけです。 Subject クラスは少し複雑で、オブザーバのリストを保持していて 自分に何か状態の変化があると保持しているオブザーバ達に通知を送ります。 ここでも自分が死んだこと(デストラクタ)自分が更新された事(Update メソッド)を オブザーバ達に通知します。

実際の使用例

実際に使用されている例を見てみます。

ここでは、STG でよく見る、 「メインウエポンの攻撃時に、サブウェポンも一緒に攻撃」を実装してみます。 (グラディウスみたいなやつです) ここではメインウェンポンが複数のサブウェポンを保持できる想定です。

// サブウェポン基礎クラス(オブザーバ)
class SubWeaponBase
{
public:
    virtual ~SubWeaponBase(void) {}
    virtual void Attack(void) = 0;

    void SetObject(MainWeapon* pMainWeapon)
    {
        m_pMainWeapon= pMainWeapon;
    }

protected:
    MainWeapon* m_pMainWeapon; 
}

// 実際のサブウェポンA(オブザーバ実装)
class SubWeaponA : public SubWeaponBase
{
public:
    SubWeaponA(void) {}
    virtual ~SubWeaponA(void) {}

    virtual void Attack(void)
    {
        // 攻撃処理
    }
}

// メインウェポン基礎クラス(サブジェクト)
class MainWeaponBase
{
public:
    MainWeaponBase(void) {}

    virtual ~MainWeaponBase(void)
    {
        std::list<SubWeaponBase*>::iterator it;
        for (it = m_subweapons.begin(); it != m_subweapons.end(); it++)
        {
            (*it)->SetObject(NULL);
        }
    }

    virtual void Attack(void)
    {
        // サブウェポンも攻撃
        std::list<SubWeaponBase*>::iterator it;
        for (it = m_subweapons.begin(); it != m_subweapons.end(); it++)
        {
            (*it)->Attack();
        }
    }

    virtual void AddSubweapon(SubWeaponBase* pSubweapon)
    {
        m_subweapons.push_back(pSubweapon);
        pObserver->SetObject(this);
    }

protected:
    std::list<SubWeaponBase*> m_subweapons;
}

// 実際のメインウェポンAクラス(サブジェクト実装)
class MainWeaponA : public MainWeaponBase
{
public:
    MainWeaponA(void) {}

    virtual ~MainWeaponA(void) {}

    virtual void Attack(void)
    {
        // 攻撃処理
        
        // 基礎クラスの攻撃処理を呼び出し(サブウェポンも攻撃)
        MainWeaponBase::Attack();
    }
}
MainWeaponA *pMainWeaponA = new MainWeaponA();
SubWeaponA *pSubWeaponA = new SubWeaponA();
// pMainWeaponA にオブサーバ(pSubWeaponA)追加
pMainWeaponA->AddSubweapon(pSubWeaponA);
// メインウェポン攻撃、オブサーバに追加したサブウェポンも攻撃
pMainWeaponA->Attack();
// メインウェポンを破棄、オブサーバのサブウェポンも攻撃
delete pMainWeaponA;