Processingでブロック崩しを作る

本記事は,以下のURLにて新しく書き直されました.新しい記事の方が圧倒的に詳しい内容になっていますので,ぜひご覧ください.

gotutiyan.hatenablog.com


ブロック崩しを作ります。完成図は以下のツイートの通りです。

今回作るのは、「ボールがブロックに当たったらブロックが消える」部分だけです。操作できるバーなどは扱いません。
初心者は、ボールと四角の当たり判定で頭が爆発しがちです。そこをクリアできるようになるのが目標です。
size(500,500)で、50個のブロックを並べます。

・ボールを動かす

まずは、準備体操として、ボールを動かす部分を解説します。
ボールを動かすには、
・ボールの中心のx,y座標と半径
・x方向、y方向のスピード
が必要になります。これらをballx,bally,r,speedx,speedyとします。

void draw()の中で、ballx+=speedx, bally+=speedyという形で足すことで、ボールは動きます。

float ballx=width/2,bally=400,speedx=5,speedy=5,r=50;
void setup(){省略}
void draw(){
  ellipse(ballx,bally,r,r);
  ballx+=speedx;
  bally+=speedy;
}

・跳ね返り

次に跳ね返るという処理ですが、頭を爆発させずに目的の座標をコードでしっかり表現できることが大切ですね。
まずは以下のWord感バリバリの図を見てみましょう。

f:id:gotutiyan:20180509225427p:plain
以下、この4点を0時、3時、6時、9時の点と呼びます。

まず把握しなければいけないのは、座標を加算するにしろ、壁で跳ね返すにしろ、あくまでも「x軸方向とy軸方向に分けて考えること」です。斜めに移動している物体でも、それはx,y座標を同時に動かしているから斜めに移動しているだけです。
跳ね返りも、x軸での跳ね返り、y軸での跳ね返りと別々に書くことになります。

跳ね返りのコードは以下の通りです。

//x軸
if(ballx-r/2<0 || ballx+r/2>width)speedx*=-1;

//y軸
if(bally-r/2<0 || bally+r/2>width)speedy*=-1;

今回は偶然、xとyが変わるだけで、他は全く変わりません。
x軸については、3時の点がwidthより大きくなるか、9時の点が0より小さくなれば跳ね返ります。
y軸については、6時の点がheightより大きくなるか、0時の点が0より小さくなれば跳ね返ります。

また、跳ね返すということは、「ある方向に進んでいたものが逆方向に進む」ことです。もう少し言えば、「正の値を足していたものが負の値を足されるようになる」ことです。
つまり、跳ね返りの処理はspeedx,speedyの正負を入れ替えるだけで済みます。これは重要な考え方です。
(もちろん、負の値を足していたものが正の値を足されるようにする、と言い換えることもできます。)

・いざ、ブロックを崩す

さて、ブロックを崩します。崩すためには、最初にブロックを配置する必要があります。
今回は配列を使った実装を考えます。

まず必要な配列は以下の通りです。
・各ブロックの、左上のx,y座標を保存するint配列 「x,y」
・各ブロックが生きているかを管理する二次元boolean配列「ok」
今回は横に10個、縦に5個並べるので、x[ 10 ]、y[ 5 ]、ok[10][5] の要素数で用意します。

int x[]=new int[10],y[]=new int[5];
boolean ok[][]=new boolean[10][5]
・配列の初期化

配列の初期位置を決めます。
setup()でfor文を回して代入していきます。
サイズは500の正方形を想定していて、横に10個並べ、縦に5個並べます。
これにより1つあたりのブロックの大きさは横50*縦25にしたいと思います。

この想定のもとで、rectで書くときの左上の座標を決めていくと、
x={0,50,100,150,,,,450}
y={0,25,50,75,100}
のような等差数列になるので、for文のループ変数をうまく使って代入します。

また、最初は全てのブロックが生きているので、okは全てtrueです。

void setup(){
  for(int i=0;i<10;i++) x[i]=i*50;
  for(int i=0;i<5;i++)y[i]=i*25;
  for(int i=0;i<10;i++)for(int j=0;j<5;j++)  ok[i][j]=true;
}
・実際に四角を描く

配列を使って四角を書いていきます。配列を用いるということから、やはりfor文を回す中で50個描くという発想です。

for(int i=0;i<10;i++){
   for(int j=0;j<5;j++){
    if(ok[i][j]) rect(x[i],y[j],50,25); 
   }
  }

x[ ]には添字 i を、y[ ]には添字 j を使うことに注意です。
これにより、
(x[0],y[0]) (x[1],y[0]) (x[2],y[0])...(x[9],y[0])
(x[0],y[1]) (x[1],y[1]) (x[2],y[1])...(x[9],y[1])
...........
(x[0],y[4]) (x[1],y[4]) (x[2],y[4])...(x[9],y[4])

