ホーム < ゲームつくろー! < デザインパターン習得編

Observer
  〜変化したら教えてください


@ 変化はデータに、結果はビューに

 ゲームは状態の変化の連続です。刻々と換わる画面を見ていると、それが良く分かると思います。ところで、実際に変化しているのは画面ではなく「データ」です。画面はそれを反映した結果に過ぎません。

 キャラクタの位置を更新して、それを描画する次のようなプログラムは良くあると思います。

class Character
{
protected:
   BITMAP m_bmp;   // キャラクタの絵
   int m_x;   // 横位置
   int m_y;   // 縦位置

public:
   virtual void Draw(){    // 画面更新
      // 指定の位置に絵を描画
   }
 
  void SetPos(int x, int y);
};


main()
{
   Character *Chara = new Character;
   while(1)
   {
       // キャラクタの位置を更新
       Chara->SetPos(x, y);
      Chara->Draw();   // 画面を更新
   }
}

 CharacterクラスのDraw関数が呼ばれるたびに、キャラクタを指定の位置に描画します。Draw関数をオーバーライドさせて描画の変化に対処しようというもくろみも見えます。

 さて、上のキャラクタを「DirectXで描画したい」となったらどうでしょう。こういうことはよくあります。Draw関数をvirtualで定義していたので、それをオーバーライドします。

class CharacterForDirectX : public Character
{
protected:
   IDirect3DTexture9 *m_pTexture;   // キャラクタの絵のテクスチャ
   int m_z;    // z座標が必要

public:
   virtual void Draw(){
      // キャラクタの絵のテクスチャをなんとか描画
   }
};

 さらに、キャラクタに名前を加え、プレイステーション2で動かせるようにしてしまおうなどと考えると、また派生で解決しますでしょうか?

class CharacterExForPS2 : public Character
{
protected:
   int m_z;    // z座標が必要
   string m_Name;  // キャラクタの名前
   PS2Device *m_PS2Dev;   // プレイステーション2のデバイスやら何たら

public:
   virtual void Draw(){
      // キャラクタをPS2でなんとか描画
   }
};


何だか作業が煩雑な気がしませんでしょうか?今キャラクタクラスしか見ていないのでまだ良いのですが、テキストクラスにもきっと同じ作業をする必要がありますし、Draw関数を派生させたすべてのクラスをさらに派生させてPS2用に改良する必要があります。

 これは設計として良くありません。データとなる部分と、描画部分が一緒になっているのが元凶です。データという数値的な部分と、描画というデバイス依存がはなはだしい部分を1つのクラスの機能に入れてしまうのは、移植性を著しく低めてしまうのです。

 ここで、ようやくObserverパターンの出番です。Observerパターンは、データとなる部分と、それを取り扱う人を用意し、データに変更があれば、それを取り扱う人に伝える事により、両者を分離する働きをなします。
 上の例だと、キャラクタの位置の情報、キャラクタの名前がデータにあたり、キャラクタの絵や描画するという行為は描画に関する部分なので「取り扱う人」に分類されるでしょう。



A Observerパターンの振る舞い

 このパターンにはSubjectクラスとObserverクラスが登場します。Subjectとは「対象」の意味ですが、データを持っている人と考えれば間違いありません。一方、Observerは「観察者」という意味です。何を観察しているかというと、Subjectの変化を観察しているんです。Subjectが別の状態になったら、Observerは直ちにそれをキャッチして、他のオブジェクトに通知します。つまり、Observerは仲介人なんですね(Mediatorパターンに似ているといえば似ています)。

 まずSubjectは、観察者であるObserverオブジェクトへのポインタを複数保持します(1つでもかまいません)。自身が更新したらNotify関数という更新通知関数を呼び出します。この関数内では登録されているObserverに「私は更新されましたよ」と通知します。ちなみに、Notifyとは「通知する」という意味です。

Subject::Notify(){
   for(int i=0; i<ObsNum; i++)
      Observer[i]->Update(this);   // thisが「私」
}

 ObserverクラスはUpdate関数を持っていて、この関数内でSubectオブジェクトから必要なデータを取得します。

Observer::Update(Subject *sub){
   // 引数のポインタが必要な対象(Subject)ならばデータを得る
   if(sub == MySubject)
      m_pos = MySubject->GetPos();   // MySubjectはSubjectクラスの派生オブジェクト
}

 Update関数内ではMySubjectと名づけたポインタからGetPos関数を呼び出していますが、これはSubjectクラスには無い関数です。このことから、Observer関数は、Subjectの具体的な派生オブジェクトを持つ必要があるのがわかります。MySubjectは、

CharacterSubject *MySubject;

と派生クラスが定義されているわけです。

ここで、Observerパターンのクラス図を示しましょう。


 複雑そうに見えますが、繋がりを目で追っていくと実は簡単です。まず、SubjectはObserverの登録関数と通知関数Notifyを持っているだけです。派生クラスConcreteSubjectでは、さらにデータを取り出すGetState関数を追加して、実用的にしています。一方ObserverクラスはUpdate関数を持っているだけです。これは、Subjectからの通知を専門に受ける事に徹しているわけですね。Observerクラスの派生クラス(ConcreteObserver)では「派生Subjectクラス(ConcreteSubject)」を持っていて、Update関数内でデータを取り出しています。上で説明したとおりの図になっているのがわかります。



B ゲームにOvserverパターンは使えるのか?

 Observerパターンはクラス間に確実な主従関係があり、システム全体に矛盾を生じさせたくない場合に使用します。殆どはデータと描画になるような気がしますが、そういう関係ならば有効に利用できます。

 このパターンは、データの変更があるたびにObserverが呼ばれます。しかし、ゲームは60分の1秒という非常に短い間に効率よく画面を作っていかなければなりません。60分の1秒の間に100個のデータに変更があったとき、毎回描画も変更していては非効率でフレームレイトも安定しないでしょう。また、ゲームの場合、60分の1秒ごとに更新することが分かっているのですから、Subjectからわざわざ報告してもらう必要はありません。ということは、ゲーム製作でObserverパターンは必要ないのでしょうか?

 まったく必要がないというわけではないと思いますが、積極的に取り入れられるかというと、Yesとは言い切れない部分があります。昨今のゲームは3Dバリバリでして、画面が動いているのが普通だったりします。変更が殆どであるときに、「変更しました」と通知してもらっても、それはあまり効率が高くなるとは思えません。「変更のあったデータだけを更新する」という使い道はできると思いますが、ある変更が他にどう影響するかが不透明な場合は、逆にアルゴリズムを複雑にしてしまい、保守管理の難しいプログラムになることもあります。

 ゲーム製作でObserverパターンの考え方が利用できるといえば、「データと描画を分離する」という部分に尽きるように思えます。ゲーム性はデータにすべて反映されますから、描画部分を差し替えれば、大きな変更無く他の媒体に移植できます(直接は難しいですが)。また、描画をあまり気にせずにデータ部分でゲーム製作ができるというのも、このパターンの魅力だと思います。