例のフルーツゲームっぽいものを作る

# processingで物理シミュレーションゲームを作る
扱うのが簡単な物理ライブラリ```fisica```の使い方について、紹介します。
ここに、fisicaの開発者サイトが有り、ヘルプなどがあります。

http://www.ricardmarxer.com/fisica/


# インストール
processingの、スケッチ＞ライブラリをインポート＞マネージライブラリから```fisica```を検索し、インストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/ccce8c26-d234-144c-200a-558f2ec1c574.png)
processingサンプルの下の方に、サンプルが追加されるので、どんな表現が可能になるのか、見ておくといいでしょう。

# Fisica Worldの初期化

最低限のFisicaを動かすコードです。
初期化のコードは```setup```の中、
drawの中で、物理計算を１フレーム進める```step```と、描画の```draw```を記述。
ウィンドウ全体に、衝突判定を追加。
マウスクリックで、一つの円を追加。

注意点としては、同じオブジェクトデータを使って、連続して```add命令```を使えない。一個追加する度に、FCircleオブジェクトを生成するような書き方にすると、混乱しないと思う。

```java
import fisica.*;

FWorld w;

void setup(){
   size(1280, 720);

   Fisica.init(this);
   w = new FWorld();
   w.setEdges(); //画面端は壁
}

void draw(){
   background(255);
  
   w.step(); //位置更新
   w.draw(); //描画
}

void mousePressed(){
   FCircle c = new FCircle(260);
   c.setPosition(mouseX, mouseY);
   c.setFillColor(color(0,128,0));
   w.add(c);
}
```
また、デフォルト設定で、オブジェクトをマウスでドラッグ移動できる。
ドラッグをオフにするには、円を生成する際に、以下の通り。
```java
c.setGrabbable(false);
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/835e8c27-b05d-5f6e-cb75-2994def31ef3.png)


# 壁の設置
左右下の壁を作る```setWall関数```を作ります。
左の壁を、右にコピーしようとすると、一つしかできなかったりするので、個別に分けています。
L,R,Bについて、大きさ、位置、静的(static)、色を指定して、```add```追加します。

```java
import fisica.*;

FWorld w;

void setup(){
   size(1280, 720);

   Fisica.init(this);
   w = new FWorld();
   w.setEdges(); //画面端は壁
   
   setWall();
}

void draw(){
   background(255);
  
   w.step(); //位置更新
   w.step(); //位置更新
   w.draw(); //描画
}

void mousePressed(){
   FCircle c = new FCircle(260);
   c.setPosition(mouseX, mouseY);
   c.setFillColor(color(0,128,0));
   w.add(c);
}

void setWall(){
   FBox L = new FBox(10,504);
   L.setPosition(width/2-448/2, height/2+50);
   L.setStatic(true);
   L.setFill(0);
   w.add(L);

   FBox R = new FBox(10,504);
   R.setPosition(width/2+448/2, height/2+50);
   R.setStatic(true);
   R.setFill(0);
   w.add(R);
   
   FBox B = new FBox(448,10);
   B.setPosition(width/2, height/2+300);
   B.setStatic(true);
   B.setFill(0);
   w.add(B);
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/55ceaf51-07ac-d4ad-9453-fe3524f1fd75.png)

# 11種類の円を用意
サイズの異なる円```FruitR```を11種類用意します。
また、それぞれの色```FruitColor```を決めます。
フルーツ名を番号で管理するようにします。
これらの情報に基づき、クリックで、フルーツが出現するようにします。
※まだ消えません。
上部に一つ、右上nextに２つ表示するようにしました。

