はじめに
今回はゲームシステムの構築の核となる、Observerパターンについて解説します。
なぜObserverパターンが必要になるのか?
例えばゲームを作ろうとすると、たいてい以下のような処理を書くことになります。
// メイン実行クラス class CMain { private: // ゲームで使う変数・フラグ…(1) public: void main() { while(TRUE) { // データの入力・加工…(2) // データを元に描画…(3) } } };
- (1)は自キャラのオブジェクトであるとか、ゲームオーバーのフラグなどです。
- (2)はキー入力の受け取りとか、弾を発射したり、敵を破壊したり、などです。
- (3)は背景、キャラ、スコアなどの描画処理です。
ただ、ゲームの規模が大きくなるにつれて、それぞれの分量が増えていきます。そこで、(1)の部分を構造体にして取り出すとします。
// ゲームデータ構造体 struct CData { // ゲームで使う変数・フラグ…(1) }; // メイン実行クラス class CMain { private: CData m_data; public: void main() { while(TRUE) { // データの入力・加工…(2) // データを元に描画…(3) } } };
データの分離です。多少スッキリしました。
さらに、(2)や(3)の処理についてもソースの分割を行ってみます。
// ゲームデータ構造体 struct CData { // ゲームで使う変数・フラグ…(1) }; // 描画クラス class CObserver { public: void Update(CData *data) { // データを元に描画…(3) } }; // データ入力・加工クラス class CSubject { private: CObserver m_observer; public: void Execute(CData *data) { // データの入力・加工…(2) m_observer.Update(data); } }; // メイン実行クラス class CMain { private: CData m_data; CSubject m_subject public: void main() { while(TRUE) { m_subject.Execute(&m_data); } } };
いきなり、ややこしくなってしまったかもしれません…。簡単に流れを説明すると、
- CMain(ゲームループ)→CSubject(データ入力・加工)→CObserver(描画)→CMain→…
という流れになっています。
ここで強調したいのは、
- 「データの入力・加工」をしてから「データを元に描画」をする、という流れ
- この2つの処理を分離する
ということです。
これを実現するのがObserverパターンなのです。(…というかここまでで、もう解説はほとんど終了していますが…)
より汎用的なシステムを構築したい場合には、以下の文章をお読みください…。
より汎用的な使い方
複数の画面(枠)を持つゲーム
例えば、「バンゲリングベイ」(といって分かる人がいるのだろうか…)のような、全方向スクロールシューティングを作るとします。そこで、以下のようなインターフェースでゲームを作るとします。
真ん中にいるのは「ゴキブリ」でなくて「ヘリ」です。念のため…
この場合に必要となるのが、
- プレイヤー視点の画面
- スコアやステージ情報
- 敵機のレーダー
です。
つまり、描画の枠が「3つ」に分かれている、こととなります。
Observerパターン
クラス図は以下のようになります。
流れは先ほどと同じく、
- CMain→CControllerMain(データの入力・加工)→IViewのexecute(描画)→CMain…
となります。
CMain
まず、CMainからの実行パターンです。CMainから以下のよう呼び出します。
void CMain::main() { // Controllerのインスタンス生成 CController *ctrl = new CControllerMain(); // Viewの登録 ctrl->addView(new CViewMain()); ctrl->addView(new CViewScore()); ctrl->addView(new CViewRadar()); CData data; while(TRUE) { // Controller実行 ctrl->execute(&data); } }
ViewクラスをControllerクラスのaddView関数で登録していきます。そして、ゲームループでControllerのexecute関数を呼ぶだけです。
Viewクラス
先にViewクラスの説明をしておきます。IViewは画面描画用の基底クラスです。
IViewをinterface(純粋仮想クラス)とし、画面描画(更新)用のメンバ関数updateを定義しています。
これを派生クラスでインプリメントします。
- CViewMain(プレイヤー視点の画面描画)
- CViewScore(スコア画面の描画)
- CViewRadar(レーダーの描画)
先ほどの「3つの画面」を実装しているわけですね。
Controllerクラス
CControllerのexecute関数は以下のように実装します。
void CController::execute(CData *data) { inputData(data); // データの入力 editData(data); // データの加工 for(UINT i = 0; i < m_viewList.size(); i++) { // 描画更新 m_viewList.at(i)->update(data); } }
inputData()とeditData()は仮想関数とし、派生クラスに実装を任せます。ポイントは、登録されたViewのupdate関数を全部実行しているところですね。
この処理をViewクラスに更新を通知(Notify)しているといい、Observerパターンのキモとなる部分です。
これにより、
ControllerはViewがどんなものであるかどうか関係なしに、 (update関数を呼べば)画面の更新を要求することができる。
ということが可能となるわけです。つまり、冒頭で解説しているように、
- 「データの入力・加工処理」と「描画処理」の分離
が可能となったわけです。
補足1
さて、ここまで読んで、「…でも、な~んか設計が複雑になってるんじゃないのー??」と思ったかもしれません。
ですが。敵の攻撃により、レーダーが破壊されたとします。そうした場合には、CViewRadarクラスをCControllerMainから外すことにより、レーダーの描画を行わないようにしたいという修正が出るかもしれません。
またはコンフィグで、スコア表示の方法を変えられるようにしたい、という要望が発生するかもしれません。
というような、「見た目の変更=仕様変更」が起きた場合、Observerパターンであれば、Viewクラスの差し替えを行うだけで、見た目の変更が簡単に行える、という利点があります。
補足2
今回の説明では省略していますが、Viewクラスには、
- 描画を行うためのグラフィックスクラスの参照を持たせる
が必要になります。
また、Controllerクラスには、
- 登録したViewを削除する関数
などが必要になります。
おまけ
今回は、「データの入力・加工」と「データを元に描画」という処理の分離について解説しましたが、補足としてもう少し別の使い方を解説します。
例えば、ある敵機は「画面上に10発しか弾を発射できない」とします。
その場合には、敵機をFactoryMethodとし、敵弾のインスタンスを生成するようにします。敵弾のインスタンスには、「敵機の参照」を持たせるようにします。敵機は内部にカウンタを持っており、10発の弾を発射します。そして、敵弾が画面外に出るなどして消滅したとします。
その場合には、敵弾は保持している「敵機の参照」から、発射元の敵機に「消滅した」というメッセージを送り、消滅を通知します。そうすると敵機は安心して敵弾インスタンスを削除し、弾を発射できるわけです。
といった、「部下が上司に辞職願いを通知する(?)」という使い方もできます。
参考
実は今回のパターンは、MVCというアーキテクチャを利用したパターンだったりします。
そこでMVCを理解するためのリンクを貼っておきます。
-
Happy Squeaking!! -オブジェクト指向再入門- [第五回:デザインパターン事始め]
- MVCについての詳しい説明がされています。
-
http://www.atmarkit.co.jp/fjava/rensai3/struts01/struts01_1.html
- StrutsというMVCを採用したWebアプリケーション用のフレームワークの解説です。