LoginSignup
3
1

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

Last updated at Posted at 2023-11-27

processingで物理シミュレーションゲームを作る

扱うのが簡単な物理ライブラリfisicaの使い方について、紹介します。
ここに、fisicaの開発者サイトが有り、ヘルプなどがあります。

インストール

processingの、スケッチ>ライブラリをインポート>マネージライブラリからfisicaを検索し、インストールします。
image.png
processingサンプルの下の方に、サンプルが追加されるので、どんな表現が可能になるのか、見ておくといいでしょう。

Fisica Worldの初期化

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

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

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

また、デフォルト設定で、オブジェクトをマウスでドラッグ移動できる。
ドラッグをオフにするには、円を生成する際に、以下の通り。

c.setGrabbable(false);

image.png

壁の設置

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

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

11種類の円を用意

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

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

画像の表示

円ではなくて、オブジェクトに画像を割り当てることができるので、オブジェクト生成時にattachImage命令で、事前に読み込んでおいた画像とリンクさせています。用意した画像は、適当にchatGTP4Dalle3で書いたもので、正方形で、透過背景にしています。画像を貼ることにより、回転している様子が、可視化されます。
画像がないと、エラーになるので、授業のTeamsで配布します。
image.png

同じ大きさのフルーツを消す

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

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

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)){//衝突した2つのオブジェクトの名前が等しいなら、消して一つ大きいのを生成
    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

壁すり抜けバグ回避

薄い壁を細かく設置することで、すり抜けないように変更し、画像なしで動くように戻した。他、行数が短くなるように変更。色相環カラーに変更。

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)) {//衝突した2つのオブジェクトの名前が等しいなら、消して一つ大きいのを生成
    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

今後

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

余談

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

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

参考

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

3
1
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
3
1