物理ライブラリを使ってブロック崩しを作る
下記、書籍の15章15.4を執筆した際は、ライブラリを使わずに、ブロック崩しをつくりましたが、ここでは、fisicaを使って、作っていきます。書籍では、古典的なブロック崩しの挙動をしますが、ここでは、fisicaの物理計算を使うので、ブロックの角に当たったときなど、反射角が大きく変化するという違いがあります。
Processingにfisicaをインストールします。
まずは、最低限動くサンプルを掲載します。
重力があるのが特徴。
二重のfor文でブロックを生成。
ボールも5つ生成。
addBlock関数では、ブロックの属性を指定したのち、追加。
addBall関数では、初速ベクトルなど属性を指定したのち、追加。
残ブロック数の表示あり。(エッジ4辺もオブジェクト数に含める)
contactStarted関数で衝突を検知し、ボールとブロックの場合ブロックを削除。順序が逆になることはなさそうである?。
import fisica.*;
FWorld w;
void setup() {
size(1280, 720);//16:9(HD)
Fisica.init(this);
w = new FWorld();
w.setEdges();
// blockを作成
for (int y=128; y<10*32; y+=32) {
for (int x=64; x<16*64; x+=64) {
addBlock(x, y);
}
}
// ballを5つ発生
addBall(640, 500);
addBall(640, 550);
addBall(640, 600);
addBall(640, 650);
addBall(640, 700);
//重力(maxあり)
w.setGravity(0,100);
}
void draw() {
background(255);
w.step();
w.draw();
fill(0);
textSize(100);
text(w.getBodyCount()-1-4-5, 100, 100);//残ブロック数
}
void contactStarted(FContact c) {
String b1 = c.getBody1().getName();
String b2 = c.getBody2().getName();
if (b2=="ball" && b1=="block")
w.remove(c.getBody1());//衝突ブロックを消す
}
void addBlock(int x, int y) {
FBox b = new FBox(64, 32);//サイズ
b.setPosition(x, y); //位置
b.setFill(0, 200, 200); //色
b.setStatic(true); //固定
b.setFriction(0); //摩擦抵抗
b.setRestitution(1); //反発係数
b.setName("block"); //名前
w.add(b);
}
void addBall(int x, int y) {
FCircle c = new FCircle(20);//サイズ
c.setPosition(x, y); //位置
c.setFill(255, 255, 255); //色
c.setVelocity(500, -500); //速度
c.setDamping(0); //減衰(空気抵抗)
c.setFriction(0); //摩擦抵抗
c.setRestitution(1); //反発係数
c.setName("ball"); //名前
w.add(c);
}
//速度はベクトルの長さで4000がMax。ななめ45度なら2000*√2=2828がMax
ブロック貫通問題について
ここで、確認しておきたいことがあります。物理演算で必ず問題となる話題です。ボールの速度が遅いときは問題が置きませんが、速くなるほど、すり抜け、貫通問題が生じます。step関数では、ボールの次の座標を計算しますが、速度が速く、移動距離がながいほど、衝突判定が難しくなります。また、衝突した際に、どっちに反射すればよいのかという計算が難しくなります。このあたりが、fisicaでどうなるのか確認します。
次のプログラムでは、クリックする度に、1フレーム(step)進むようになっていますので、これで、じっくり挙動を観察します。
最大速度は4000
まずボールの速度についてです。addBall関数で、ボールの初速を決めていますが、大きな数字を入力しても、限界があるようです。試しに、contactStarted関数で速度を表示してみても、2800前後で、速度が頭打ちになります。色々やってみると、速度ベクトルの長さが最大4000に制限されているようです(fisicaマニュアルには記載がありませんでした。box2Dにはあるかもしれません)。ですので、ななめ45度なら4000x0.5x√2で約2828が限界です。おそらく、衝突判定で貫通問題が生じないように、最大速度を決めて、その範囲内なら、貫通しないように設計されているのでしょう。
最小のサイズは2
次にブロックの厚みです。addBlock関数でサイズを指定する際に、
FBox(64, 1)
と、してみます。するとエラーで止まります。
FBox(64, 2)
としてみると、動いてくれることから、最低2の厚みがないといけないようです。一般に薄くなるほど貫通問題が起きやすいのですが、問題は起きないようです。この他、ブロック側も速度があり、速く動く場合や、速く回転する場合など、貫通問題が起きやすいですが、ブロック崩しには、ない状況なので、検証はやめます。
重力は自由だが速度制限4000の範囲内で運用
最後に、重力の最大値について確認しました。
mouseClicked関数で重力の設定値を表示&設定します。色々観察しました。重力の初期値は(0,10)です。重力は設定した値の1/20になるようですので
setGravity(0,10*20)
がデフォルトです。重力の設定値は、いくらでも大きく設定できるものの、計算されるボールは、速度ベクトルの長さが最大4000に制限されるのは変わりなく、ボールが地面にへばりつくなど、挙動がおかしくなります。
import fisica.*;
FWorld w;
void setup() {
size(800, 800);
Fisica.init(this);
w = new FWorld();
w.setEdges();
addBlock(400, 400);
addBall(200, 200);
}
void draw() {
}
void mouseClicked() {
w.step();
w.draw();
//println(w.getGravity());
//w.setGravity(0,400*20);//初期値は(0,10)。1/20
}
void contactStarted(FContact c) {
print(c.getBody2().getVelocityX()+" ");
println(c.getBody2().getVelocityY());
}
void addBlock(int x, int y) {
FBox b = new FBox(64, 32);//サイズ(Minは2)
b.setPosition(x, y); //位置
b.setFill(0, 200, 200); //色
b.setStatic(true); //固定
b.setFriction(0); //摩擦抵抗
b.setRestitution(1); //反発係数
b.setName("block"); //名前
w.add(b);
}
void addBall(int x, int y) {
FCircle c = new FCircle(20);//サイズ
c.setPosition(x, y); //位置
c.setFill(255, 255, 255); //色
c.setVelocity(2828, 2828);//速度
c.setDamping(0); //減衰(空気抵抗)
c.setFriction(0); //摩擦抵抗
c.setRestitution(1); //反発係数
c.setName("ball"); //名前
w.add(c);
}
//ベクトルの長さで4000がMax。ななめ45度なら2828
エクセルでステージを作る
まず、CSVフォーマットの説明をします。代表的なCSVデータは数値や文字をカンマ区切りで、保存したシンプルなテキストデータです。エクセルでステージをデザインして、別途CSV形式で保存し、processingでcsvを読み込んで使います。
ファイルの中身はテキストデータなので、
windowsであれば「メモ帳」
macであれば「テキストエディット」の「フォーマット>標準テキスト」モードでも編集可能です。
※リッチテキストモード、htmlモードなどは、ダメです。
最終的に、以下のようなCSVファイルを生成します。
9,9,9,9,9,9,9,9,9,9,9,9,9,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
9,0,1,1,1,1,1,1,1,1,1,1,0,9
9,0,2,2,2,2,2,2,2,2,2,2,0,9
9,0,3,3,3,3,3,3,3,3,3,3,0,9
9,0,4,4,4,4,4,4,4,4,4,4,0,9
9,0,5,5,5,5,5,5,5,5,5,5,0,9
9,0,6,6,6,6,6,6,6,6,6,6,0,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
9,0,0,0,0,0,0,0,0,0,0,0,0,9
エクセルの場合は、セルの幅と高さを調整可能です。
幅64、高さ32にしました。
※初期設定でも、不都合はありません。
また、数値で適当に色つけをすると、編集ミスを減らせます。
※塗らなくても、不都合はありません。
ステージデータが完成したら「名前をつけて保存」から、CSVフォーマットで保存します。
UTF-8ありとなしがありますが、とりあえず、macならUTF-8で問題ないと思います。
CSVファイルをprocessingで読み込む
ファイル選択ウィンドウをselectInput命令で開きます。選択したファイルのパス(ファイルのある場所)がfileSelect関数のselectionの中に渡されて、getAbsolutePath命令で取り出すことができます。そのパスを使って、csvファイルを開き、(今回は整数なので)getInt命令で、行yと、列xを指定して、数字を読み込み、ここでは、print文で表示しています。
csvファイルを開いて、tableクラスに読み込み、内容をprint文で表示
Table csv;
void setup() {
selectInput("message", "fileSelect");
}
void fileSelect(File selection) {
if (selection == null) {
} else {
String path = selection.getAbsolutePath();
//println(path);
csv = loadTable(path);
for (int y=0; y<20; y++) {
for (int x=0; x<14; x++) {
print(csv.getInt(y, x));
}
println();
}
}
}
続いて、windowにtext命令で表示
※一度、stageという配列にいれてから表示
Table csv;
int stage[][] = new int[14][20];
void setup() {
size(1280,800);
selectInput("message", "fileSelect");
}
void draw(){
background(0);
for (int y=0; y<20; y++) {
for (int x=0; x<14; x++) {
text(str(stage[x][y]),x*64,y*20+20);
}
}
}
void fileSelect(File selection) {
if (selection == null) {
} else {
String path = selection.getAbsolutePath();
print(path);
csv = loadTable(path);
for (int y=0; y<20; y++) {
for (int x=0; x<14; x++) {
int t = csv.getInt(y, x);
//print(t);
stage[x][y] = t;
}
//println();
}
}
}
次にブロックで表示し、ボール衝突で消えるようにcontactStarted関数を追加します。しかし、壁まで壊れてしまうため、壁は別の作り方をするひつようがありそうです
import fisica.*;
FWorld w;
Table csv;
int stage[][] = new int[14][26];
void setup() {
size(1280, 800);
selectInput("message", "fileSelect");
Fisica.init(this);
w = new FWorld();
w.setEdges();
}
void draw() {
background(90);
w.step();
w.draw();
}
void fileSelect(File selection) {
if (selection == null) {
} else {
String path = selection.getAbsolutePath();
print(path);
csv = loadTable(path);
for (int y=0; y<26; y++) {
for (int x=0; x<14; x++) {
int t = csv.getInt(y, x);
//print(t);
stage[x][y] = t;
if (t > 0) {
addBlock(x*64, y*32);
}
}
//println();
}
}
addBall(400,700);
}
void contactStarted(FContact c) {
String b1 = c.getBody1().getName();
String b2 = c.getBody2().getName();
if (b2=="ball" && b1=="block")
w.remove(c.getBody1());//衝突ブロックを消す
}
void addBlock(int x, int y) {
FBox b = new FBox(64, 32);//サイズ
b.setPosition(x, y); //位置
b.setFill(0, 200, 200); //色
b.setStatic(true); //固定
b.setFriction(0); //摩擦抵抗
b.setRestitution(1); //反発係数
b.setName("block"); //名前
w.add(b);
}
void addBall(int x, int y) {
FCircle c = new FCircle(20);//サイズ
c.setPosition(x, y); //位置
c.setFill(255, 255, 255); //色
c.setVelocity(500, -500); //速度
c.setDamping(0); //減衰(空気抵抗)
c.setFriction(0); //摩擦抵抗
c.setRestitution(1); //反発係数
c.setName("ball"); //名前
w.add(c);
}
そこで、addBlock関数の引数を増やします。文字列を受け渡して、ブロックの名前に使います。そして、衝突判定時に、壁なのかブロックなのか判定します。
import fisica.*;
FWorld w;
Table csv;
int stage[][] = new int[14][26];
void setup() {
size(1280, 800);
selectInput("message", "fileSelect");
Fisica.init(this);
w = new FWorld();
w.setEdges();
}
void draw() {
background(90);
w.step();
w.draw();
}
void fileSelect(File selection) {
if (selection == null) {
} else {
String path = selection.getAbsolutePath();
print(path);
csv = loadTable(path);
for (int y=0; y<26; y++) {
for (int x=0; x<14; x++) {
int t = csv.getInt(y, x);
//print(t);
stage[x][y] = t;
if (t > 0 && t<6) {
addBlock(x*64, y*32, "block");
}
if(t==9){
addBlock(x*64, y*32, "wall");
}
}
//println();
}
}
addBall(400,700);
}
void contactStarted(FContact c) {
String b1 = c.getBody1().getName();
String b2 = c.getBody2().getName();
if (b2=="ball" && b1=="block")
w.remove(c.getBody1());//衝突ブロックを消す
}
void addBlock(int x, int y, String s) {
FBox b = new FBox(64, 32);//サイズ
b.setPosition(x, y); //位置
b.setFill(0, 200, 200); //色
b.setStatic(true); //固定
b.setFriction(0); //摩擦抵抗
b.setRestitution(1); //反発係数
b.setName(s); //名前
w.add(b);
}
void addBall(int x, int y) {
FCircle c = new FCircle(20);//サイズ
c.setPosition(x, y); //位置
c.setFill(255, 255, 255); //色
c.setVelocity(500, -500); //速度
c.setDamping(0); //減衰(空気抵抗)
c.setFriction(0); //摩擦抵抗
c.setRestitution(1); //反発係数
c.setName("ball"); //名前
w.add(c);
}
もう少し、addBlock関数を改良して、色情報(緑)を渡すようにしました。ここは、画像を参照するようにしてもいいのですが、画像を用意すると、テキストだけで完結しないので、パスします。
import fisica.*;
FWorld w;
Table csv;
int stage[][] = new int[14][26];
void setup() {
size(1280, 800);
selectInput("message", "fileSelect");
Fisica.init(this);
w = new FWorld();
w.setEdges();
}
void draw() {
background(90);
w.step();
w.draw();
}
void fileSelect(File selection) {
if (selection == null) {
} else {
String path = selection.getAbsolutePath();
print(path);
csv = loadTable(path);
for (int y=0; y<26; y++) {
for (int x=0; x<14; x++) {
int t = csv.getInt(y, x);
//print(t);
stage[x][y] = t;
if (t > 0 && t<6) {
addBlock(x*64, y*32, "block", t*20);
}
if(t==9){
addBlock(x*64, y*32, "wall", t*20);
}
}
//println();
}
}
addBall(400,700);
}
void contactStarted(FContact c) {
String b1 = c.getBody1().getName();
String b2 = c.getBody2().getName();
if (b2=="ball" && b1=="block")
w.remove(c.getBody1());//衝突ブロックを消す
}
void addBlock(int x, int y, String s, int green) {
FBox b = new FBox(64, 32);//サイズ
b.setPosition(x, y); //位置
b.setFill(0, green, 0); //色
b.setStatic(true); //固定
b.setFriction(0); //摩擦抵抗
b.setRestitution(1); //反発係数
b.setName(s); //名前
w.add(b);
}
void addBall(int x, int y) {
FCircle c = new FCircle(20);//サイズ
c.setPosition(x, y); //位置
c.setFill(255, 255, 255); //色
c.setVelocity(500, -500); //速度
c.setDamping(0); //減衰(空気抵抗)
c.setFriction(0); //摩擦抵抗
c.setRestitution(1); //反発係数
c.setName("ball"); //名前
w.add(c);
}
最後に、パドル(マウス)を追加します。
setup関数で追加することと、
draw関数で、名前で検索して、マウス座標をセットします。
import fisica.*;
FWorld w;
Table csv;
int stage[][] = new int[14][26];
void setup() {
size(1280, 800);
selectInput("message", "fileSelect");
Fisica.init(this);
w = new FWorld();
w.setEdges();
addBlock(mouseX, mouseY, "paddle", 255);
}
void draw() {
ArrayList<FBody> bodies = w.getBodies();
for (FBody b : bodies) {
if (b.getName()==null) {
break;
}
if (b.getName().equals("paddle")) {
b.setPosition(mouseX, 750);
}
}
background(90);
w.step();
w.draw();
}
void fileSelect(File selection) {
if (selection == null) {
} else {
String path = selection.getAbsolutePath();
print(path);
csv = loadTable(path);
for (int y=0; y<26; y++) {
for (int x=0; x<14; x++) {
int t = csv.getInt(y, x);
//print(t);
stage[x][y] = t;
if (t > 0 && t<6) {
addBlock(x*64, y*32, "block", t*20);
}
if (t==9) {
addBlock(x*64, y*32, "wall", t*20);
}
}
//println();
}
}
addBall(400, 700);
}
void contactStarted(FContact c) {
String b1 = c.getBody1().getName();
String b2 = c.getBody2().getName();
if (b2=="ball" && b1=="block")
w.remove(c.getBody1());//衝突ブロックを消す
}
void addBlock(int x, int y, String s, int green) {
FBox b = new FBox(64, 32);//サイズ
b.setPosition(x, y); //位置
b.setFill(0, green, 0); //色
b.setStatic(true); //固定
b.setFriction(0); //摩擦抵抗
b.setRestitution(1); //反発係数
b.setName(s); //名前
w.add(b);
}
void addBall(int x, int y) {
FCircle c = new FCircle(20);//サイズ
c.setPosition(x, y); //位置
c.setFill(255, 255, 255); //色
c.setVelocity(500, -500); //速度
c.setDamping(0); //減衰(空気抵抗)
c.setFriction(0); //摩擦抵抗
c.setRestitution(1); //反発係数
c.setName("ball"); //名前
w.add(c);
}
参考