Arduino(5) ブロック崩しを作ってみる(2)

前回に続き、今回はブロック崩しのメインとなる、ブロックとボールの衝突処理を実装します。

まずブロックの画面上での配置です。

画面は30×40に分割します。これは前回書いたとおりです。
描画のとき以外は、全てこの30×40の座標系の上で処理を行います。

ブロックのサイズは3×2とします。
これを横方向に10個、縦方向に5個並べます。
ブロックの総数は50個となります。

各ブロックは、下図のように番号で表します。

arduino04-1.jpg

ブロックが存在している・いないを表すために、論理型の配列existを定義します。
例えばexist[k]==trueなら、k番のブロックは消されずに残っています。

#define BLOCKWIDTH 3
#define BLOCKHEIGHT 2
#define BLOCKTOP 3
#define BLOCKBOTTOM 12
#define BLOCKLINES ((BLOCKBOTTOM-BLOCKTOP+1)/BLOCKHEIGHT)
#define NBLOCKS (BLOCKLINES*SCRNWIDTH/BLOCKWIDTH)

boolean exist[NBLOCKS];

関数BlocksInit()は、この配列の全要素をtrueで初期化します。
関数BlocksLeft()は、全要素中のtrueの個数を返します。
BlocksLeftの結果が0であれば、ステージをクリアしたことになります。

void BlocksInit()
{
int8_t i;

for(i=0; i<NBLOCKS; i++) {
exist[i] = true;
}
}

uint8_t BlocksLeft()
{
int8_t i;
int8_t leftBlocks;

leftBlocks = 0;
for (i=0; i<NBLOCKS; i++){
if (exist[i])
leftBlocks++;
}
return leftBlocks;
}

次に、ブロックの描画と消去です。

使用しているLCDは240×320ピクセルですので、1×1の領域は8×8ピクセルとなります。
プログラム内での座標を描画用座標を変換するマクロTOSCRNを定義します。
また、ブロックの各行の色を配列colorsに格納しておきます。

#define TOSCRN(v) (v<<3)
uint16_t colors[BLOCKLINES]={RED,GREEN,BLUE,YELLOW,MAGENTA};

ブロック崩しでは最初に全てのブロックを表示し、ゲーム開始後はボールが当たったブロックを一つずつ消していきます。
関数BlocksDrawAllは全てのブロックを描画します。
関数BlocksEraseOne(n)は、n番のブロックを消去します。

void BlocksDrawAll(){
int8_t x,y;
int8_t w,h;
int8_t c;
int8_t i;

w=TOSCRN(BLOCKWIDTH);
h=TOSCRN(BLOCKHEIGHT);
c=0;
i=0;
for(y=BLOCKTOP; y<=BLOCKBOTTOM; y += BLOCKHEIGHT) {
for(x=0; x<SCRNWIDTH; x += BLOCKWIDTH) {
if (exist[i++]) {
tft.fillRect(TOSCRN(x), TOSCRN(y), w, h, colors[c]);
}
}
c++;
}
}

void BlocksEraseOne(int8_t block) {
int8_t x,y;
int8_t w,h;
int8_t rows;

w=TOSCRN(BLOCKWIDTH);
h=TOSCRN(BLOCKHEIGHT);
rows = SCRNWIDTH/BLOCKWIDTH;
x = (block % rows) * BLOCKWIDTH;
y = (block / rows) * BLOCKHEIGHT + BLOCKTOP;
tft.fillRect(TOSCRN(x), TOSCRN(y), w, h, BLACK);
}

そしていよいよ、ブロックとボールの衝突の処理です。

まず、ブロックとボールが衝突したかどうかの判定は、比較的簡単です。
ボールが移動しようとしている位置にブロックが存在していれば、そのブロックと衝突したと判定できます。

一方、衝突したあと、ボールを跳ね返らせる処理は、やや複雑です。
ぶつかる瞬間のボールとブロックの位置関係により、下図のような場合があることが判ります。
それぞれに応じた跳ね返り方向を設定することが必要です。