```java
import fisica.*;

FWorld w;

int next0Fruit, next1Fruit, next2Fruit;

void setup() {
  size(1280, 720);

  Fisica.init(this);
  w = new FWorld();
  w.setEdges(); //画面端は壁

  setWall();

  next0Fruit = (int)random(0, 5);
  next1Fruit = (int)random(0, 5);
  next2Fruit = (int)random(0, 5);
}

void draw() {
  background(255);

  w.step(); //位置更新
  w.step(); //位置更新
  w.draw(); //描画

  fill(FruitColor[next2Fruit]);
  ellipse(1050, 100, FruitR[next2Fruit], FruitR[next2Fruit]);
  fill(FruitColor[next1Fruit]);
  ellipse(1000, 100, FruitR[next1Fruit], FruitR[next1Fruit]);
  fill(FruitColor[next0Fruit]);
  ellipse(mouseX, 100, FruitR[next0Fruit], FruitR[next0Fruit]);
}

void mousePressed() {
  addFruit(next0Fruit);
  next0Fruit = next1Fruit;
  next1Fruit = next2Fruit;
  next2Fruit = (int)random(0, 5);
}

void addFruit(int n) {
  FCircle c = new FCircle(FruitR[n]);
  c.setPosition(mouseX, 100);
  c.setFillColor(FruitColor[n]);
  w.add(c);
}

void setWall() {
  FBox L = new FBox(10, 504);
  L.setPosition(width/2-448/2, height/2+50);
  L.setStatic(true);
  L.setFill(0);
  w.add(L);

  FBox R = new FBox(10, 504);
  R.setPosition(width/2+448/2, height/2+50);
  R.setStatic(true);
  R.setFill(0);
  w.add(R);

  FBox B = new FBox(448, 10);
  B.setPosition(width/2, height/2+300);
  B.setStatic(true);
  B.setFill(0);
  w.add(B);
}

final int CHERRY = 0;
final int ICHIGO = 1;
final int GRAPE = 2;
final int DEKOP = 3;
final int KAKI = 4;
final int RINGO = 5;
final int NASHI = 6;
final int PEACH = 7;
final int PINE = 8;
final int MELON = 9;
final int SUIKA = 10;

int[] FruitR = {
  34,
  42,
  51,
  63,
  77,
  94,
  115,
  141,
  173,
  212,
  260
};

color[] FruitColor = {
  color(255, 20, 0),
  color(250, 120, 70),
  color(180, 120, 255),
  color(255, 180, 40),
  color(255, 145, 0),
  color(255, 30, 0),
  color(255, 255, 100),
  color(255, 210, 190),
  color(255, 237, 0),
  color(150, 250, 0),
  color(20, 150, 0)
};
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/ec301c66-a002-5bb8-4bce-36626113affa.png)

# 画像の表示
円ではなくて、オブジェクトに画像を割り当てることができるので、オブジェクト生成時に```attachImage命令```で、事前に読み込んでおいた画像とリンクさせています。用意した画像は、適当にchatGTP4Dalle3で書いたもので、正方形で、透過背景にしています。画像を貼ることにより、回転している様子が、可視化されます。
画像がないと、エラーになるので、授業のTeamsで配布します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/71f76821-19a3-6c41-ad3f-d8270fd60f4f.png)


# 同じ大きさのフルーツを消す
フルーツ同士の衝突時には、```contactStarted関数```が呼ばれ、その引数から、衝突した２つのオブジェクトに```getBody命令```でアクセスすることができます。

さて、ここで、衝突した２つが同じフルーツであるか（例：リンゴとリンゴであるか）を、チェックする必要があります。サイズで判定しようかと思いましたが、名前をつけることができるので、それを利用します。名前の設定には、```setName関数```、名前の取得には```getName関数```が使えます。
同じフルーツであれば、```remove関数```で２つを削除し、新規にフルーツを追加します。このときに、左右下の壁際で新しくフルーツを追加すると、半径が壁を超えて、フルーツが外に出てしまうバグがあります。


```java
import fisica.*;

FWorld w;

int next0Fruit, next1Fruit, next2Fruit;
PImage[] FruitImage = new PImage[11];

void setup() {
  size(1280, 720);

  Fisica.init(this);
  w = new FWorld();
  w.setEdges(); //画面端は壁

  setWall();

  FruitImage[CHERRY] = loadImage("CHERRY.png");
  FruitImage[CHERRY].resize(FruitR[CHERRY], FruitR[CHERRY]);
  FruitImage[ICHIGO] = loadImage("ICHIGO.png");
  FruitImage[ICHIGO].resize(FruitR[ICHIGO], FruitR[ICHIGO]);
  FruitImage[GRAPE] = loadImage("GRAPE.png");
  FruitImage[GRAPE].resize(FruitR[GRAPE], FruitR[GRAPE]);
  FruitImage[DEKOP] = loadImage("DEKOP.png");
  FruitImage[DEKOP].resize(FruitR[DEKOP], FruitR[DEKOP]);
  FruitImage[KAKI] = loadImage("KAKI.png");
  FruitImage[KAKI].resize(FruitR[KAKI], FruitR[KAKI]);
  FruitImage[APPLE] = loadImage("APPLE.png");
  FruitImage[APPLE].resize(FruitR[APPLE], FruitR[APPLE]);
  FruitImage[NASHI] = loadImage("NASHI.png");
  FruitImage[NASHI].resize(FruitR[NASHI], FruitR[NASHI]);
  FruitImage[PEACH] = loadImage("PEACH.png");
  FruitImage[PEACH].resize(FruitR[PEACH], FruitR[PEACH]);
  FruitImage[PINE] = loadImage("PINE.png");
  FruitImage[PINE].resize(FruitR[PINE], FruitR[PINE]);
  FruitImage[MELON] = loadImage("MELON.png");
  FruitImage[MELON].resize(FruitR[MELON], FruitR[MELON]);
  FruitImage[SUIKA] = loadImage("SUIKA.png");
  FruitImage[SUIKA].resize(FruitR[SUIKA], FruitR[SUIKA]);

  next0Fruit = (int)random(0, 5);
  next1Fruit = (int)random(0, 5);
  next2Fruit = (int)random(0, 5);
}

