猫でも分かるタスクシステム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; // 耐久度を設定
というように、ポインタのキャストにより、
個別の情報を設定することが可能となります。