ゲーム開発のためのオブジェクト指向〜Mediatorによる衝突判定の分離

実は、kenmoが今作っているゲームのソースコードが結構大変なことになっちゃっていたりします。
 
というのは、ゲームのメインとなるシーンのクラス(SceneMain)に、
たくさんのTask(オブジェクト)がぶら下がっているからです。
…えーと、こんな感じ(´д`;


まあ、ぶら下がっているTaskが2つぐらいなら、この方法でも良いのですが、
3つ以上になると、それぞれのTask同士の通信(主に当たり判定とか)が複雑になってしまい、
SceneMainクラスの仕事が増えたり、変なフラグが増えたりして、ブクブクと太ってしまいます。
 
…そんな感じで、
(あと、ほかにも5,6個Taskがあって)
おかげでSceneMainが1000行を突破しちゃっています。
(まあ、1000行くらいたいしたことないのかもしれませんが…)
 
 
そこで、
 
「何とかせねば…」
 
と考え、とりあえずリファクタリングした結果、こうなりました。

階層を1つ増やし、
Taskの生成・更新・削除を管理する、TaskManager(デザパタで言うFactoryクラス)
を導入しました。
 
これにより、SceneMainでnew/deleteを呼ぶ必要がなくなり、多少スッキリしました。
 
…しかし、これだけは単にオブジェクトの管理層を追加しただけです。
例えば「当たり判定」のような、Task同士(最下層)の通信をするためには、
あいかわらず、上位層のSceneMainまでTaskを引っ張り出す必要があります。
(最下層でお互いに通信することはできないので)
 
 
そこで、Mediatorパターンの登場です。
 
Mediatorとは「仲介人」の意味で、ここでは「最下層の通信を一元管理」する役割を持ちます。
つまり、
「Mediatorに当たり判定をやらせてしまおう!」
というわけです。
 
で、Mediatorを加えると、こんな感じです。

なんか線がたくさん増えて、キモイクラス図になってしまいましたが…(´д`;
 
 
これは何をしているかというと、各TaskManagerがMediatorを参照しています。
そして、TaskMediatorも各TaskManagerを参照しています。(相互参照ですね)
 
こうすることにより、
各TaskManagerに更新(update)があった場合、
それをTaskMediatorの「通知(update)」を呼び出すことにより、
TaskMediatorに当たり判定を任せてしまうことができます。
 
 
フローは以下の順番になります。

  1. SceneMainがTaskManagerのupdateを呼ぶ
  2. TaskManagerはTaskのupdateを呼び、Taskの座標を更新する
  3. その後、TaskManagerは自分を更新されたことをTaskMediatorに通知する。(TaskMediatorのupdateにthisポインタを渡して呼ぶ)
  4. TaskMediatorのupdateでは、渡されたTaskManagerのidを見て、それぞれの当たり判定を行う

 
コードの例としては、以下のようになります。
 
1.SceneMainクラス

SceneMain::update()
{
	// TaskManagerを全て実行
	Iterator it = taskManagerList.getIterator();
	while(it.hasNext())
	{
		((TaskManager)it.next()).update(); // ->2へ
	}
}

なんと、SceneMainは、TaskManagerのupdateを呼ぶだけでよくなりました。
 
2/3.TaskManagerクラス

TaskManager::update()
{
	// Taskの座標を全て更新
	Iterator it = taskList.getIterator();
	while(it.hasNext())
	{
		((Task)it.next()).update();
	}
	mediator.update(this); // ->4へ
}

まず、保持しているTaskを全て更新します。
そして、最後のところがキモです。
TaskManagerがTaskMediatorを呼ぶ、という「制御の逆転」を行っています。
(従来の手続き型プログラムとは反対の考え方ですね)
 
4.TaskMediatorクラス

TaskMediator::update(TaskManager taskManager)
{
	switch(taskManager.getId())
	{
	case PLAYER:
		// プレイヤータスクだったとします。
		Iterator it1 = taskManager.getTaskList().getIterator();
		while(it1.hasNext())
		{
			Task task1 = (Task)it1.next();
			// 当たり判定の対象は、「敵」
			TaskManager mgr2 = (TaskManager)taskManagerList.get(ENEMY)
			Iterator it2 = mgr2.getIterator();
			while(it2.hasNext())
			{
				Task task2 = (Task)it2.next();
				// 「プレイヤー vs 敵」の当たり判定を行う
				if(isCollide(task1, task2))
				{
					taskManager.remove(task1);
					mgr2.remove(task2);
				}
			}
		}
		break;
	case ENEMY:
		…
		break;
	…
	}
	…
}

まあ、ちょっと香ばしい書き方ですが…(´∀`;
とりあえず、これで、当たり判定処理をSceneMainから分離できましたぜぇ!!
 
 
…でも実は、このままだと、例えば、
「プレイヤーの更新後の座標」と「敵の更新前の座標」を比較してしまうので、
変な当たり判定を行ってしまう可能性があります。
 
そこで、厳密にチェックを行いたい場合は、TaskManagerを全て更新した後、
TaskMediatorに通知を行う必要があります。
 

おまけ

シーケンス図

 
 
…以上、Mediatorパターンでしたー。