猫でも分かるタスクシステム2

クマレーザー2

タスクシステムの特徴

まず、タスクシステムの特徴は、以下の3つあります。

  • データの定義
  • データの管理
  • 振る舞いの定義

これらを効率良く扱うことができるのが、タスクシステムです。
 
さて、ここでは、「タスクシステム進化論」を提唱します。
 
それは何かというと、
最初にあえて効率の悪い方法を紹介しておいて、
「タスクシステム」という良いシステムに進化させる、
という思考的な流れを解説したいと思います。
 
ざっと流れは以下のようになります。
 

データの定義

ばらばらの変数→構造体
 

データの管理

ばらばらの変数→配列→リスト
 

振る舞いの定義

switch〜case文→関数ポインタ
 
ということで今日は、「データの定義」について解説します。

(ゲーム)プログラム初心者にありがちな設計

(ゲーム)プログラム初心者は、ゲームを作るとき、次のように作ってしまいます。
 
例えば、シューティングで自機キャラを表示する場合には、以下の情報が必要になります。

  • 表示座標
  • 表示サイズ(当たり判定)
  • 生存フラグ

この情報を初心者は、以下のように定義してしまいます。

int  g_player_pos_x;  // 表示座標X
int  g_player_pos_y;  // 表示座標Y
int  g_player_size_x; // 表示サイズX(当たり判定)
int  g_player_size_y; // 表示サイズY(当たり判定)
bool g_player_alive;  // 生存フラグ

そして、敵キャラを作ると、

int  g_enemy_pos_x;  // 表示座標X
int  g_enemy_pos_y;  // 表示座標Y
int  g_enemy_size_x; // 表示サイズX(当たり判定)
int  g_enemy_size_y; // 表示サイズY(当たり判定)
bool g_enemy_alive;  // 生存フラグ

こうなります。
 
つまり、表示キャラが増えるたびに変数が増えていく、
という状況に陥ります。
 
もう、なんというか、変数があちこちに散らかってしまっている、
というような状態です。
 

タスクシステムでは…?

それに対してタスクシステムでは、構造体というものを利用します。

struct Task
{
	int  pos_x;  // 表示座標X
	int  pos_x;  // 表示座標Y
	int  size_x; // 表示サイズX(当たり判定)
	int  size_y; // 表示サイズY(当たり判定)
	bool alive;  // 生存フラグ
};

このように定義しておくと、

Task g_player; // 自機の情報のかたまり
Task g_enemy;  // 敵機の情報のかたまり

というように宣言すればよく、

g_player.pos_x = 100; // 表示座標Xに100をセット
g_player.pos_y = 200; // 表示座標Yに200をセット

というように、プレイヤーの情報が見たかったら、「g_player」を見ろ!
とになります。
 
というように「構造体」という枠に変数を閉じ込めることができるので、
データのまとまりが良くなります。

(似たようなデータ・関連性のあるデータは、同じところ、または近い場所にまとめよ!
というソフトウェア開発の格言があります)
 
さらに、

struct Point2D
{
	int x;
	int y;
};

というように定義しておくと、

struct Task
{
	Point2D pos;   // 表示座標
	Point2D size;  // 表示サイズ(当たり判定)
	bool    alive; // 生存フラグ
};

という効率良い定義ができます。
 
使うときは、

g_player.pos.x = 100; // 表示座標Xに100をセット
g_player.pos.y = 200; // 表示座標Yに200をセット

というように使うことができます。
 
 
次回は、データの管理について説明をします。
 

補講1

今回、説明したように、タスクシステムは言語レベルで、

  • 「構造体」

を使えないと実装することができません。
 
他にも、まだ説明していませんが、

  • 「双方向リスト」
  • 「関数ポインタ」

も必要となります。
 
「え〜〜!なんだ、、、私の使っている言語だとどれもないので意味ないじゃん」
ということになるかもしれません。
ですが、タスクシステムの概念は「効率良い」ゲーム設計を生み出すので、
理解しておいて損はないです。
 
とりあえず、今日は、
「キャラのデータを1つにまとめる、という方法があるんだ〜」
と理解してもらえれば、うれしいです。
 

補講2

今回紹介した方法では、例えば自機と敵機で異なる種類の情報を持つことができません。
例えば、自機には選択している武器、敵機には耐久度を持たせたいとします。
そこで、

struct Task
{
	Point2D pos;     // 表示座標
	Point2D size;    // 表示サイズ(当たり判定)
	bool    alive;   // 生存フラグ
	int     weapon;  // 武器の種類
	int     durable; // 耐久性
};

としておけば問題なさそうです。
 
しかし、このように拡張を続けていくと、
自機・敵機で互いに使わない情報がどんどん増えていき、
とてもごちゃごちゃしてしまいます。
 
そこで、以下のようにします。

struct Task
{
	Point2D pos;   // 表示座標
	Point2D size;  // 表示サイズ(当たり判定)
	bool    alive; // 生存フラグ
	char    work[256]; // ワークエリア
};

このようにworkという個別に拡張したい部分を用意しておきます。
 
こうすることにより、

struct WorkPlayer
{
	int     weapon;  // 武器の種類
};
struct WorkEnemy
{
	int     durable; // 耐久性
};

と個別のワークエリアを用意すると、

Task enemy;
WorkEnemy *pWork = (WorkEnemy*)enemy.work;
pWork->durable = 10; // 耐久度を設定

というように、ポインタのキャストにより、
個別の情報を設定することが可能となります。