void draw() {
  background(255);

  w.step(); //位置更新
  w.step(); //位置更新
  w.draw(); //描画

  image(FruitImage[next2Fruit], 1050, 100);
  image(FruitImage[next1Fruit], 1000, 100);
  image(FruitImage[next0Fruit], mouseX, 100);
  /*
  ellipse(1050, 100, FruitR[next2Fruit], FruitR[next2Fruit]);
   fill(FruitColor[next1Fruit]);
   ellipse(1000, 100, FruitR[next1Fruit], FruitR[next1Fruit]);
   fill(FruitColor[next0Fruit]);
   ellipse(mouseX, 100, FruitR[next0Fruit], FruitR[next0Fruit]);
   */
}

void mousePressed() {
  addFruit(next0Fruit, mouseX, 100);
  next0Fruit = next1Fruit;
  next1Fruit = next2Fruit;
  next2Fruit = (int)random(0, 5);
}

void contactStarted(FContact c) {//衝突時に呼び出される関数
  String b1 = c.getBody1().getName();//衝突オブジェクト1を取得し、名前を取得。
  String b2 = c.getBody2().getName();//衝突オブジェクト2を取得し、名前を取得。
  if(b1==null)//名前がないnullなら、戻る
    return;
  if(b2==null)
    return;
    
  if(b1.equals(b2)){//衝突した２つのオブジェクトの名前が等しいなら、消して一つ大きいのを生成
    float x = (c.getBody1().getX()+c.getBody2().getX())/2;//生成するx座標
    float y = (c.getBody1().getY()+c.getBody2().getY())/2;//生成するy座標
    w.remove(c.getBody1());//削除
    w.remove(c.getBody2());//削除
    addFruit(int(b1)+1, x, y);//新規生成
  }
}

void addFruit(int n, float x, float y) {//新規生成する関数
  FCircle c = new FCircle(FruitR[n]);
  c.setPosition(x, y);
  c.setFillColor(FruitColor[n]);
  c.setName(str(n));//フルーツを消すときに、名前をつけて、名前が同じなら削除する作戦
  c.attachImage(FruitImage[n]);
  w.add(c);
}

void setWall() {
  FBox L = new FBox(50, 504);
  L.setPosition(width/2-448/2, height/2+50);
  L.setStatic(true);
  L.setFill(0);
  w.add(L);

  FBox R = new FBox(50, 504);
  R.setPosition(width/2+448/2, height/2+50);
  R.setStatic(true);
  R.setFill(0);
  w.add(R);

  FBox B = new FBox(448, 40);
  B.setPosition(width/2, height/2+300);
  B.setStatic(true);
  B.setFill(0);
  w.add(B);
}

final int CHERRY = 0;
final int ICHIGO = 1;
final int GRAPE = 2;
final int DEKOP = 3;
final int KAKI = 4;
final int APPLE = 5;
final int NASHI = 6;
final int PEACH = 7;
final int PINE = 8;
final int MELON = 9;
final int SUIKA = 10;

int[] FruitR = {
  34,
  42,
  51,
  63,
  77,
  94,
  115,
  141,
  173,
  212,
  260
};

color[] FruitColor = {
  color(255, 20, 0),
  color(250, 120, 70),
  color(180, 120, 255),
  color(255, 180, 40),
  color(255, 145, 0),
  color(255, 30, 0),
  color(255, 255, 100),
  color(255, 210, 190),
  color(255, 237, 0),
  color(150, 250, 0),
  color(20, 150, 0)
};
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/db051d11-a18f-ccc9-d5ad-3031e525f395.png)

