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

投稿者: | 2012年6月2日

昔作っていたゲームのソースコードが結構大変なことになっちゃってました。

というのは、ゲームのメインとなるシーンのクラス(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に通知を行う必要があります。

おまけ

シーケンス図