この章では、シューティングゲームの雑魚キャラのように、複数の敵が出てくる場合の制御方法についての基本的な考え方を学ぶ。
キャラクタの情報は、構造体を利用すると管理しやすいことは第5章で学んだ。
/* キャラクタ・ステータス情報 */ typedef struct tarSTATUS { int type; // 敵のタイプ int x, y; // 表示座標 int move_x, move_y; // 移動量 int life; // ライフ int score; // スコア RECT rect; // 画像の矩形 RECT hitrect; // 当たり判定用の矩形 int width, height; // キャラクタの幅と高さ } STATUS;※ここでは説明のために1つの構造体を使っているが、実際は5章4で学んだとおり、構造体を2つに分割するべきである
この構造体を使った構造体変数を宣言して、キャラクタの様々な情報を制御する。敵キャラは複数表示することが多いため、配列で宣言するのが一般的である。
#define ENEMY_MAX 50 // 画面上に表示される敵の最大数
static STATUS Enemy[ENEMY_MAX];※敵の最大数を超える数のキャラを出さないようにプログラムしなければならない
ゲーム開始時に、様々なキャラクタ情報の初期化を行うが、このとき、
例えばゲームオーバーになってから再度ゲームを始める場合などのことを考えて、初期化処理を行わなければならない。そうしないと、再度ゲームを始めると、前の状態(データ)が一部残ってることによる様々なバグが発生してしまう。
その為に、必ず初期化しなければならないのは、ずばりライフ(life)である。ゲーム中はライフがDEADである敵キャラは一切の判定を行わないようにプログラムすることにより、位置情報などのデータが残っていても、ライフさえクリアされていれば問題ない。
/* 敵キャラのライフをクリアする */
#define DEAD 0
・
・
・
for (i=0 ; i<ENEMY_MAX ; i++)
Enemy[i].life =DEAD;※lifeに入れる値を「0」という数値ではなく、defineで指定した名前を使うことにより、プログラムを見やすくしている
この処理は通常、ゲーム開始時に行う。複数のステージがあるゲームの場合は
ステージが切り替わるたびに行う必要がある。
敵キャラを発生させるには、次の手順で行う。
- 発生させたいキャラクタの情報を敵キャラ変数に格納し、ライフをDEAD以外のものにする
- キャラクタ描画処理は、敵キャラ変数を先頭から調べ、ライフがDEADでないものを表示する
- ゲーム画面との当たり判定処理は、敵キャラ変数を先頭から調べ、ライフがDEADでないもののみ、判定&処理を行う
- 敵キャラの当たり判定処理は、敵キャラ変数を先頭から調べ、ライフがDEADでないもののみ、判定&処理を行う(死んだらライフをDEADにする)
※どの状態になったら発生させるのかは、ゲームによって異なる
通常敵キャラは、タイプ別に動きを制御する。よって、次の手順でプログラムするのが一般的である。
- 敵キャラ用配列を先頭から検索し、ライフがDEADでないもののタイプを調べる
- タイプ別に移動処理を行う
これをプログラムすると、次のようになる。
/* 敵キャラ用配列 */ static STATUS Enemy[ENEMY_MAX]; ・ ・ ・ /* 配列を調べ、生きているもののみ移動処理を行う */ for (i=0 ; i<ENEMY_MAX ; i++) {if (Enemy[i].life != DEAD)EnemyMove(&Enemy[i]);// 移動処理用の関数を実行 } ・ ・ ・ /* 移動処理用の関数 */ static void EnemyMove(STATUS *status) {switch (status->type){ case 0: // 敵0用の移動処理(上から下へ) status->y += status->move_y; break; case 1: // 敵1用の移動処理(左上から右下へ) status->x += status->move_x; status->y += status->move_y; break; case 2: // 敵2用の移動処理(右上から左下へ) status->x -= status->move_x; status->y += status->move_y; break; ・ ・ ・ } }※敵のタイプによって移動処理が変わるため、switch文でタイプを調べ、case句内にタイプ別の移動処理を組み込んでいる
一見シンプルに見えるプログラムだが、これには大きな落とし穴がある。それは次の通り。
- 敵キャラの種類が増えると、EnemyMove関数が巨大化する
- 複雑な動きをさせる敵キャラが複数いる場合、プログラムが見難くなる
これを解消するには、次のように変更する。
/* 移動処理用の関数 */ static void EnemyMove(STATUS *status) { switch (status->type) { case 0: // 敵0用の移動処理(上から下へ)EnemyMove_0(status);break; case 1: // 敵1用の移動処理(左上から右下へ)EnemyMove_1(status);break; case 2: // 敵2用の移動処理(右上から左下へ)EnemyMove_2(status);break; ・ ・ ・ } } /* 敵0用移動処理 */ void EnemyMove_0(STATUS *status) { status->y += status->move_y; } /* 敵1用移動処理 */ void EnemyMove_0(STATUS *status) { status->x += status->move_x; status->y += status->move_y; } /* 敵2用移動処理 */ void EnemyMove_0(STATUS *status) { status->x -= status->move_x; status->y += status->move_y; } ・ ・ ・※敵キャラ別に、移動処理用関数を用意した
この方法ではソース自身は大きくなるが、1つの移動処理を1つの関数で作っているため、メンテナンスしやすくなる。また、グループでプログラムを作る場合、分業がしやすくなる。
4で説明した移動処理には、画面をはみ出したときの処理が記述されていない。これは、
画面をはみ出した場合の処理は、タイプとは関係なく共通のロジックで行うからである。移動処理の関数ごとにはみ出した場合の処理を記述するよりも、すべての移動処理が終わってからまとめてはみ出した場合の処理を行ったほうがプログラム的にはすっきりする。
これも5と同じ理由で、移動処理の関数ごとに自キャラとの当たり判定処理を記述するよりも、すべての移動処理が終わってからまとめて当たり判定処理を行ったほうがプログラム的にはすっきりする。