# 壁すり抜けバグ回避
薄い壁を細かく設置することで、すり抜けないように変更し、画像なしで動くように戻した。他、行数が短くなるように変更。色相環カラーに変更。
```java
import fisica.*;

FWorld w;

int[] FruitR = { 34, 42, 51, 63, 77, 94, 115, 141, 173, 212, 260};
int nextFruit;
color[] FruitColor = {0xFFE60012, 0xFFF39800, 0xFFFFF100, 0xFF8FC31F, 0xFF009944, 0xFF009E96, 0xFF00A0E9, 0xFF0068B7, 0xFF1D2088, 0xFF920783, 0xFFE4007F};
color[] FruitColorD = {0xFFC60012, 0xFFD37800, 0xFFDFD100, 0xFF6FA31F, 0xFF007924, 0xFF007E76, 0xFF00A0C9, 0xFF004897, 0xFF1D2068, 0xFF720763, 0xFFC4005F};

void setup() {
  size(1280, 720);

  Fisica.init(this);
  w = new FWorld();
  w.setEdges(); //画面端は壁

  for (int x=416; x<432; x+=2)
    setWall(2, 500, x, 410);
  for (int x=864; x<880; x+=2)
    setWall(2, 500, x, 410);
  for (int y=660; y<676; y+=2)
    setWall(450, 2, 640, y);

  nextFruit = (int)random(0, 5);
}

void draw() {
  background(255);

  w.step(); //位置更新
  w.step(); //位置更新
  w.draw(); //描画

  noStroke();
  fill(FruitColor[nextFruit]);
  ellipse(mouseX, 100, FruitR[nextFruit], FruitR[nextFruit]);
}

void mousePressed() {
  addFruit(nextFruit, mouseX, 100);
  nextFruit = (int)random(0, 5);
}

void contactStarted(FContact c) {//衝突時に呼び出される関数
  String b1 = c.getBody1().getName();//衝突オブジェクト1を取得し、名前を取得。
  String b2 = c.getBody2().getName();//衝突オブジェクト2を取得し、名前を取得。
  if (b1==null)//名前がないnullなら、戻る
    return;
  if (b2==null)
    return;

  if (b1.equals(b2)) {//衝突した２つのオブジェクトの名前が等しいなら、消して一つ大きいのを生成
    float x = (c.getBody1().getX()+c.getBody2().getX())/2;//生成するx座標
    float y = (c.getBody1().getY()+c.getBody2().getY())/2;//生成するy座標
    w.remove(c.getBody1());//削除
    w.remove(c.getBody2());//削除
    addFruit(int(b1)+1, x, y);//新規生成
  }
}

void addFruit(int n, float x, float y) {//新規生成する関数
  FCircle c = new FCircle(FruitR[n]);
  c.setPosition(x, y);
  c.setFillColor(FruitColor[n]);
  c.setStrokeColor(FruitColorD[n]);
  c.setStrokeWeight(n+2);
  c.setName(str(n));//フルーツを消すときに、名前をつけて、名前が同じなら削除する作戦
  w.add(c);
}

void setWall(int W, int H, int x, int y) {
  FBox B = new FBox(W, H);
  B.setPosition(x, y);
  B.setStatic(true);
  B.setFill(0);
  w.add(B);
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/bb4dc952-5f76-bba7-b078-9de216704635.png)


# 今後
マウスカーソルの位置に画像を表示するのを左にずらす。
~~フルーツの融合時に、すり抜けないようにする。（生成座標を工夫するか、壁を細かく幾重にもする。~~
ゲームオーバーを作る。（現状、スイカが２つ融合すると、配列の参照オーバーで止まるはず。（やってみたら、エラー出るものの止まらずに動き続けるが、フルーツが消えなくなる。）
フルーツを落とせる、左右の範囲を設ける。
摩擦、重力、質量などの他の物理量も、設定してみる。
ゲームバランスや、ゲームルールを変更アレンジする。

# 余談
本家のスイカゲームのフルーツの大きさの拡大率は一定ではありません。ここにゲーム設計の妙が隠されているかもしれません。なお、本サンプルでは、拡大率を１．２２５と一定にしましたが、本家に近づけるなら、以下のような数字が近いでしょう。
34
47
65
70
88
112
129
163
176
220
260

自分で作った、なっちゃってスイカゲームでも、面白いので、このゲームには根源的な魅力が隠されている気がします。

# 参考
フルーツではなくて、数字で見せるのが、わかりやすいかもしれません。

https://games.wkb.jp/ykg/?game_id=legend2048