こんにちは。
今回は、processing で「粉遊び」的なものをつくってみます。
「粉遊び」というゲーム自体は、以前にセル・オートマトンを取り上げた際に「ラングトンのアリ」の部分で少し引き合いに出しました。
dan-ball.jp taq.hatenadiary.jp
ダンボールのゲーム自体は昔から好きで、シンプルながら奥深いというか、「棒レンジャー」なんかは割とやりこんだりしていました。
このサイトに載っているようなものをつくってみたいと思って processing に手を伸ばした節もあります。
そんなとき、こんなツイートを見かけました。
one easy way to make a simulation of falling sand piling up-
— Matt Henderson (@matthen2) 2023年10月13日
just define what happens for all 16 possible 2x2 grids, and repeatedly apply these rules to the picture pic.twitter.com/OJNhmkfz8A
まさに「粉遊び」だと思うとともに、以前にセル・オートマトンを少し触ったこともあるので、自分でやってみたいと思いました。
ということで、やります。
目次
前準備
メインの計算部分をつくる前に、いろいろ準備しておきます。
描画画面を整える
格子状に線を引き、「粉」のある部分を白で塗ります。
int n = 100; // 横 or 縦に並べるセルの数 int[][] cell = new int[n][n]; // セルの情報を入れておく配列 (0: 無、1: 粉) float l; void setup(){ size(500, 500); l = width/n; // セルの一辺の長さ } void draw(){ background(0); // グリッド線 stroke(100); for(int i = 0; i < n; i ++){ line(i*l, 0, i*l, height); } for(int j = 0; j < n; j ++){ line(0, j*l, width, j*l); } // 粉の描画 for(int i = 0; i < n; i ++){ for(int j = 0; j < n; j ++){ if(sand[i][j] == 1){ // 粉のある部分を白で塗る fill(255); rect(i*l, j*l, l, l); } } } // メインの計算部分 // cell = calc(cell); }
粉を追加できるようにする
draw()
内に以下のコードを書き加えます。
このコードでは、マウスの重なった位置のセルを「粉」のあるセルに変えます。
// 粉の追加 if(mousePressed == true){ for(int i = 0; i < n; i ++){ for(int j = 0; j < n; j ++){ if(i*l < mouseX && mouseX <= (i+1)*l){ if(j*l < mouseY && mouseY <= (j+1)*l){ sand[i][j] = 1; } } } } }
まだこの段階では「粉」は動きません。
以降で動かし方をつくっていきます。
素直に実装してみる
それでは、計算部分の作成に移ります。
イントロで挙げたツイートから、16種類の変化をそのままコードに打ち込めばひとまず実装できそうです。
計算部分の処理として関数 calc()
を定義して、draw()
内で呼び出すことで実行できるようにしてみました。
他にやり方はあるかもしれませんが、参考までに。
int[][] calc(int[][] cell){ for(int j = n-2; j > -1; j --){ for(int i = 0; i < n-1; i ++){ int a = cell[i][j]; // 左上 int b = cell[i+1][j]; // 右上 int c = cell[i][j+1]; // 左下 int d = cell[i+1][j+1]; // 右下 if(a == 0 && b == 0 && c == 0 && d == 0){ cell[i][j] = 0; cell[i+1][j] = 0; cell[i][j+1] = 0; cell[i+1][j+1] = 0; } if(a == 0 && b == 0 && c == 0 && d == 1){ cell[i][j] = 0; cell[i+1][j] = 0; cell[i][j+1] = 0; cell[i+1][j+1] = 1; } if(a == 0 && b == 0 && c == 1 && d == 0){ cell[i][j] = 0; cell[i+1][j] = 0; cell[i][j+1] = 1; cell[i+1][j+1] = 0; } if(a == 0 && b == 0 && c == 1 && d == 1 || a == 0 && b == 1 && c == 0 && d == 1 || a == 0 && b == 1 && c == 1 && d == 0 || a == 1 && b == 0 && c == 1 && d == 0 || a == 1 && b == 1 && c == 0 && d == 0 || a == 1 && b == 0 && c == 0 && d == 1){ cell[i][j] = 0; cell[i+1][j] = 0; cell[i][j+1] = 1; cell[i+1][j+1] = 1; } if(a == 0 && b == 1 && c == 0 && d == 0){ cell[i][j] = 0; cell[i+1][j] = 0; cell[i][j+1] = 0; cell[i+1][j+1] = 1; } if(a == 0 && b == 1 && c == 1 && d == 1 || a == 1 && b == 1 && c == 0 && d == 1){ cell[i][j] = 0; cell[i+1][j] = 1; cell[i][j+1] = 1; cell[i+1][j+1] = 1; } if(a == 1 && b == 0 && c == 0 && d == 0){ cell[i][j] = 0; cell[i+1][j] = 0; cell[i][j+1] = 1; cell[i+1][j+1] = 0; } if(a == 1 && b == 0 && c == 1 && d == 1 || a == 1 && b == 1 && c == 1 && d == 0){ cell[i][j] = 1; cell[i+1][j] = 0; cell[i][j+1] = 1; cell[i+1][j+1] = 1; } if(a == 1 && b == 1 && c == 1 && d == 1){ cell[i][j] = 1; cell[i+1][j] = 1; cell[i][j+1] = 1; cell[i+1][j+1] = 1; } } } return(cell); }
まあ煩雑ですね。
2 × 2 のセルを、左上、右上、左下、右下の順に a
、b
、c
、d
に対応させ、各場合について変化前後での状態をそのまま書き下していったような感じになっています。
これを画面全体について繰り返し適用していくことで、次のステップでの「粉」の位置を決める、という方法です。
実行すると、
ここで、「画面全体に適用する」というときに、
for(int j = n-2; j > -1; j --){ for(int i = 0; i < n-1; i ++){
という、下から考えていくようにしないとうまく動きません。
for(int i = 0; i < n-1; i ++){ for(int j = 0; j < n-1; j ++){
このようにすると、動きはしますが過程が無く、「粉」を追加した途端に一番下まで落ちている、というような形になってしまいます。
(これを解決するのに調べ物をしていたら、自分がやってみたいことを詰め込んだようなものを見つけてしまいました (砂遊びシミュレーション|クリエイティブコーディングの教科書)。)
少し工夫
先程のコードでは煩雑だと思い、改良 (?) してみました。
(あまり変わってない感じもしますが・・・)
int[][] calc(int[][] cell){ int[][] cn = {{0, 0}, {0, 0}}; // 変換後の 2×2 セル // 16種類の 2×2 セル int[][] c0000 = {{0, 0}, {0, 0}}; int[][] c0001 = {{0, 0}, {0, 1}}; int[][] c0010 = {{0, 0}, {1, 0}}; int[][] c0011 = {{0, 0}, {1, 1}}; int[][] c0100 = {{0, 1}, {0, 0}}; int[][] c0101 = {{0, 1}, {0, 1}}; int[][] c0110 = {{0, 1}, {1, 0}}; int[][] c0111 = {{0, 1}, {1, 1}}; int[][] c1000 = {{1, 0}, {0, 0}}; int[][] c1001 = {{1, 0}, {0, 1}}; int[][] c1010 = {{1, 0}, {1, 0}}; int[][] c1011 = {{1, 0}, {1, 1}}; int[][] c1100 = {{1, 1}, {0, 0}}; int[][] c1101 = {{1, 1}, {0, 1}}; int[][] c1110 = {{1, 1}, {1, 0}}; int[][] c1111 = {{1, 1}, {1, 1}}; for(int j = n-2; j > -1; j --){ for(int i = 0; i < n-1; i ++){ int[][] c = {{cell[i][j], cell[i+1][j]}, {cell[i][j+1], cell[i+1][j+1]}}; // c の種類に応じて cn を変える if(check(c, c0001) == true){cn = c0001;} if(check(c, c0010) == true){cn = c0010;} if(check(c, c0011) == true){cn = c0011;} if(check(c, c0100) == true){cn = c0001;} if(check(c, c0101) == true){cn = c0011;} if(check(c, c0110) == true){cn = c0011;} if(check(c, c0111) == true){cn = c0111;} if(check(c, c1000) == true){cn = c0010;} if(check(c, c1001) == true){cn = c0011;} if(check(c, c1010) == true){cn = c0011;} if(check(c, c1011) == true){cn = c1011;} if(check(c, c1100) == true){cn = c0011;} if(check(c, c1101) == true){cn = c0111;} if(check(c, c1110) == true){cn = c1011;} if(check(c, c1111) == true){cn = c1111;} if(check(c, c0000) == true){cn = c0000;} // cn を cell にあてはめる cell[i][j] = cn[0][0]; cell[i+1][j] = cn[0][1]; cell[i][j+1] = cn[1][0]; cell[i+1][j+1] = cn[1][1]; } } return(cell); } boolean check(int[][] a, int[][] b){ // 配列 a と b を比較 if(a[0][0] == b[0][0] && a[0][1] == b[0][1] && a[1][0] == b[1][0] && a[1][1] == b[1][1]){ return(true); } else{ return(false); } }
先に16種類の 2 × 2 のセル c0000
~ c1111
をつくっておき、今注目している部分はどれに当てはまるかを check()
で調べます。
それに応じて変化させ、その結果を cn
に入れます。
最後に cn
を元の cell
に戻して完了、という感じです。
やり方を見直す
素直に実装したりちょっと工夫してみたりしましたが、どちらにせよコードがやたら長くなってしまって分かりづらいです。
また、この後の話になりますが、「粉」の種類を増やす点でもやりづらい部分があります。
ということで、やり方を見直さなければなりません。
初めの16種類の変化でやりたいことは、「粉」が下に落ちることと、「粉」が重なっていたら上のものが左右どちらかに落ちること、と考えられます。
というわけで、これを実装すると、
int[][] calc(int[][] cell){ for(int j = n-2; j > -1; j --){ for(int i = 0; i < n-1; i ++){ if(cell[i][j] == 1){ if(cell[i][j+1] == 0){ // 下に何もなかったら cell[i][j+1] = 1; // 下に動かす cell[i][j] = 0; // 元々あったところから消す } else if(cell[i][j+1] == 1){ // 下に「粉」があったら int a = 2; int p = int(random(-a, a)); // 移動量を決める if(i+p < 0 || n-1 < i+p){ // 画面外に出そうだったら continue; // 何もしない } else if(cell[i+p][j] == 0){ // 移動予定地に何もなかったら cell[i+p][j] = 1; // 動かす cell[i][j] = 0; // 元の位置のは消す } } } } } return(cell); }
やり方の性質上、「粉」が常に動くような感じにはなってしまいますが、一応それなりに単純に書けるようになりました。
下に「粉」があったときの処理を、
else if(cell[i][j+1] == 1 || cell[i][j+1] == 2){ float p = random(0, 100); if(p < 1){ if(i-1 < 0){ continue; } else if(cell[i-1][j] == 0){ cell[i-1][j] = 1; cell[i][j] = 0; } } else if(p >= 99){ if(n-1 < i+1){ continue; } else if(cell[i+1][j] == 0){ cell[i+1][j] = 1; cell[i][j] = 0; } } }
のようにすると、p
の大きさや移動方向の分岐をどの程度の p
までにするかを調整することで、左右の動きが小さい「粉」をつくることもできるかと思います。
「粉」の種類を増やす
これまでで「粉遊び」的な基本となる部分はできたかと思います。
ここからはおまけ的な感じで、「粉」の種類を増やしてみます。
セル・オートマトンの良いところですが、セルの情報は割り当てる数によって好きなように設定できます。
ここでは、1 を水、2 を油、3 を壁に対応させてみます。
int[][] calc(int[][] cell){ for(int j = n-2; j > -1; j --){ for(int i = 0; i < n-1; i ++){ if(cell[i][j] == 1){ // 水に関する処理 if(cell[i][j+1] == 0){ cell[i][j+1] = 1; cell[i][j] = 0; } else if(cell[i][j+1] == 1 || cell[i][j+1] == 3){ int a = 2; int p = int(random(-a, a)); if(i+p < 0 || n-1 < i+p){ continue; } else if(cell[i+p][j] == 0 || cell[i+p][j] == 2){ cell[i][j] = cell[i+p][j]; cell[i+p][j] = 1; } } else if(cell[i][j+1] == 2){ float p = random(0, 1); if(p < 0.5){ cell[i][j+1] = 1; cell[i][j] = 2; } } } if(cell[i][j] == 2){ // 油に関する処理 if(cell[i][j+1] == 0){ cell[i][j+1] = 2; cell[i][j] = 0; } else if(cell[i][j+1] == 1 || cell[i][j+1] == 2 || cell[i][j+1] == 3){ int a = 2; int p = int(random(-a, a)); if(i+p < 0 || n-1 < i+p){ continue; } else if(cell[i+p][j] == 0){ cell[i+p][j] = 2; cell[i][j] = 0; } } } } } return(cell); }
突貫工事感もありますが、油 (灰色) は水 (白色) に浮く一方で、水は油の下に沈んでいくという表現ができたのではないでしょうか。
なんとなくですが、水が沈んでいくときに抵抗があるような感じにしてみました。
これを初めの方法でやるとなると、{{1, 0}, {2, 2}}
を {{2, 0}, {1, 2}}
に変える、などとやっていけばいいのかもしれませんが、面倒です。
まとめ
以上、processing で「粉遊び」的なものをつくってみました。
皆さんも、自分なりのルールで、多種多様な「粉」をつくって遊んでみてください。
最後まで読んでいただいてありがとうございました。また次回。