という形で、かつ、okがtrueであればブロックが描かれます。
(x[i],y[j])のブロックにはok[i][j]が対応していることが重要です。

・ブロックとの当たり判定

次に当たり判定です。これも、「for文を回す中で、50個のブロック全てについて当たっているかどうかを順番に見ていく」という発想です。

for(int i=0;i<10;i++){
   for(int j=0;j<5;j++){
    if(y[j]<bally-r/2 && bally-r/2<y[j]+25 && ballx+r/2>x[I] && ballx-r/2<x[i]+50 && ok[i][j]){
      speedy*=-1;
      ok[i][j]=false;
    }
   }
  }

当たり判定のif文が非常に複雑ですが、これは結局「0時の点がブロックの中に入っていて、3時の点と9時の点のどちらかのx座標が、ブロックのx座標の範囲と被っている」ことを示しています。

まず前半のy[j] < bally-r/2 && bally-r/2 < y[j]+25 では、y座標について考えていて、以下の図の範囲に対応しています。条件式の文法上2つに分かれていますが、内容は
y[i] < bally-r/2 < y[i]+25
の不等式を書いているだけです。

最後に、ok[i][j]がtrue の時に当たり判定を発動させます。okは四角を描くかどうかだけでなく、当たり判定の有無にも関わってきます。
f:id:gotutiyan:20180510000335p:plain

後半のballx+r/2 > x[i]&&ballx-r/2 < x[i]+50 では、
ballx+r/2 > x[i] が3時の点がブロックの左側より右側にある
ballx-r/2 < x[i]+50 が9時の点がブロックの右側より左側にある
ことを示します。何言ってんだって感じですね、ほんとに。
結局、ボールの中心が以下の図の範囲にあるときです。だいぶ広めに取っています。
f:id:gotutiyan:20180510000837p:plain

これらの条件式を全て満たしたとき、ボールは跳ね返ります。つまり、speedyにマイナスを掛けます。また、ok[i][j]をfalseにすることで、当たったブロックはそれ以降描かれなくなります。

x軸周りの当たり判定を広く取ることで、ブロックに対してボールが真横から当たることを未然に防いでいるので、speedyをいじるだけで結構自然な挙動になります。

・最後にひと足し

さあ、これでだいぶ形にはなりました。もっと当たり判定を厳密にするために、当たり判定のfor文の中に魔法の1行を加えます。

bally=y[j]+25+1+r/2

これですね。これなんですね。
これは何をしているかというと、当たった瞬間に0時の点を、ブロックの下側より1ピクセル下にずらします。つまり、ボールとブロックが被らないようにします。
これにより、座標の更新による当たり判定の重複などを起こりにくくします。

speedyの値にもよりますが、時と場合と運により、当たり判定が重複して発生し、speedy*=-1が2回素早く行われて結局意味が無くなったり、消したブロックのそのさらに上のブロックまで突っ込んだりと言った不具合がよくあります。この魔法の1行は(多分)それらを全て解決します。

最終的なコードは以下のようになります。

int x[]=new int[10],y[]=new int[5];
boolean ok[][]=new boolean[10][5];
float ballx=width/2,bally=400,speedx=5,speedy=5,r=50;

void setup(){
  size(500,500);
  //初期化
  for(int i=0;i<10;i++) x[i]=i*50;
  for(int i=0;i<5;i++)y[i]=i*25;
  for(int i=0;i<10;i++)for(int j=0;j<5;j++)  ok[i][j]=true;
  
}

void draw(){
  background(255);
  //四角を描くところ
  for(int i=0;i<10;i++){
   for(int j=0;j<5;j++){
    if(ok[i][j])rect(x[i],y[j],50,25); 
   }
  }
  
  //ボールを描くところ
  ellipse(ballx,bally,r,r);
  ballx+=speedx;
  bally+=speedy;
  if(ballx-r/2<0 || ballx+r/2>width)speedx*=-1;
  if(bally-r/2<0 || bally+r/2>width)speedy*=-1;
  
  //当たり判定の部分。当たれば跳ね返して、okをfalseに。
  for(int i=0;i<10;i++){
   for(int j=0;j<5;j++){
    if(y[j]<bally-r/2 && bally-r/2<y[j]+25 && ballx+r/2>x[i]&&ballx-r/2<x[i]+50 && ok[i][j]){
      speedy*=-1;
      bally=y[j]+25+1+r/2;
      ok[i][j]=false;
    }
   }
  }
}

・・・たくさん書きましたね。本当はもっと簡素なものにするつもりだったんですが。書き出すと止まらないですね・・。ではでは。