昔作っていたゲームのソースコードが結構大変なことになっちゃってました。
というのは、ゲームのメインとなるシーンのクラス(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に当たり判定を任せてしまうことができます。
フローは以下の順番になります。
- SceneMainがTaskManagerのupdateを呼ぶ
- TaskManagerはTaskのupdateを呼び、Taskの座標を更新する
- その後、TaskManagerは自分を更新されたことをTaskMediatorに通知する。(TaskMediatorのupdateにthisポインタを渡して呼ぶ)
- 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に通知を行う必要があります。
おまけ
シーケンス図