0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

物理ライブラリを使ってブロック崩しを作る

 下記、書籍の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

image.png

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

 ここで、確認しておきたいことがあります。物理演算で必ず問題となる話題です。ボールの速度が遅いときは問題が置きませんが、速くなるほど、すり抜け、貫通問題が生じます。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

image.png
薄くて、速くても、突き抜けない
image.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

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

image.png

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

image.png

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();
    }
  }
}

image.png

続いて、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();
    }
  }
}

image.png

次にブロックで表示し、ボール衝突で消えるように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);
}

image.png

そこで、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);
}

image.png

最後に、パドル(マウス)を追加します。
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);
}

image.png

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?