この記事は,processingを用いてブロック崩しを作るためのチュートリアルです.初心者でも完成までたどり着けるように、詳しく書いていきたいと思います. 出てくるプログラムの知識は int型, float型、変数、if、for程度です.どれも事前知識があると嬉しいですが、全て解説は入れているので、問題ないのではないかなと思います.
完成図は以下のようになります.
ではでは.
はじめに
座標系
processingを使うと、自由に図形を描くことができます。図形を描画する際に必ず指定するのが座標なのですが、processingの場合は座標系における軸の取り方がいつもとは違います。 数学における座標系は、上に行くほどy座標が増えて、右に行くほどx座標が増えます。また、原点は左下です。 対して、processingにおける座標系は、下に行くほどy座標が増えて、右に行くほどx座標が増えます。また、原点は左上です。 最初は戸惑うかもしれませんが、徐々に慣れていきましょう。
setup()とdraw()
ゲームを作るために欠かせないのは「パラパラ漫画」の機構です。ページを素早くめくるように、ゲームは高速に画面が更新されています。例えばテトリスではブロックが落ちていきますが,落ちていくように見えるのは画面が素早く更新されているからです.この更新の速さを「フレームレート」と呼んだりしますが、フレームレートが低いことは、いわゆる「カクつく」「処理が重い」と感じる原因です。パラパラ漫画をめくる速度が遅ければ、当然絵もカクつきますね。
さて、processingにはこの「高速に画面を更新する」仕組みがあります。それがsetup()とdraw()です。これらはプログラミングの中では非常に重要な「関数」というものですが、今は割愛します。 setup()は、ゲームが始まった瞬間に一度だけ処理が行われます。 draw()は、ゲームを遊んでいる間、裏で何回も何回もひたすら処理が行われます。draw()が何回も実行されて、画面がその度に更新されることによって、パラパラ漫画はめくられます.ブロックは落ちるし、キャラは動きます。
具体的には、以下のようなテンプレを元に作成することになります。
void setup(){ //1回だけ実行される処理 } void draw(){ //何回も実行される処理 }
初期設定
今回ゲームを作るにあたって、初期設定をしましょう。 まずは実行画面の大きさです。ゲームをプレイする画面を小さくしたり、大きくしたりできます。 これはsize()によって指定することができます。サイズは一度設定すればそれ以降固定されるので、setup()に書きます。 サイズは何でも良いですが、本記事では500*500にすることにします。
void setup(){ size(500, 500); } void draw(){ //何回も実行される処理 }
実行ボタンを押すと、ある程度の大きさのウィンドウが表示されます。
ボールを作る
まずはボールを動かしましょう.ブロック崩しにおけるボールは,プレイヤーの意思に関わらず動き続けます.それに,壁やブロックに当たれば跳ね返ります.まずはブロックもバーも無いものと仮定して,壁に跳ね返るところまで実装します.
ボールの描画
ボールを画面に表示させます.まずは,画面の中心に円を描くことからやってみます.円はcircle(中心のx座標, 中心のy座標, 直径)
で描けます.円はellipse()
と覚えているかもしれませんが,最近のバージョンでcircle()も追加されました.
void setup(){ size(500,500); } void draw(){ background(255); //背景を白に circle(250, 250, 100); //円 }
実行すると,画面に円が表示されます.
ついでに,早めにbackground(R, G, B)
も紹介しておきます.これは背景の色を指定するものです.R,G,Bの3つの要素で色を指定します.例えば,(0,0,0)
なら黒,(255,255,255)
なら白,(255,0,0)
なら赤になります.そしてもう一つ便利な機能があって,RにもGにもBにも同じ値を指定するときに限っては,1つだけ指定することで同じことができます.つまり,background(255,255,255) = background(255)
です.
ボールの話に戻りますが,今の状態では,円を"動かす"ことはできません.なぜかというと,円の座標を250
という定数で指定しているからです.例えば,この値が250→251→252→253→...と変化していけば,円は動いているように見えるはずです.そのために,変数を使うことにします.
変数は,ある特定の値を保存できるものです.250
のような「定数」と比べて「変数」が便利なのは,保存した値に対して足し引きなどの演算ができることです.また,変数には型があります.これは保存できる値の種類を示すもので,本記事では以下の2つをよく使います.
- int型:整数を保存できる型.
...-2 -1 0 1 2 ...
- float型:小数を保存できる型.
3.14 2.0 1.999
など - boolean型:真偽値を保存できる型.
true
かfalse
だけ.
では,円の座標を変数で置き換えてみます.
// 変数の宣言 float ball_x, ball_y, ball_r; void setup(){ size(500,500); // 初期値の設定 ball_x = 250; ball_y = 250; ball_r = 100; } void draw(){ background(255); // 変数に置き換えた circle(ball_x, ball_y, ball_r); }
float型の変数を3つ作成して,circle()
の中身を置き換えました.変数は必ず「宣言」してから使います.宣言は型名 変数名
で行います.変数名は自由に決めることができるので,ball_x
以外の名前でも構いません.宣言したら,実際に値を代入できます.初期値の代入は最初の一回だけ行えば良いので,setup()
に書きます.
実行結果は何も変わっていませんが,変数で置き換えることには成功しました.
ボールを動かす
では,ボールを動かしていきます.動かすためには,速さの定義が必要です.つまり,ball_x
やball_y
がどれくらいの速さで動くかということです.速さも変数にしましょう.
float ball_x, ball_y, ball_r; // 速度の変数を宣言 float speed_x, speed_y; void setup(){ size(500,500); ball_x = 250; ball_y = 250; ball_r = 100; // 速さの初期値を設定 speed_x = 1; speed_y = -2; } void draw(){ background(255); circle(ball_x, ball_y, ball_r); // 座標を更新 ball_x += speed_x; ball_y += speed_y; }
速さを表す変数を,speed_x, speed_y
という変数名で宣言しました.それぞれx座標の速さと,y座標の速さを表しています.これは,座標の変数の値に対して,速さの変数の値を足します.例えば,ball_x
は初期値250
で,speed_x
は1
なので,ball_x += speed_x
はフレームを重ねるたびに,250→251→252...
と増えていきます.同じように,ball_y
は250→248→246...
と減っていきます.ボールの座標の更新は毎フレーム行うべきなのでdraw()
に書きます.
実行すると,円が動き出しました!
円を跳ね返す(進む向きを反転させる)
円が動いたのは良いですが,見ているとすぐに画面外に消えてしまいます.これではゲームにならないので,壁で跳ね返るようにします.
まず,「跳ね返る」ことについて考えます.実は跳ね返りは,x軸方向とy軸方向で分けて考えます.例えば右の壁に当たって跳ね返ることを考えると,x軸方向の向きは反対になりますが,y軸方向の向きは変化しないのです.ベクトルを分解するイメージがある人はわかりやすいと思います.
同様に考えると,左右の壁に当たったときはx軸方向の向きだけが反転し,上下の壁に当たったときはy軸方向の向きがだけ反転します.
また,「向きが反転する」というのは,正の方向に進んでいたものが負の方向になる,もしくはその逆です.つまり,速度に-1
をかけるだけで実装できます.
// 進む向きを反転させる例 speed_x *= -1; speed_y *= -1;
円を跳ね返す(当たり判定)
速度に-1
をかけるのは,あくまでもボールが壁に当たったときにやることです.そもそも「壁に当たった」ことを検知できないといけません.
そのために,当たり判定を実装していきます.基本的に,if文で実装します.
if文は,特定の条件を指定できるものです.「ある変数の値がこの値の時」とか「この変数の値がこの変数の値より大きい時」みたいなことが書けます.また,これらの条件を複数指定して,全て同時に満たさないといけないとか,いずれか1つ満たせば良いなども指定できます.
// ifの使用例 if(a == b){ 処理 } // aとbが等しい if(a > b){処理} // bよりaの方が大きい if(a == b && c == d) // aとbが等しく,cとdも等しい(同時に満たす必要がある) if(a == b || c == d) // aとbが等しい,もしくはcとdが等しい(どちらか一方満たせばok)
さて,ボールを跳ね返す時はどういう時かを考えると,ボールが壁にめり込む直前(直後?)であることが分かります.これをどうif文に落とし込むかが大事です.
まず,ボールの上下左右の点の座標を考えます.
ちょっと分かりにくいかもしれませんが,こうなります.ball_r
は直径なので,/2
します.この座標と,画面の端の座標との大小を見ることによって,壁にめり込むかどうかを判定できます.
float ball_x, ball_y, ball_r; // 速度の変数を宣言 float speed_x, speed_y; void setup(){ size(500,500); ball_x = 250; ball_y = 250; ball_r = 100; // 速度の初期値を設定 speed_x = 1; speed_y = -2; } void draw(){ background(255); circle(ball_x, ball_y, ball_r); ball_x += speed_x; ball_y += speed_y; // 当たり判定! if(ball_x + ball_r/2 > width) speed_x *= -1; // 右の壁に当たる時 if(ball_x - ball_r/2 < 0) speed_x *= -1; // 左の壁に当たる時 if(ball_y - ball_r/2 < 0) speed_y *= -1; // 上の壁に当たる時 }
width
というのは画面の幅を表す値で,システム変数と呼ばれます.システム変数は,processingが勝手に値を入れてくれます.具体的には,冒頭でsize(500, 500)
と指定した時に,勝手にwidth
には500
が保存されています.蛇足ですが,同様に画面の高さを表すheight
というシステム変数にも500
が保存されています.
バーを作る
ボールを跳ね返すためのバーを作ります.これも,当たり判定を考慮しないといけません.
一つ前に紹介した,ボールが壁で跳ね返るというのは,「円と線の当たり判定」だったので簡単でした.ですが,バーを使って跳ね返すというのは,「円と四角の当たり判定」です.これは少々めんどくさいのです.でも,やります.
ちなみに,バーは四角でなくてただの線でも十分実装できます.でも,後でブロックを実装した時に結局円と四角の当たり判定は必要になるので,敢えてバーを四角にして先取りすることにします.
マウスに追従させる
まずはバーを描いて,マウスに追従させます.まずは四角の描画からです.四角はrect(左上のx座標,左上のy座標,幅,高さ)
で書けます.
また,新しいシステム変数としてmouseX
と mouseY
を紹介します.これはマウス座標を表します.これを直接バーの座標とすることで追従するようになります.
// ここではdrawと宣言だけ書いている. float bar_w = 100, bar_h = 30; void draw(){ background(255); circle(ball_x, ball_y, ball_r); ball_x += speed_x; ball_y += speed_y; if(ball_x+ball_r/2 > width) speed_x *= -1; if(ball_x-ball_r/2 < 0) speed_x *= -1; if(ball_y - ball_r/2 <0) speed_y *= -1; // バーの描画 rect(mouseX, mouseY, bar_w, bar_h); }
円と四角の当たり判定
さて,ここが少し難しいですが,とても重要です.現在はバーに注目していますが,ブロックを作る時にも同じ考え方をします.まずは,バー座標がどのように表されるのかを以下に示します.ボールの座標についても,先ほど載せたものを再掲します.
さて,これらの座標を使って当たり判定を実装します.ちなみに,本記事は「最小限」なので,上から降ってきたボールに対して当たることだけ考えます.本当は上下左右から当たるようにするべきですが,これでも十分成立します.
この時,次の2点を満たす必要があります.
x座標で見たときに少しでもボールとバーが被っている.
y座標で見たときに,ボールの下の点がバーに被っている.
実装すると,こんな感じになります.正直,これは図形的なイメージを持ちながら,座標と真剣に向き合うしかありません.
float ball_x, ball_y, ball_r; float speed_x, speed_y; float bar_w = 100, bar_h = 30; void setup(){ size(500,500); ball_x = 250; ball_y = 250; ball_r = 50; speed_x = 1; speed_y = -2; } void draw(){ background(255); circle(ball_x, ball_y, ball_r); ball_x += speed_x; ball_y += speed_y; if(ball_x+ball_r/2 > width) speed_x *= -1; if(ball_x-ball_r/2 < 0) speed_x *= -1; if(ball_y - ball_r/2 <0) speed_y *= -1; rect(mouseX, mouseY, bar_w, bar_h); // バーとボールの当たり判定(上からの分) if((ball_x + ball_r/2 > mouseX && ball_x - ball_r/2 < mouseX+bar_w) //x座標に関するもの &&(mouseY < ball_y + ball_r/2 && ball_y + ball_r/2 < mouseY+bar_h)){ //y座標に関するもの speed_y *= -1; } }
複雑な条件式ですが,使っている座標は先ほど画像で示したものだけしか使っていません.
これを実行すると,バーでボールを跳ね返せるようになることが分かります.
ブロックを作る
さて,ブロックを作ります.ここでは5*5の25個のブロックを作ることにします.要件としては
ボールと当たり判定が発生する
ボールと当たれば消える
の2点を目指します.
ブロックの描画
まずはブロックを描画します.ブロックは25個作ると決めているので,直感的にはrect()
を25個書けば良いです.でもちょっとめんどくさいので,for文を使うことにします.
for文は繰り返しを行うものです.ざっくりした文法は
for(型 変数名 = 初期値; 変数名<限界値; 変化量の記述){処理}
です.例えば,for(int i=0; i<5; i++)
と書くと,i
という変数が0 → 1 → 2 → 3 → 4
で変化します.また,この時のi
をループ変数と呼ぶことが多いです.i
もただの変数名なので何でも良いですが,慣習的にi
j
k
あたりがよく使われます.
また,ブロックの幅と高さはすぐにわかります.5*5なので,幅はいわゆるwidth/5 = 500/5
で,100
です.高さは適度に画面の上部に埋めたいので,適当に30
にしましょう.これらは変数block_w
とblock_h
で宣言することにします.
次は,横一列の5個だけ描画することを考えます.この時,5つのブロックのx座標は0, 100, 200, 300, 400
です.y座標はいずれも0
です.
これを愚直に書けば
rect(0, 0, block_w, block_h); rect(100, 0, block_w, block_h); rect(200, 0, block_w, block_h); rect(300, 0, block_w, block_h); rect(400, 0, block_w, block_h);
となります.これを実行すると,横一列にブロックが並びます.でもちょっと冗長です.
ここで注目するべきなのが,x座標は100ずつ増えていることです.こういう等間隔に値が変化する時は,for文で書き直せることが多いです.
for(int i=0;i<5;i++){ rect(i*block_w, 0, block_w, block_h); }
これを実行しても,同じ結果が得られます.今の所,ソースコード全体としては以下のようになります.
float ball_x, ball_y, ball_r; float speed_x, speed_y; float bar_w = 100, bar_h = 30; // ブロックの幅と高さを宣言 float block_w = 100, block_h = 30; void setup(){ size(500,500); ball_x = 250; ball_y = 250; ball_r = 50; speed_x = 1; speed_y = -2; } void draw(){ background(255); circle(ball_x, ball_y, ball_r); ball_x += speed_x; ball_y += speed_y; if(ball_x+ball_r/2 > width) speed_x *= -1; if(ball_x-ball_r/2 < 0) speed_x *= -1; if(ball_y - ball_r/2 <0) speed_y *= -1; rect(mouseX, mouseY, bar_w, bar_h); if((ball_x + ball_r/2 > mouseX && ball_x - ball_r/2 < mouseX+bar_w) &&(mouseY < ball_y + ball_r/2 && ball_y + ball_r/2 < mouseY+bar_h)){ speed_y *= -1; } // まずは横一列 for(int i=0;i<5;i++){ rect(i*block_w, 0, block_w, block_h); } }
では,これを25個に増やしましょう.いま,ブロックをfor文によって横に5個並べました.次はfor文で「横に5個並べたものを」縦に5個並べましょう.
for(int j=0;j<5;j++){ //縦に5個並べる for(int i=0;i<5;i++){ //横に5個並べる // ↓iを使う! ↓jを使う! rect(i*block_w, j*block_h, block_w, block_h); } }
ブロックが描画できました!
ブロックでも跳ね返るようにする
ブロックでもボールが跳ね返るようにします.円と四角の当たり判定は既にやっているので,ほとんど同じことをすれば良いです.唯一変わるのは,ボールの来る向きが違うので,下から来たボールに対する当たり判定になるということです.
実装としては,バーの当たり判定をコピぺして,バーの座標だったところをブロックの座標になるように書き換えます.また,ball_y + ball_r/2
だったところを,ball_y - ball_r/2
に書き換えます.注目するボールの頂点が下の頂点から上の頂点に変わったということです.
float ball_x, ball_y, ball_r; float speed_x, speed_y; float bar_w = 100, bar_h = 30; // ブロックの幅と高さを宣言 float block_w = 100, block_h = 30; void setup(){ size(500,500); ball_x = 250; ball_y = 250; ball_r = 50; speed_x = 1; speed_y = -2; } void draw(){ background(255); circle(ball_x, ball_y, ball_r); ball_x += speed_x; ball_y += speed_y; if(ball_x+ball_r/2 > width) speed_x *= -1; if(ball_x-ball_r/2 < 0) speed_x *= -1; if(ball_y - ball_r/2 <0) speed_y *= -1; rect(mouseX, mouseY, bar_w, bar_h); if((ball_x + ball_r/2 > mouseX && ball_x - ball_r/2 < mouseX+bar_w) &&(mouseY < ball_y + ball_r/2 && ball_y + ball_r/2 < mouseY+bar_h)){ speed_y *= -1; } for(int j=0;j<5;j++){ for(int i=0;i<5;i++){ rect(i*block_w, j*block_h, block_w, block_h); // 当たり判定! if((ball_x + ball_r/2 > i*block_w && ball_x - ball_r/2 < (i+1)*block_w) &&(j*block_h < ball_y - ball_r/2 && ball_y - ball_r/2 < (j+1)*block_h)){ speed_y *= -1; } } } }
複雑ですが,一度理解すればやっていることはそこまで難しくありません.
さて,これでやっと当たり判定が完成〜!と思いたいんですが,一つ問題があります.以下の図のような場合です.
左のように,ボールのx座標がただ一つのブロックに被るなら問題ありません.普通に跳ね返ります.しかし,右の図のように,x座標が2つのブロックと被る場合,跳ね返りません.
この原因は,同じフレームで,AとBの2つのブロックに対して当たり判定が行われているからです.このとき,speed_y*=-1
は2回実行されるので,結局元に戻ってしまいます.これを解決するため,魔法の1行を加えましょう.
if((ball_x + ball_r/2 > i*block_w && ball_x - ball_r/2 < (i+1)*block_w) &&(j*block_h < ball_y - ball_r/2 && ball_y - ball_r/2 < (j+1)*block_h)){ speed_y *= -1; ball_y = ball_r/2 + (j+1)*block_h+1; //これ! }
これで,当たり判定の問題は無くなりました.あるブロックに当たった瞬間,そのブロックにギリギリ被らない位置にボールを移動させています.これにより,同一フレーム内では二度と当たり判定が起こらなくなります.
ブロックが消えるようにする
これが最後の山場です!!!
ブロックが消えなければ,クリアできないブロック崩しになってしまいます.そこで,「ブロックが生きているか」という情報を変数で持つことを考えます.この情報は,25個のブロックそれぞれが持つことになります.ということで,配列を使って生きているかどうかを保存しましょう.
配列は,複数の変数を一つの変数かのように管理できるものです.例えば,何かしらのx座標を持つ変数を10個宣言したいとき,
float x1, x2, x3, x4, x5, x6, x7, x8, x9, x10;
と書くのは面倒です.配列を使えば
float x[] = new float[10];
だけで宣言できます.あくまでも変数はx
一つだけしか宣言していませんが,x[0], x[1], ..., x[9]
というように,[数字]
を使って参照できます.この数字のことを添字,indexと呼んだりします.添字は必ず0
から始まります.
さらに,配列には次元があります.1次元から始まり,2次元,3次元といくらでも増やせます.
今回は,5*5の2次元配列で管理します.
また,変数の型はbooleanにします.booleanは冒頭でも紹介した通りtrue
かfalse
しか持たない型です.ここでは生きていればtrue
,そうでなければfalse
とします.
では実装です.まずは,boolean型の2次元配列を作ります.
boolean is_alive[][] = new boolean[5][5];
この配列はまだ値が入っていないので,setup()
で初期値を入れたいですが,その前に,配列とfor文の相性に触れておきます.いま,配列の添字は0 1 2 3 4...
と増えていきます.また,例えばfor(int i=0;i<10;i++)
と書けばi = 0 1 2 3 4...
と増えていきます.これらは全く同じ変化をしているので,for文の中でi
(正確にはループ変数です)を直接添字にしてしまうことができます.配列とfor文は相性抜群なのです.
ではこの相性の良さを2次元配列で実感してみます.
// setup()の中の話 for(int i=0; i<5; i++){ for(int j=0; j<5; j++){ //最初は全部生きているので初期値はtrue // i と j を直接添字に is_alive[i][j] = true; } }
後は,if文にbooleanを用いた条件式を追加します.生きているかどうかで処理が変わるのは,ブロックを描画することと,当たり判定が発生することなので,2箇所書き加えることになります.
また,当たり判定が発生すればブロックは消えるべきです.消えるということは,true
だったものをfalse
にするということです.false
になった途端,描画もされないし,当たり判定も発生しなくなります.
float ball_x, ball_y, ball_r; float speed_x, speed_y; float bar_w = 100, bar_h = 30; float block_w = 100, block_h = 30; // 「生きているか」を表すbooolean配列 boolean is_alive[][] = new boolean[5][5]; void setup(){ size(500,500); ball_x = 250; ball_y = 250; ball_r = 50; speed_x = 1; speed_y = -2; // 全ての初期値をtrueに for(int i=0;i<5;i++){ for(int j=0;j<5;j++){ is_alive[i][j] = true; } } } void draw(){ background(255); circle(ball_x, ball_y, ball_r); ball_x += speed_x; ball_y += speed_y; if(ball_x+ball_r/2 > width) speed_x *= -1; if(ball_x-ball_r/2 < 0) speed_x *= -1; if(ball_y - ball_r/2 <0) speed_y *= -1; rect(mouseX, mouseY, bar_w, bar_h); if((ball_x + ball_r/2 > mouseX && ball_x - ball_r/2 < mouseX+bar_w) &&(mouseY < ball_y + ball_r/2 && ball_y + ball_r/2 < mouseY+bar_h)){ speed_y *= -1; } for(int j=0;j<5;j++){ for(int i=0;i<5;i++){ // 生きていればブロックを描画する if(is_alive[i][j]){ rect(i*block_w, j*block_h, block_w, block_h); } if((ball_x + ball_r/2 > i*block_w && ball_x - ball_r/2 < (i+1)*block_w) &&(j*block_h < ball_y - ball_r/2 && ball_y - ball_r/2 < (j+1)*block_h) && is_alive[i][j]){ //条件式に「生きていれば」を追加 speed_y *= -1; ball_y = ball_r/2 + (j+1)*block_h+1; is_alive[i][j] = false; // ブロックは消えた } } } }
これで,ブロックが消えるようになりました!
以上で,最小限のブロック崩しは完成です.お疲れ様でした!
今後の展開
今回扱ったのは最小限のブロック崩しであり,少なくともこの記事を見た人は同じようなブロック崩しが完成します.
もっとちゃんとしたブロック崩しを作るなら,自分色をどう出していくかは考えないといけません.ここではそのアイデアを書き残します.
- スタート画面やクリア画面を作る.
これらはゲームのシーン遷移ということになるので,以下の記事が参考になるかもしれません.
ブロックに耐久値を設定する.
今は1回当たればブロックが消えますが,例えば2,3回当たれば消えるみたいなことです.スコアを設定する.
ブロック1つ消したら100点みたいな.時間制限を付ける.
上記のスコアと絡めて,「60秒間で何点取れるか」みたいなシステムにすると面白いかも.デザイン面を強化する.
ボールに残像を残したり,上記の耐久値と絡めてあと何回叩けば良いのかを色で表したり,背景にちょっとアニメーションみたいなのをいれたり.ブロックが増える?
最初見えている範囲以外にも実はブロックがあって,右からどんどん追加されるとか.
他の最小限ゲーム制作シリーズ
今回はブロック崩しを扱いましたが,ほかにも同じような粒度で解説しているものがあります.よろしければご覧ください.