processingで物理シミュレーションゲームを作る
扱うのが簡単な物理ライブラリfisica
の使い方について、紹介します。
ここに、fisicaの開発者サイトが有り、ヘルプなどがあります。
インストール
processingの、スケッチ>ライブラリをインポート>マネージライブラリからfisica
を検索し、インストールします。
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);
壁の設置
左右下の壁を作る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);
}
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)
};
画像の表示
円ではなくて、オブジェクトに画像を割り当てることができるので、オブジェクト生成時にattachImage命令
で、事前に読み込んでおいた画像とリンクさせています。用意した画像は、適当にchatGTP4Dalle3で書いたもので、正方形で、透過背景にしています。画像を貼ることにより、回転している様子が、可視化されます。
画像がないと、エラーになるので、授業のTeamsで配布します。
同じ大きさのフルーツを消す
フルーツ同士の衝突時には、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)
};
壁すり抜けバグ回避
薄い壁を細かく設置することで、すり抜けないように変更し、画像なしで動くように戻した。他、行数が短くなるように変更。色相環カラーに変更。
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);
}
今後
マウスカーソルの位置に画像を表示するのを左にずらす。
フルーツの融合時に、すり抜けないようにする。(生成座標を工夫するか、壁を細かく幾重にもする。
ゲームオーバーを作る。(現状、スイカが2つ融合すると、配列の参照オーバーで止まるはず。(やってみたら、エラー出るものの止まらずに動き続けるが、フルーツが消えなくなる。)
フルーツを落とせる、左右の範囲を設ける。
摩擦、重力、質量などの他の物理量も、設定してみる。
ゲームバランスや、ゲームルールを変更アレンジする。
余談
本家のスイカゲームのフルーツの大きさの拡大率は一定ではありません。ここにゲーム設計の妙が隠されているかもしれません。なお、本サンプルでは、拡大率を1.225と一定にしましたが、本家に近づけるなら、以下のような数字が近いでしょう。
34
47
65
70
88
112
129
163
176
220
260
自分で作った、なっちゃってスイカゲームでも、面白いので、このゲームには根源的な魅力が隠されている気がします。
参考
フルーツではなくて、数字で見せるのが、わかりやすいかもしれません。