描画処理とデータ処理の分割(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→…
という流れになっています。
 
ここで強調したいのは、

  1. 「データの入力・加工」をしてから「データを元に描画」をする、という流れ
  2. この2つの処理を分離する

ということです。
 
これを実現するのがObserverパターンなのです。
(…というかここまでで、もう解説はほとんど終了していますが…(´Д`;
 より汎用的なシステムを構築したい場合には、以下の文章をお読みください(笑)

より汎用的な使い方

複数の画面(枠)を持つゲーム

例えば、「バンゲリングベイ」(といって分かる人がいるのだろうか…w)のような、
全方向スクロールシューティングを作るとします。
 
そこで、以下のようなインターフェースでゲームを作るとします。

…まあ、、、絵心はない人なので、苦笑いでもしてやってください...(´Д`;
 
あ、真ん中にいるのは「ゴキブリ」でなくて「ヘリ」です。
念のため(´∀`;)
 
この場合に必要となるのが、

  1. プレイヤー視点の画面
  2. スコアやステージ情報
  3. 敵機のレーダー

です。
 
この場合、

  • 描画の枠が「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を理解するためのリンクを貼っておきます。
 

MVCについての詳しい説明がされています。
 

StrutsというMVCを採用したWebアプリケーション用のフレームワークの解説です。