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

Delegate
  〜いろんなクラスのメソッドの代表者になれる!〜



 超久しぶりにデザインパターン編を更新です。本章で扱う「Delegete」はGoFには含まれておりませんが、ゲーム製作に非常に有用で且つ多用される仕組みであるためここで取り上げる事にしました。ちょっとややこしいですが、いつものようにじっくり参りましょう(^-^)



@ Delegateって何?

 そもそも、Delegateとは何なのか?そのイメージが重要ですよね。Delegateは日本語で「代表者」です。何の代表者かというと「関数」の代表者なんです。簡単に言えば、代表者の中に関数を登録すると、それを持ち運びできて、他の人がいつでもどこでもそれを実行できてしまう機能を持ちます:


 この一番の使い道として、例えばゲーム中に何らかのイベント(メッセージ)が発生した時に、それに呼応するイベントを起こすというのがあります。あるクラス内にデリゲータさんを沢山保持しておいて、イベントが発生した時にそれをどばっと実行する。しかも、デリゲータさんはオブジェクトなので、クラスの中から動的に取り外しができます:


 これによって、イベント発生とそれに呼応する処理を分離できるため、自由度の高い振る舞いを起こす事ができます。



A クラスのメソッドを持たせるのは大変なんです

 @のDelegateの話を実現するには何をする必要があるのか?ここからはそれを紐解いていこうと思います。

 あるクラスの中で他のオブジェクトのメソッドを呼ぶ一番簡単な方法は、そのオブジェクトを保持してメソッドを直接呼び出す事ですよね。例えば下記のような方法です:

// メイン関数
int main() {
   // ドアベルに音を設定
   DoorBell doorBell;
   Sound doorBellSound( "DoorBell.wav" );
   doorBell.setSoundOnOpenDoor( doorBellSound );   // ドアベルの音を登録

   ...
}

// DoorBellクラス実装部

// ドアが開いた時のイベントハンドラ
void DoorBell::OnOpenDoor() {
   m_doorBellSound.play();   // チリ〜ン
}

 main関数内でドアベルオブジェクトにドアベルの音(サウンド)を登録しておきます。ドアが開いた時に呼び出されるOnOpenDoorメソッドが実行されたら、内部でサウンドを「チリ〜ン」と鳴らせば(Sound::playメソッド)、イベントに対して呼応した事になります。

 しかし、言わずもがなですが、上の実装だとSoundという特定のクラスをDoorBellさんが知っていなければなりません。もしドアが開いた時にドアベルが揺れるとか、音だけじゃなくてBGMが流れるとか、近くの犬が庭駆け回るとか、要は「何が起こるかわからない」という状況だったらどうでしょう?DoorBellさんはそれに対応するために「世の中のすべて」を知る必要があります。それじゃ…どうしようもないですよね:

こんな事は非現実↓
// DoorBellクラス実装部

// ドアが開いた時のイベントハンドラ
void DoorBell::OnOpenDoor() {
   if ( m_doorBellSound )
      m_doorBellSound->play();   // チリ〜ン
   if ( m_dog )
      m_dog->AroundGaden();      // わんわん

   ...世界中のオブジェクトを検査...
}


 これを打開するために「関数ポインタ」をドアベルクラスに登録するのが非常に有用な方法です:

// 犬が庭を駆け回る関数
void runDogAroundGarden( int value ) {
   // わんわん
}

// ドアベルの音が鳴る関数
void playDoorBellSound( int value ) {
   Sound sound( "DoorBell.wav" );
   sound.play();    // チリ〜ン
}

int main() {
  // ドアベルにイベント関数を登録
  DoorBell doorBell;
  doorBell.setOnOpenDoorHandler( runDogAroundGarden );   // 庭駆け回るイベント
  doorBell.setOnOpenDoorHandler( playDoorBellSound  );   // チリ〜ンイベント
}


typedef void (*EventFunc)( int );

// ドアが開いた時のイベントハンドラ

void DoorBell::OnDoorOpen() {

   // 登録されている関数を実行する
   list< EventFunc >::iterator it  = m_eventList.begin();
   list< EventFunc >::iterator end = m_eventList.end();

   for ( ; it != end; it++ ) {
      EventFunc = *it;
      EventFunc( 0 );
   }
}


 これは先ほどよりも大分にましです。DoorBell::setOnOpenDoorHandlerメソッドは、ドアが開いた時に呼応する関数を登録します。登録された関数はDoorBell::OnDoorOpenメソッド内でリストをイテレートして実行されます。この実装なら、同じ関数型であればいくらでも登録できます。また、DoorBellクラスも「何が実行されるか」知る必要が無くなりました。世界を検査しなくて良くなったんです。


 しかしです!まだ困った事は起こります。庭に2匹の犬がいたらどうでしょう?しかも、ドアが開いた時に庭駆け回るのはそのうちの1匹だとしたら…。まさか犬1号用にrunDog1AroundGarden関数を作るわけにはいきません。世の中にいるオブジェクトすべての呼応関数を作るのはナンセンスでしょう。普通に考えると、まず犬クラスを作って、そのインスタンス(dog1)のAroundGardenメソッドを呼び出せば事足ります。

 ただし、この段階で登録すべき関数ポインタはグローバル関数から「特定のクラスのメソッドのポインタ」に変わっています。そして、この何気ない変化を吸収するのが言語の仕様上とてもやっかいなんです。