arduino04-2.jpg

今回はボールの跳ね返りを以下のように決定しています。

ボールの移動方向を(vx,vy)とすると、

(1)ボールを(vx,0)だけ移動するとブロックに衝突するならば、水平方向に跳ね返る。
(2)ボールを(0,vy)だけ移動するとブロックに衝突するならば、垂直方向に跳ね返る。
(3)ボールを(vx,vy)だけ移動するとブロックに衝突するならば、水平かつ垂直方向に跳ね返る。
(4)以上をボールが跳ね返らなくなるまで繰り返す。

(1)(2)は同時に起こる場合がある(上図の右上のパターン)ので、(4)の処理が必要になります。

関数BlocksFindは、与えられた座標のブロック番号を返します。
ブロックが存在していても存在していなくても動作は同じです。
関数BlocksHitは、ボールの座標と移動ベクトルから、ブロックとの衝突有無を判断します。
衝突していなければ、-1を返します。
衝突していた場合は、衝突したブロックの番号を返すと共に、ボールの跳ね返る方向へ移動ベクトルの値を書き換えます。

int8_t BlocksFind(uint8_t x, uint8_t y)
{
if (x > SCRNWIDTH) {
return -1;
}
if ((y < BLOCKTOP)||(y > BLOCKBOTTOM)) {
return -1;
}
uint8_t rows=SCRNWIDTH/BLOCKWIDTH;
return (y-BLOCKTOP)/BLOCKHEIGHT*rows + x/BLOCKWIDTH;
}

int8_t BlocksHit(uint8_t x, uint8_t y, int8_t *vx, int8_t *vy)
{
int8_t block;

block = BlocksFind(x + *vx, y);
if ((block >= 0) && exist[block]) {
*vx = -*vx;
return block;
}
block = BlocksFind(x, y + *vy);
if ((block >= 0) && exist[block]) {
*vy = -*vy;
return block;
}
block = BlocksFind(x + *vx, y + *vy);
if ((block >= 0) && exist[block]) {
*vx = -*vx;
*vy = -*vy;
return block;
}
return -1;
}

最後に、ゲームの状態遷移です。

ブロックを全部消したら、ゲームを再開する処理が必要になります。
このため、ゲームには

●ゲーム中
●ステージクリア後のゲーム再開待ち

の二つの状態を持たせます。

#define GAME_ONGOING 1
#define GAME_RESTART 0

uint8_t gameStatus;

また、ゲーム再開の処理では、プレイヤーを混乱させないようボールの座標はリセットしないことにします。
すると、ブロックを描画したい領域からボールが出ていくまでは、再開処理を行えなくなります。
ゲーム再開待ち状態では、ボールがブロックの描画領域よりも画面下方にあり、かつ移動方向がも下向きとなったときにブロックの描画を行います。

関数game_restartがこの処理を行います。
この関数は、ブロックを描画できたときにtrue、まだボールが出て行くのを待っている場合はfalseを返します。
関数game_ongoingはボール とブロックの衝突判定を行い、衝突したらそのブロックを消去します。

boolean game_restart()
{
if ((ball.y > BLOCKBOTTOM) && (ball.vy >0)) {
BlocksInit();
BlocksDrawAll();
return true;
} else {
return false;
}
}

void game_ongoing()
{
int8_t block;

do {
block=BlocksHit(ball.x,ball.y,&ball.vx,&ball.vy);
if (block >= 0) {
exist[block] = false;
BlocksEraseOne(block);
}
} while (block > 0);
}

そして、loop()の中で、状態遷移の管理を行います。

void loop()
{
switch (gameStatus) {
case GAME_RESTART:
if (game_restart()) {
gameStatus = GAME_ONGOING;
}
break;
case GAME_ONGOING:
game_ongoing();
if (BlocksLeft() == 0) {
gameStatus=GAME_RESTART;
}
break;
}
(以下は前回と同じなので略)

最後にスケッチファイルと、動作の様子のビデオを置いておきます。

コメント