描画処理とデータ処理の分割(Observerパターン)
はじめに
今回はゲームシステムの構築の核となる、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パターンなのです。
(…というかここまでで、もう解説はほとんど終了していますが…(´Д`;
より汎用的なシステムを構築したい場合には、以下の文章をお読みください(笑)
より汎用的な使い方
複数の画面(枠)を持つゲーム
例えば、「バンゲリングベイ」(といって分かる人がいるのだろうか…w)のような、
全方向スクロールシューティングを作るとします。
そこで、以下のようなインターフェースでゲームを作るとします。
…まあ、、、絵心はない人なので、苦笑いでもしてやってください...(´Д`;
あ、真ん中にいるのは「ゴキブリ」でなくて「ヘリ」です。
念のため(´∀`;)
この場合に必要となるのが、
- プレイヤー視点の画面
- スコアやステージ情報
- 敵機のレーダー
です。
この場合、
- 描画の枠が「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発の弾を発射します。
そして、敵弾が画面外に出るなどして消滅したとします。
その場合には、敵弾は保持している「敵機の参照」から、発射元の敵機に
「わいは消滅したんや」
というメッセージを送り、消滅を通知します。
そうすると敵機は安心して敵弾インスタンスを削除し、弾を発射できるわけです。
といった、「部下が上司に辞職願いを通知する(?)」という使い方もできます。