生態系のシミュレーションを行うProcessingプログラムを作っています(途中)。
たまにこういうのをずっと眺めたくなります。
1. Procesingで作った
このシミュレーションは、複雑な生態系の相互作用をモデル化し、視覚的に表現しています。
パラメータを調整することで、様々な生態系の状況を観察できるようになっています。
ArrayList<Water> water;
ArrayList<Tree> trees;
ArrayList<Grass> grass;
ArrayList<Herbivore> herbivores;
ArrayList<Carnivore> carnivores;
// Parameters
int waterCount = 5;
int treeCount = 10;
int grassCount = 50;
int herbivoreCount = 20;
int carnivoreCount = 5;
float grassGrowthRate = 0.02;
float treeGrowthRate = 0.005;
float herbivoreSpeed = 2;
float carnivoreSpeed = 4;
// Season
int season = 0; // 0: Spring, 1: Summer, 2: Autumn, 3: Winter
int seasonDay = 0;
int seasonLength = 100;
// Population history
ArrayList<Integer> treeHistory = new ArrayList<Integer>();
ArrayList<Integer> grassHistory = new ArrayList<Integer>();
ArrayList<Integer> herbivoreHistory = new ArrayList<Integer>();
ArrayList<Integer> carnivoreHistory = new ArrayList<Integer>();
void setup() {
size(1200, 800);
resetSimulation();
}
void draw() {
background(0);
// Update season
seasonDay++;
if (seasonDay >= seasonLength) {
season = (season + 1) % 4;
seasonDay = 0;
}
// Apply seasonal effects
applySeasonalEffects();
// Update and display all elements
updateAndDisplayElements();
// Draw environment border
noFill();
stroke(255);
rect(width * 0.2, height * 0.2, width * 0.6, height * 0.6);
// Display current season and population counts
displayInfo();
// Update and display population history graph
updatePopulationHistory();
displayPopulationGraph();
}
void resetSimulation() {
water = new ArrayList<Water>();
trees = new ArrayList<Tree>();
grass = new ArrayList<Grass>();
herbivores = new ArrayList<Herbivore>();
carnivores = new ArrayList<Carnivore>();
for (int i = 0; i < waterCount; i++) {
water.add(new Water(random(width * 0.6) + width * 0.2, random(height * 0.6) + height * 0.2));
}
for (int i = 0; i < treeCount; i++) {
trees.add(new Tree(random(width * 0.6) + width * 0.2, random(height * 0.6) + height * 0.2));
}
for (int i = 0; i < grassCount; i++) {
grass.add(new Grass(random(width * 0.6) + width * 0.2, random(height * 0.6) + height * 0.2));
}
for (int i = 0; i < herbivoreCount; i++) {
herbivores.add(new Herbivore(random(width * 0.6) + width * 0.2, random(height * 0.6) + height * 0.2));
}
for (int i = 0; i < carnivoreCount; i++) {
carnivores.add(new Carnivore(random(width * 0.6) + width * 0.2, random(height * 0.6) + height * 0.2));
}
}
void applySeasonalEffects() {
switch(season) {
case 0: // Spring
grassGrowthRate = 0.03;
treeGrowthRate = 0.006;
break;
case 1: // Summer
grassGrowthRate = 0.04;
treeGrowthRate = 0.007;
herbivoreSpeed = 2.5;
carnivoreSpeed = 3.5;
break;
case 2: // Autumn
grassGrowthRate = 0.02;
treeGrowthRate = 0.004;
break;
case 3: // Winter
grassGrowthRate = 0.01;
treeGrowthRate = 0.002;
herbivoreSpeed = 1.5;
carnivoreSpeed = 2.5;
break;
}
}
void updateAndDisplayElements() {
for (Water w : water) {
w.affect();
w.display();
}
for (int i = trees.size() - 1; i >= 0; i--) {
Tree t = trees.get(i);
t.grow();
t.generateGrass();
t.display();
if (t.isDead()) {
trees.remove(i);
}
}
for (int i = grass.size() - 1; i >= 0; i--) {
Grass g = grass.get(i);
g.grow();
g.display();
if (g.isDead()) {
grass.remove(i);
}
}
for (int i = herbivores.size() - 1; i >= 0; i--) {
Herbivore h = herbivores.get(i);
h.move();
h.eat();
h.reproduce();
h.display();
if (h.isDead()) {
herbivores.remove(i);
}
}
for (int i = carnivores.size() - 1; i >= 0; i--) {
Carnivore c = carnivores.get(i);
c.move();
c.hunt();
c.reproduce();
c.display();
if (c.isDead()) {
carnivores.remove(i);
}
}
}
void updatePopulationHistory() {
treeHistory.add(trees.size());
grassHistory.add(grass.size());
herbivoreHistory.add(herbivores.size());
carnivoreHistory.add(carnivores.size());
// Keep only the last 200 data points
if (treeHistory.size() > 200) {
treeHistory.remove(0);
grassHistory.remove(0);
herbivoreHistory.remove(0);
carnivoreHistory.remove(0);
}
}
void displayInfo() {
// 半透明の背景を描画
fill(0, 0, 0, 150); // 黒色で透明度150(0-255の範囲)
noStroke();
rect(width * 0.8 - 10, 10, width * 0.2 - 10, 160);
fill(255, 255, 255, 200); // 白色のテキストで透明度200
textSize(16);
String[] seasons = {"Spring", "Summer", "Autumn", "Winter"};
text("Season: " + seasons[season], width * 0.8, 30);
text("Trees: " + trees.size(), width * 0.8, 60);
text("Grass: " + grass.size(), width * 0.8, 90);
text("Herbivores: " + herbivores.size(), width * 0.8, 120);
text("Carnivores: " + carnivores.size(), width * 0.8, 150);
}
void displayPopulationGraph() {
float graphWidth = width * 0.6;
float graphHeight = height * 0.2;
float graphX = width * 0.2;
float graphY = height * 0.85;
// 半透明のグラフ背景を描画
fill(50, 50, 50, 150); // 暗灰色で透明度150
noStroke();
rect(graphX, graphY - graphHeight, graphWidth, graphHeight);
// グラフの枠線を描画
stroke(255, 255, 255, 200); // 白色で透明度200
noFill();
rect(graphX, graphY - graphHeight, graphWidth, graphHeight);
// Calculate max population
int maxPopulation = 0;
for (int i = 0; i < treeHistory.size(); i++) {
maxPopulation = max(maxPopulation, treeHistory.get(i));
maxPopulation = max(maxPopulation, grassHistory.get(i));
maxPopulation = max(maxPopulation, herbivoreHistory.get(i));
maxPopulation = max(maxPopulation, carnivoreHistory.get(i));
}
// Draw population lines
drawPopulationLine(treeHistory, color(0, 128, 0, 200), graphX, graphY, graphWidth, graphHeight, maxPopulation);
drawPopulationLine(grassHistory, color(0, 255, 0, 200), graphX, graphY, graphWidth, graphHeight, maxPopulation);
drawPopulationLine(herbivoreHistory, color(255, 255, 0, 200), graphX, graphY, graphWidth, graphHeight, maxPopulation);
drawPopulationLine(carnivoreHistory, color(255, 0, 0, 200), graphX, graphY, graphWidth, graphHeight, maxPopulation);
}
void drawPopulationLine(ArrayList<Integer> history, color c, float graphX, float graphY, float graphWidth, float graphHeight, int maxPopulation) {
stroke(c);
noFill();
beginShape();
for (int i = 0; i < history.size(); i++) {
float x = map(i, 0, history.size() - 1, graphX, graphX + graphWidth);
float y = map(history.get(i), 0, maxPopulation, graphY, graphY - graphHeight);
vertex(x, y);
}
endShape();
}
// マウスクリックでシミュレーションをリセットする機能を追加
void mousePressed() {
resetSimulation();
}
class Water {
float x, y;
float radius = 30;
Water(float x, float y) {
this.x = x;
this.y = y;
}
void affect() {
for (Grass g : grass) {
if (dist(x, y, g.x, g.y) < radius * 2) {
g.growthRate *= 3;
} else if (dist(x, y, g.x, g.y) < radius * 5) {
g.growthRate *= 1.3;
}
}
}
void display() {
fill(0, 0, 255);
ellipse(x, y, radius * 2, radius * 2);
}
}
class Tree {
float x, y;
float size = 10;
float maxSize = 40;
float energy = 50;
float maxEnergy = 100;
float reproduceEnergy = 80;
float grassGenerationThreshold = 70; // この閾値を超えるとgrassを生成
float viewRadius = 10; // 木の「視野」範囲
Tree(float x, float y) {
this.x = x;
this.y = y;
}
void grow() {
if (energy > 0 && size < maxSize) {
size += treeGrowthRate;
energy += treeGrowthRate * 2;
} else if (size >= maxSize && energy > reproduceEnergy) {
reproduceTree();
generateGrass();
}
energy = constrain(energy, 0, maxEnergy);
size = constrain(size, 10, maxSize);
}
void reproduceTree() {
if (random(1) < 0.05 && trees.size() < 100) {
float reproductionChance = 0.05;
if (countNearbyTrees() == 0) reproductionChance *= 2; // 近くに木がない場合、繁殖確率を2倍に
if (random(1) < reproductionChance) {
float newX = x + random(-100, 100);
float newY = y + random(-100, 100);
if (newX > width * 0.2 && newX < width * 0.8 && newY > height * 0.2 && newY < height * 0.8) {
trees.add(new Tree(newX, newY));
energy -= 30;
}
}
}
}
void generateGrass() {
int grassesToGenerate = int(random(1, 4)); // 1~3の草を生成
for (int i = 0; i < grassesToGenerate; i++) {
if (grass.size() < 200) {
float gx = x + random(-100, 100);
float gy = y + random(-100, 100);
if (gx > width * 0.2 && gx < width * 0.8 && gy > height * 0.2 && gy < height * 0.8) {
grass.add(new Grass(gx, gy));
energy -= 5; // 草1つ生成するごとに10エネルギーを消費
}
}
}
}
int countNearbyTrees() {
int count = 0;
for (Tree t : trees) {
if (t != this && dist(x, y, t.x, t.y) < viewRadius) {
count++;
}
}
return count;
}
boolean isDead() {
if (trees.size() <= 1) return false;
int nearbyTrees = countNearbyTrees();
return nearbyTrees > 10;
}
void display() {
fill(0, 128, 0);
ellipse(x, y, size, size);
}
}
class Grass {
float x, y;
float size = 5;
float maxSize = 15;
float growthRate = grassGrowthRate;
float energy = 20;
float maxEnergy = 40;
boolean isEaten = false;
float viewRadius = 10; // 草の「視野」範囲
Grass(float x, float y) {
this.x = x;
this.y = y;
}
void grow() {
if (energy > 0 && size < maxSize) {
size += growthRate;
energy += growthRate * 2;
} else if (size >= maxSize && energy > 30) {
reproduceGrass();
}
energy = constrain(energy, 0, maxEnergy);
size = constrain(size, 5, maxSize);
}
void reproduceGrass() {
float reproductionChance = 0.05;
if (countNearbyGrass() == 0) reproductionChance *= 2; // 近くに草がない場合、繁殖確率を2倍に
if (random(1) < reproductionChance && grass.size() < 1000) {
float newX = x + random(-30, 30);
float newY = y + random(-30, 30);
if (newX > width * 0.2 && newX < width * 0.8 && newY > height * 0.2 && newY < height * 0.8) {
grass.add(new Grass(newX, newY));
energy -= 20;
}
}
}
int countNearbyGrass() {
int count = 0;
for (Grass g : grass) {
if (g != this && dist(x, y, g.x, g.y) < viewRadius) {
count++;
}
}
return count;
}
boolean isDead() {
return grass.size() > 10 && (isEaten || energy <= 0);
}
void display() {
fill(0, 255, 0);
ellipse(x, y, size, size);
}
}
class Herbivore {
float x, y;
float size = 10;
float maxSize = 20;
float energy = 100;
float maxEnergy = 200;
float reproduceEnergy = 10;
float speed = herbivoreSpeed;
float viewRadius = 100;
PVector direction;
int movementDuration = 0;
int maxMovementDuration = 20;
int eatingPause = 10;
int maxEatingPause = 3; // 食事後の停止フレーム数
Herbivore(float x, float y) {
this.x = x;
this.y = y;
newDirection();
}
void newDirection() {
if (energy < maxEnergy * 0.5) {
Grass nearestGrass = findNearestGrass();
if (nearestGrass != null) {
direction = PVector.sub(new PVector(nearestGrass.x, nearestGrass.y), new PVector(x, y)).normalize();
} else {
direction = PVector.random2D();
}
} else {
direction = PVector.random2D();
}
Carnivore nearestCarnivore = findNearestCarnivore();
if (nearestCarnivore != null) {
PVector escapeDirection = PVector.sub(new PVector(x, y), new PVector(nearestCarnivore.x, nearestCarnivore.y)).normalize();
direction.add(escapeDirection.mult(1.5));
}
direction.normalize().mult(speed);
movementDuration = 0;
}
void move() {
if (eatingPause > 0) {
eatingPause--;
return; // 食事中は移動せずエネルギー消費もなし
}
if (movementDuration >= maxMovementDuration || random(1) < 0.02) {
newDirection();
}
x += direction.x;
y += direction.y;
x = constrain(x, width * 0.2, width * 0.8);
y = constrain(y, height * 0.2, height * 0.8);
energy -= 0.5;
movementDuration++;
}
Grass findNearestGrass() {
Grass nearest = null;
float minDist = viewRadius;
for (Grass g : grass) {
float d = dist(x, y, g.x, g.y);
if (d < minDist) {
minDist = d;
nearest = g;
}
}
return nearest;
}
Carnivore findNearestCarnivore() {
Carnivore nearest = null;
float minDist = viewRadius;
for (Carnivore c : carnivores) {
float d = dist(x, y, c.x, c.y);
if (d < minDist) {
minDist = d;
nearest = c;
}
}
return nearest;
}
void eat() {
if (eatingPause > 0) return; // 既に食事中なら何もしない
for (int i = grass.size() - 1; i >= 0; i--) {
Grass g = grass.get(i);
if (dist(x, y, g.x, g.y) < 10) {
grass.remove(i);
energy += 20;
if (size < maxSize) size += 0.1;
eatingPause = maxEatingPause; // 食事後の停止を開始
break;
}
}
energy = constrain(energy, 0, maxEnergy);
}
void reproduce() {
if (energy > reproduceEnergy*2 && random(1) < 0.05 && herbivores.size() < 200) {
herbivores.add(new Herbivore(x, y));
energy = reproduceEnergy;
eatingPause = maxEatingPause*10;
}
}
boolean isDead() {
return herbivores.size() > 1 && energy <= 0;
}
void display() {
fill(255, 255, 0);
ellipse(x, y, size, size);
if (eatingPause > 0) {
// 食事中であることを示す表示(例: 小さな緑の円)
fill(0, 255, 0);
ellipse(x, y, size / 2, size / 2);
}
}
}
class Carnivore {
float x, y;
float size = 15;
float maxSize = 30;
float energy = 150;
float maxEnergy = 300;
float reproduceEnergy = 100;
float speed = carnivoreSpeed;
float viewRadius = 300;
PVector direction;
int movementDuration = 0;
int maxMovementDuration = 60;
int eatingPause = 10;
int maxEatingPause = 45; // 食事後の停止フレーム数(草食動物より長め)
Carnivore(float x, float y) {
this.x = x;
this.y = y;
newDirection();
}
void newDirection() {
if (energy < maxEnergy * 0.5) {
Herbivore nearestHerbivore = findNearestHerbivore();
if (nearestHerbivore != null) {
direction = PVector.sub(new PVector(nearestHerbivore.x, nearestHerbivore.y), new PVector(x, y)).normalize();
} else {
direction = PVector.random2D();
}
} else {
direction = PVector.random2D();
}
direction.normalize().mult(speed);
movementDuration = 0;
}
void move() {
if (eatingPause > 0) {
eatingPause--;
return; // 食事中は移動せずエネルギー消費もなし
}
if (movementDuration >= maxMovementDuration || random(1) < 0.02) {
newDirection();
}
float newX = x + direction.x;
float newY = y + direction.y;
// 木との衝突チェック
boolean canMove = true;
for (Tree tree : trees) {
if (dist(newX, newY, tree.x, tree.y) < (size / 2 + tree.size / 2)) {
canMove = false;
break;
}
}
if (canMove) {
x = newX;
y = newY;
} else {
newDirection();
}
x = constrain(x, width * 0.2, width * 0.8);
y = constrain(y, height * 0.2, height * 0.8);
energy -= 1;
movementDuration++;
}
Herbivore findNearestHerbivore() {
Herbivore nearest = null;
float minDist = viewRadius;
for (Herbivore h : herbivores) {
float d = dist(x, y, h.x, h.y);
if (d < minDist) {
// 木を通過せずに到達できるかチェック
boolean canReach = true;
for (Tree tree : trees) {
if (lineIntersectsCircle(x, y, h.x, h.y, tree.x, tree.y, tree.size / 2)) {
canReach = false;
break;
}
}
if (canReach) {
minDist = d;
nearest = h;
}
}
}
return nearest;
}
boolean lineIntersectsCircle(float x1, float y1, float x2, float y2, float cx, float cy, float r) {
float dx = x2 - x1;
float dy = y2 - y1;
float a = dx * dx + dy * dy;
float b = 2 * (dx * (x1 - cx) + dy * (y1 - cy));
float c = cx * cx + cy * cy + x1 * x1 + y1 * y1 - 2 * (cx * x1 + cy * y1) - r * r;
float discriminant = b * b - 4 * a * c;
return discriminant >= 0;
}
void hunt() {
if (eatingPause > 0) return; // 既に食事中なら何もしない
for (int i = herbivores.size() - 1; i >= 0; i--) {
Herbivore h = herbivores.get(i);
if (dist(x, y, h.x, h.y) < 20) {
herbivores.remove(i);
energy += 50;
if (size < maxSize) size += 0.2;
eatingPause = maxEatingPause; // 食事後の停止を開始
break;
}
}
energy = constrain(energy, 0, maxEnergy);
}
void reproduce() {
if (energy > reproduceEnergy && random(1) < 0.03 && carnivores.size()*10 < herbivores.size()) {
carnivores.add(new Carnivore(x, y));
energy -= 75;
eatingPause = 30;
}
}
boolean isDead() {
return carnivores.size() > 1 && energy <= 0;
}
void display() {
fill(255, 0, 0);
ellipse(x, y, size, size);
if (eatingPause > 0) {
// 食事中であることを示す表示(例: 小さな黄色の円)
fill(255, 255, 0);
ellipse(x, y, size / 2, size / 2);
}
}
}
たった1つのファイルのコードだけでできています。
今回は、学習コードが多そうなJava版にしました。
Pythonモードだとエラーも多かったです。
1-1. シミュレーターの特徴
主な特徴は以下の通りです:
- 水、木、草、草食動物(Herbivore)、肉食動物(Carnivore)の5つの要素で構成されています。
- 季節の変化があり、各季節で生物の成長率や動物の速度が変化します。
- 各生物はエネルギーを持ち、成長や行動によってエネルギーを消費・回復します。
- 木は成長し、一定条件下で草を生成します。
- 草食動物は草を食べ、肉食動物は草食動物を捕食します。
- 動物は視野範囲内で餌を探し、捕食者から逃げる行動をとります。
- 肉食動物は木を通り抜けられません。
- 動物は食事後、一時的に移動を停止します。
- 画面右上に各生物の数を表示し、下部にグラフで個体数の推移を表示します。
- マウスクリックでシミュレーションをリセットできます。
1-2. 処理の大まかな流れ
各生物クラス(Water, Tree, Grass, Herbivore, Carnivore)は、それぞれ独自の行動ロジックを持っており、シミュレーション内で相互に影響し合います。
この循環的な処理により、動的な生態系シミュレーションが実現されています。
初期化 (setup関数):
- ウィンドウサイズの設定
- シミュレーションのリセット(各生物の初期配置)
メインループ (draw関数):
- 背景の描画
- 季節の更新と季節効果の適用
- 全ての生物の更新と描画
- 環境の境界線の描画
- 情報表示(現在の季節と各生物の個体数)
- 個体数履歴の更新とグラフ表示
生物の更新 (updateAndDisplayElements関数):
- 水の影響を適用
- 木の成長、草の生成、表示、死亡判定
- 草の成長、表示、死亡判定
- 草食動物の移動、摂食、繁殖、表示、死亡判定
- 肉食動物の移動、狩猟、繁殖、表示、死亡判定
イベント処理:
- マウスクリック時にシミュレーションをリセット
2. Claudeに与えた初期プロンプト
今回は、Claude 3.5 Sonnetで雛形を作りました。
生態系シミュレーターをシミュレーターをProcessingで作成してください。
水は
・動物が通れない
・植物は水の近くほど増えやすい
・近くにゆっくり下草を生産する樹木
・成長はゆっくり
・近くに少しずつ下草を生産する
・草食動物も食べられない
・樹木が近くに多すぎると共倒れする下草
・草食動物に食べられる
・成長は早い
・ある程度成長すると樹木になる草食動物
・移動する
・動くたびに栄養を消費し、不足すると餓死する
・下草を見つけると食べ、栄養を得る
・ある程度、成長すると交尾して、繁殖する肉食動物
・移動する
・動くたびに栄養を消費し、不足すると餓死する
・草食動物を追いかけ、食べる
・ある程度、成長すると交尾して、繁殖する
伝えたイメージとはだいぶ違いますが、なんとなくそれっぽいものが30分ほどの微調整で形になるのが嬉しいです。