物理ライブラリ利用でブロック崩し

# 物理ライブラリを使ってブロック崩しを作る
　下記、書籍の15章15.4を執筆した際は、ライブラリを使わずに、ブロック崩しをつくりましたが、ここでは、fisicaを使って、作っていきます。書籍では、古典的なブロック崩しの挙動をしますが、ここでは、fisicaの物理計算を使うので、ブロックの角に当たったときなど、反射角が大きく変化するという違いがあります。

https://amzn.asia/d/4IOfnGA

### Processingにfisicaをインストールします。

https://qiita.com/hextomino/items/c3b8418c74d10a79b51b#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB

まずは、最低限動くサンプルを掲載します。
重力があるのが特徴。
二重のfor文でブロックを生成。
ボールも５つ生成。
addBlock関数では、ブロックの属性を指定したのち、追加。
addBall関数では、初速ベクトルなど属性を指定したのち、追加。
残ブロック数の表示あり。（エッジ４辺もオブジェクト数に含める）
contactStarted関数で衝突を検知し、ボールとブロックの場合ブロックを削除。順序が逆になることはなさそうである？。

```java
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
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/5b20ee65-a8cc-1330-2957-d431e30dc219.png)

# ブロック貫通問題について

　ここで、確認しておきたいことがあります。物理演算で必ず問題となる話題です。ボールの速度が遅いときは問題が置きませんが、速くなるほど、すり抜け、貫通問題が生じます。step関数では、ボールの次の座標を計算しますが、速度が速く、移動距離がながいほど、衝突判定が難しくなります。また、衝突した際に、どっちに反射すればよいのかという計算が難しくなります。このあたりが、fisicaでどうなるのか確認します。
　次のプログラムでは、クリックする度に、1フレーム(step)進むようになっていますので、これで、じっくり挙動を観察します。

### 最大速度は4000

 まずボールの速度についてです。addBall関数で、ボールの初速を決めていますが、大きな数字を入力しても、限界があるようです。試しに、contactStarted関数で速度を表示してみても、2800前後で、速度が頭打ちになります。色々やってみると、速度ベクトルの長さが最大4000に制限されているようです（fisicaマニュアルには記載がありませんでした。box2Dにはあるかもしれません）。ですので、ななめ４５度なら4000x0.5x√2で約2828が限界です。おそらく、衝突判定で貫通問題が生じないように、最大速度を決めて、その範囲内なら、貫通しないように設計されているのでしょう。

### 最小のサイズは2

　次にブロックの厚みです。addBlock関数でサイズを指定する際に、
```FBox(64, 1)```
と、してみます。するとエラーで止まります。
```FBox(64, 2)```
としてみると、動いてくれることから、最低2の厚みがないといけないようです。一般に薄くなるほど貫通問題が起きやすいのですが、問題は起きないようです。この他、ブロック側も速度があり、速く動く場合や、速く回転する場合など、貫通問題が起きやすいですが、ブロック崩しには、ない状況なので、検証はやめます。

### 重力は自由だが速度制限4000の範囲内で運用

最後に、重力の最大値について確認しました。
mouseClicked関数で重力の設定値を表示＆設定します。色々観察しました。重力の初期値は(0,10)です。重力は設定した値の1/20になるようですので
```setGravity(0,10*20)```
がデフォルトです。重力の設定値は、いくらでも大きく設定できるものの、計算されるボールは、速度ベクトルの長さが最大4000に制限されるのは変わりなく、ボールが地面にへばりつくなど、挙動がおかしくなります。

```java
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。ななめ４５度なら2828
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/92de4147-37f2-c985-2b58-04516030a76f.png)
薄くて、速くても、突き抜けない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/a8dab0b0-47db-fdf9-d009-ecad71c49e51.png)





# エクセルでステージを作る
まず、CSVフォーマットの説明をします。代表的なCSVデータは数値や文字をカンマ区切りで、保存したシンプルなテキストデータです。エクセルでステージをデザインして、別途CSV形式で保存し、processingでcsvを読み込んで使います。
　ファイルの中身はテキストデータなので、
windowsであれば「メモ帳」
macであれば「テキストエディット」の「フォーマット＞標準テキスト」モードでも編集可能です。
※リッチテキストモード、htmlモードなどは、ダメです。
最終的に、以下のようなCSVファイルを生成します。

```:stage2211222.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にしました。
※初期設定でも、不都合はありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/d901987f-e851-6be2-5371-e0ecbc0405a3.png)

また、数値で適当に色つけをすると、編集ミスを減らせます。
※塗らなくても、不都合はありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/ae929fa9-40eb-51a8-9968-a89efe2c17c3.png)

ステージデータが完成したら「名前をつけて保存」から、CSVフォーマットで保存します。
UTF-8ありとなしがありますが、とりあえず、macならUTF-8で問題ないと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/0b90508b-311f-6e4b-c630-49807802d9ca.png)

# CSVファイルをprocessingで読み込む
ファイル選択ウィンドウをselectInput命令で開きます。選択したファイルのパス（ファイルのある場所）がfileSelect関数のselectionの中に渡されて、getAbsolutePath命令で取り出すことができます。そのパスを使って、csvファイルを開き、（今回は整数なので）getInt命令で、行yと、列xを指定して、数字を読み込み、ここでは、print文で表示しています。


csvファイルを開いて、tableクラスに読み込み、内容をprint文で表示
```java
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();
    }
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/55b7a9fd-64fb-1438-347d-171bf2a84de6.png)

続いて、windowにtext命令で表示
※一度、stageという配列にいれてから表示
```java
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();
    }
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/6a7bd308-02e2-0902-78d1-3e189ab66bfc.png)


次にブロックで表示し、ボール衝突で消えるようにcontactStarted関数を追加します。しかし、壁まで壊れてしまうため、壁は別の作り方をするひつようがありそうです

```java
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);
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/a3c03a5a-28a5-c81b-c79c-50ea28b3b585.png)

そこで、addBlock関数の引数を増やします。文字列を受け渡して、ブロックの名前に使います。そして、衝突判定時に、壁なのかブロックなのか判定します。
```java
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関数を改良して、色情報（緑）を渡すようにしました。ここは、画像を参照するようにしてもいいのですが、画像を用意すると、テキストだけで完結しないので、パスします。

```java
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);
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/d21a1aeb-1cca-bd54-8dfd-cf35efcb6853.png)

最後に、パドル（マウス）を追加します。
setup関数で追加することと、
draw関数で、名前で検索して、マウス座標をセットします。

```java
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);
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/8d6d8ba8-d164-c3bc-8f79-fdc3390da258.png)

# 参考

https://proclass.jp/blog/?p=3074

https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%AB%E3%82%AB%E3%83%8E%E3%82%A4%E3%83%89

https://ja.wikipedia.org/wiki/%E8%AC%8E%E3%81%AE%E5%A3%81_%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF%E3%81%8F%E3%81%9A%E3%81%97