B テンプレートで少しましに

 ドアベルクラスに犬クラスのAroundGardenメソッドの関数ポインタを登録しようと次のように実装すると失敗します:

class Dog {
public:
   void AroundGarden( int value );   // 庭駆け回るアクション
};


typedef void (*EventFunc)( int );

// ドアベル開いた時のハンドラ登録
void DoorBell::setOnOpenHandler( EventFunc func ) {
   // ...省略ね
}


int main() {
   
   DoorBell doorBell;
   Dog dog1, dog2;    // 庭駆け回るのはdog1

   doorBell.setOnOpenHandler( &dog1.AroundGarden );   // 失敗!!
}

 setOpenHandlerメソッドにdog1.AroundGardenメソッドのポインタを渡そうとするとコンパイルエラーになってしまいます。これは単純に引数の型が違うからです。setOnOpenHandlerメソッドの引数とdog1.AroundGardenメソッドはそれぞれ次のような型になっています:

setOnOpenHandlerメソッドの引数  void (*)( int )
犬クラスのAroundGardenメソッド  void (Dog::*)( int )

 上は「戻り値が無くて引数にint型を取る関数型」です。一方下の犬クラスのAroundGardenメソッドは「戻り値が無くて引数にint型を取る『Dogクラス内の』関数型」です。両者は戻り値も引数も型は一緒なのですが、C++の中ではまったくの別物として扱われてしまいます。


 さぁ困りました。これが登録できないとドアが開いた時にイベントを起こせません。そこで、ちょっとだけ歩み寄ってみましょう。任意のクラスとまでは行かないまでも、ある1つのクラスのメソッドを呼べるようにする必殺技があります。それは「テンプレート」を使う事です:

template < class T >
class DoorBell {

   typedef void (T::*EventFunc)( int );

   struct EventInfo {
      T* object;         // T型のオブジェクトへのポインタ
      EventFunc func;    // int型を引数に持つTクラス内のメソッドへのポインタ
   };

   // ドアが開いた時のハンドラ登録
   void setOnOpenHandler( T* obj, EventFunc eventFunc ) {

      EventInfo ei;
      ei.object = obj;
      ei.func   = eventFunc;
      m_eventList.push_back( ei );
   }

   // ドアが開いた
   void OnOpenDoor() {

      list< EventInfo >::iterator it  = m_eventList.begin();
      list< EventInfo >::iterator end = m_eventList.end();

      for ( ; it != end; it++ ) {
         T* obj = it->object;
         EventFunc func = it->func;

         (obj->*func)( 0 );   // イベントハンドラ実行
      }
   }
};

 DoorBellクラスがテンプレート引数Tを取るようになりました。こうすると、setOnOpenHandlerメソッドにはクラスTが持つint型を引数に取る戻り値無しの関数ポインタであれば登録できるようになります。つまり、

DoorBell< Dog >        doorBellForDog;   // 犬庭駆け回り用
DoorBell< BellSound >  doorBellForSound; // 音鳴らし用

Dog dog1, dog2;
BellSound sound( "DoorBell.wav" );

doorBellForDog.setOnOpenHandler( &dog1, &Dog::AroundGarden );
doorBellForSound.setOnOpenHandler( &sound, &BellSound::Play );

このようにDoorBellのテンプレート引数に特定のクラスを指定すれば、そのクラスが持つメソッドを渡す事ができます。ちなみに、第1引数にオブジェクト自身を渡すのは、メソッドの実行対象が必要なためです(これはC++の仕様)。少しましにはなったのですが、でもこれだと1つのDoorBellオブジェクトに1つのクラスしか対応できません。惜しいんですが、ちょっと違いますね…。



C デリゲート登場

 ここから先の対策がデリゲートに繋がります。
Bまでの段階はかなり惜しいところまで行っています。もう少し発展させるとすべてが丸く収まります。

 今残された問題は、他のクラスのメソッドを持てないという点です。それを解決するために、おもむろですが次のようなクラスを用意します:

class DelegateBase {
public:
   DelegateBase(){};
   virtual ~DelegateBase(){};

   // int型を引数に持つ戻り値無しの関数のように呼べるようにせよ!
   virtual void operator()( int value ) = 0;
};

 このDelegateBaseクラスの中には演算子「()」が純粋仮想関数として宣言されています。具体的には、int型を引数に持つ戻り値無しの関数のように呼び出した時の処理を派生クラスで実装せよ!と言っているわけです。

 そこで、実際に派生クラスを作ります:

template < class T >
class Delegate : public DelegateBase {

public:
   Delegate(){};
   virtual ~Delegate(){};

   // オペレータ実装
   virtual void operator()( int value ) {
      (m_obj->*m_func)( value );   // ハンドラ実行!
   }


   typedef void (T::*EventFunc)( int );

   // オブジェクトとメソッドを登録
   void set( T* obj, EventFunc func ) {
      m_obj = obj;
      m_func = func;
   }

protected:
   T* m_obj;               // オブジェクト
   EventFunc m_func;       // 関数ポインタ
};

 このクラスはT型のオブジェクトとT型の関数ポインタをメンバ変数に持つ事で、オブジェクトの特定のメソッド(int型引数戻り値無し)を呼び出せます。例えば、

Delegate< Dog > onOpenDoorHandler;   ドアが開いた時のハンドラ
Dog dog1, dog2;

onOpenDoorHandler.set( &dog, &Dog::AroundGarden );  // 庭駆け回る関数登録

int value = 0;
onOpenDoorHandler( value );    // 庭駆け回る関数が実行される!!

このようにset関数でオブジェクトとメソッドを登録する事で、最下段のようにonOpenDoorHandler変数がまるで関数であるかのように振舞う事ができるようになります。これは演算子「()」を再定義しているからです。最下段のデリゲート変数がsetされた様々なメソッドの代表者(入れ物)のような振る舞いをするのがわかりますね。

 ただ、このままではDogというクラスのメソッドしか登録できないので、Bの実装と何も変わりません。そこで、先ほどのDelegateクラス内に次のような静的な生成関数を作ります:

class Delegate : public DelegateBase {

   ...中略

   // デリゲータ生成関数
   static DelegateBase* createDelegator( T* obj, void (T::*func)( int ) ) {
      Delegate* dg = new Delegate;
      dg->set( obj, func );
      return dg;
   }
};

 この静的な関数は内部でデリゲートオブジェクトを作っています。そしてそれを「DelegateBase型」として返しています。ここがポイントです!内部では特定のクラスTのオブジェクトとメソッドを保持していますが、戻された段階でそれが親の型として吸収されてしまいます。よって、例えば、

Dog dog1, dog2;
Cat cat1;

// デリゲータ作成
DelegateBase* pDG_dog = Delegate<Dog>::createDelegator( &dog1, &Dog::AroundGarden );
DelegateBase* pDG_cat = Delegate<Cat>::createDelegator( &cat1, &Cat::SleepInKotatsu );

int value = 0;
(*pDG_dog)( value );    // dog1.AroundGardenメソッドが実行される!
(*pDG_cat)( value );    // cat1.SleepInKotatsuメソッドが実行される!

と親クラスDelegateBaseが持つ演算子「()」を通して特定のメソッドを実行することが可能になります!



C 犬は庭駆け回り、猫はこたつで、ベルはチリ〜ン

 こうなるともう話は見えています。Dogクラスの庭駆け回りメソッド(AroundGarden)やSoundクラスの再生メソッド(Play)をデリゲータに包んであげます。DoorBellクラスはDelegateBaseクラスを知っていて、ドアが開いた時に実行すべきデリゲータを登録できるようにします:

Dog dog1, dog2;
Cat cat1;
Sound sound( "DoorBell.wav" );

// デリゲータに包んであげる
DelegateBase* AroundGarden_DG   = Delegate<Dog>::createDelegator( &dog1, Dog::AroundGarden );
DelegateBase* SleepInKotatsu_DG = Delegate<Cat>::createDelegator( &cat1, Dog::SleepInKotatsu );
DelegateBase* Play_DG           = Delegate<Sound>::createDelegator( &sound, Sound::Play );

// ドアベルに登録
DoorBell doorBell;
doorBell.setOnOpenDoorHandler( AroundGarden_DG );
doorBell.setOnOpenDoorHandler( SleepInKotatsu_DG );
doorBell.setOnOpenDoorHandler( Play_DG );

AroundGarden_DGもSleepInKotatsuもPlay_DGも同じDelegateBase型へのポインタですから、まったく同じように扱えるわけです。もちろん、DoorBell内部ではリストに登録され、ドアが開くイベントが発生した時にイテレータによって順次登録メソッドが実行されるわけです。ドアが静かに開いた時、犬1は庭駆け回り、猫はこたつで眠り、ドアベルはチリ〜ンと涼やかに音を鳴らす事でしょう。ここにデリゲートが完成しました〜〜!



 後は登録方法の簡潔化(+=や-=の演算子を作るのが良くあります)や、登録できるメソッドの種類を増やすなど工夫ができます。特定オブジェクトの特定メソッドを持ち運べられるなんて凄いなぁ〜と私などは思いました。考えた人に感謝ですね。