インベーダーゲームを作ってみる(3)インベーダーの移動処理

かつてマイコン少年だったころ、インベーダーゲームで強く印象に残った処理が2つありました。

(1)グラフィカルな敵が多数動いている。処理が大変そう
(2)陣地が敵弾にだんだん崩される様子がリアル。あらかじめ用意した画像を使っているのではなさそう

今回は、このうち(1)のインベーダーの移動処理を扱います。

インベーダー群は、左右に行進しつつ、インベーダーが画面の端まで行くと、一段下へ移動します。
画面最下端へインベーダーが到達したらゲームオーバーです。

従って、移動処理では

(1)画面左右端に到達したインベーダーが居るかどうかの判定
(2)画面最下端に到達したインベーダーが居るかどうかの判定

を行う必要があります。

また、画面の端まで到達できるのは生きているインベーダーだけなので、これらの判定処理は、全ての(生きている)インベーダーについて行う必要があります。
また、「どんなゲームだったのか」で書いたとおり、インベーダーは一度に一匹しか動きません。

これらのことを踏まえ、今回はインベーダーの移動処理を以下のように実装しました。

(1)インベーダーの座標は、全体を一括して扱う(一匹ごとの座標は持たない)
(2)インベーダーの生死はフラグ(の配列)で判定する(ブロック崩しと同じ)
(3)インベーダーを一匹移動させる都度、画面左右下端への到達をチェックする
(4)インベーダーがすべて移動し終わったときに、行進方向の変更や下降の処理を行うかどうか判定する

なお、(1)のインベーダーの座標は全体の「現在位置」「次に動くべき位置」で表し、個別のインベーダーの位置は「次に動かすインベーダーの番号」で表現します。
図にすると下のようになります。この図ではインベーダーは右から左へ移動しています。

iv07.png

インベーダー全体としては(cur_left, cur_top)から(nxt_left, nxt_top)へ移動するのですが、実際には0番~(nxt_update -1)番までが移動済みで、nxt_update版~49番までが現在位置に留まっています。

インベーダーの番号は左下が0番で、右へ進むごとに+1、最後の49番は右上のインベーダーです。
オリジナルのインベーダーゲームの映像をよく見ると、最初に動き出すインベーダーは左下で、そこから右隣が順次移動し、次は下から2列目の左端から動き始めていますので、そのような順で番号を振りました。

インベーダーが端に到達したかどうかは、インベーダーを1匹移動させるごとに、そのときの描画座標でチェックします。
(オリジナルのインベーダーでは、画面の端にドットがあるかどうかで判定していたらしいです。)

コードはGitHubで公開していますが、上の説明に対応する部分を載せておきます。

まずインベーダーを表すデータ構造です。

struct aliens_t {
boolean exist[ALN_NUM];
uint8_t alive;    // number of aliens alive
int16_t cur_left;
int16_t cur_top;
int16_t nxt_left;
int16_t nxt_top;
int16_t v;      // left move or right move
uint8_t nxt_update; // which invader will be moved at the next tick
uint8_t pose;   // index num for iv?_img[]
int16_t bottom;   // be updated in aliens_draw
boolean move_down;  // down-move flag (updated in aliens_draw)
boolean touch_down; // an alien touched bottom line flag
int8_t status;
uint8_t hit_idx;  // which invader is been hit
} g_aliens;

そして、移動ルーチンが以下です。
動かすべきインベーダーが見つかれば(最初のif文)、その番号をnxt_updateに代入して終わりです。
49番まで行っても見つからなければ、生存しているすべてのインベーダーを移動し終えたので、最下端に到達したか(touch_down)の確認、X座標の更新、左右端へ到達した場合はY座標の更新を行います。

void aliens_move(struct aliens_t *a) {
uint8_t u = a->nxt_update;
do {
u++;
} while ((u < ALN_NUM) && (!a->exist[u]));

if (u < ALN_NUM) {
a->nxt_update = u;
}
else {  // all aliens have moved
if (a->bottom >= SCRN_BOTTOM)
a->touch_down = true;
for (u = 0; (u < ALN_NUM) && (!a->exist[u]) ; u++);
a->nxt_update = u;
a->pose = (a->pose + 1) % 2;
a->cur_left = a->nxt_left;
a->nxt_left += a->v;
a->cur_top = a->nxt_top;
if (a->move_down) { // this flag had been set in aliens_draw()
a->move_down = false;
a->v = -a->v;
a->nxt_top = a->cur_top + ALN_VMOVE;
a->nxt_left = a->cur_left + a->v;
}
}
}

一匹のインベーダーを描画する処理は以下の通りです。ほとんど3種類を描き分ける処理だけですが、最後のところで左右端への到達判定(move_down)と、インベーダー群の中で最大(最下端)のY座標の保存(bottom)を行っています。

void aliens_draw(struct aliens_t *a) {
if (!a->exist[a->nxt_update])
return;

uint8_t row = A_ROW(a->nxt_update);
uint8_t col = A_COL(a->nxt_update);
int16_t x = a->nxt_left + col * (ALN_W + ALN_HSPACING);
int16_t y = a->nxt_top + row * (ALN_H + ALN_VSPACING);
uint8_t *img;

switch (row) {
case 0: img = (uint8_t *)aln1_img[a->pose]; break;
case 1:
case 2: img = (uint8_t *)aln2_img[a->pose]; break;
case 3:
case 4: img = (uint8_t *)aln3_img[a->pose]; break;
}
DRAW_BITMAP(x, y, img, ALN_W, ALN_H, WHITE);

if (((x <= SCRN_LEFT) && (a->v < 0)) ||
((x >= SCRN_RIGHT - ALN_W) && (a->v > 0)))
a->move_down = true;

a->bottom = max(a->bottom, y + ALN_H);
}

コメント