Processing 模拟生态系统
博文简介
使用Processing实现了简单的池塘生态系统。
主要是鱼——水蚤——水草三类生物的行为。
关键技术是遗传算法和自治智能体
生物知识
生态系统
生态系统指在自然界的一定的空间内,生物与环境构成的统一整体,在这个统一整体中,生物与环境之间相互影响、相互制约,并在一定时期内处于相对稳定的动态平衡状态。
组成成分:非生物的物质和能量、生产者、消费者、分解者。
这里模拟了简单的池塘生态系统。忽略掉非生物的物质和能量,还有分解者。只有 生产者–水草、低级消费者–水蚤(以水草为食)、高级消费者–鱼(以水蚤为食)
核心规则:
1、·能量传递的过程中逐级递减,传递率为10%~20%
这点问题不大,简单来说就是 鱼:水蚤:水草 =100:10:1,当然实际上没有这么简单,实现的过程中没有严格按照这点,大概就是注意鱼的数量少于水蚤少于水草,并且鱼的寿命大于水蚤大于水草,生命值也不同。
2、生物进化
这里用到了遗传算法,实现这点的方法是寿命,生物活得越久繁殖概率越大,DNA也就会传递下去
3、动态平衡
生态系统通常具有自我调节能力,可以实现动态平衡。
生物进化和动态平衡在实现的过程中都出现了问题,比我一开始想的出入很大,在下文 问题与思考 中详细阐述。
实现内容
DNA:
寿命 (有最大最小限制)
速度 (进化需有一定随机性 、突变性)
大小
交叉
复制
变异
水草:
生命值(被吃会变透明)
寿命
大小(与寿命相关)
速度
死亡
无性繁殖
种子扩散
有微弱的移动能力
水蚤(指低级消费者):
寿命(性成熟)
性别
大小
速度
躲避行为(躲鱼)
寻偶
群集行为(主要是对齐)
吸收养分(吃植物)
有性繁殖
鱼(指高级消费者):
寿命(性成熟)
性别
大小
速度
性别
大小
捕食(追水蚤)
寻偶
群集行为(主要是分离)
吸收养分(吃虾)
有性繁殖
效果展示
完整视频:https://www.bilibili.com/video/av50958125
操作说明
可以通过单击鼠标左键、右键、滑轮添加鱼、水蚤、水草,或单击左上角得“-”号减少
拖拽可以持续生成(后期生物过多,单击生成减少太慢)
通过交互人为调节生态平衡。
相关类
类间关系如图
下面是各类的域和方法,具体实现和全部代码见 https://github.com/sssal/EcoSystem
主类
World world;
Boolean isOpenIntroduce;
void setup() {
size(800, 600);
frameRate(60);
world = new World(100, 20, 0);
textFont(createFont("KaiTi-48.vlw", 48));
isOpenIntroduce = true;
}
void draw() {
background(200);
world.update();
textSize(15);
fill(50);
text("Plants:" + (int)world.getPlantNum(), 20, 20);
text("Fishes:" + (int)world.getFishNum(), 20, 40);
text("Fleas:" + (int)world.getFleaNum(), 20, 60);
rect(100, 7, 20, 12);
rect(100, 27, 20, 12);
rect(100, 47, 20, 12);
fill(250);
rect(105, 12, 10, 3);
rect(105, 32, 10, 3);
rect(105, 52, 10, 3);
//说明界面
if (isOpenIntroduce) {
fill(0, 200);
rect(width/4, height/4, width/2, height/2, 20);
textSize(30);
fill(250);
text("说明", width/2-50, height/4+40);
textSize(20);
text("按 “ i ” 退出/显示说明界面", width/4+20, height/4+70);
text("左上角表示示植物、水蚤、鱼数量", width/4+20, height/4+100);
text("鼠标单击左键生成鱼,右键水蚤,中键植物", width/4+20, height/4+130);
text("鼠标拖拽持续生成",width/4+20,height/4+160);
text("鼠标单击左上角“-”减少",width/4+20,height/4+190);
text("鼠标在左上角“-”处拖拽持续减少",width/4+20,height/4+220);
}
}
void mouseDragged() {
//print(0);
if (mouseX>100&&mouseX<120&&mouseY>7&&mouseY<19) {
world.reducePlant();
} else if (mouseX>100&&mouseX<120&&mouseY>27&&mouseY<39) {
world.reduceFish();
} else if (mouseX>100&&mouseX<120&&mouseY>47&&mouseY<59) {
world.reduceFlea();
} else {
if (mouseButton == LEFT) {
print(1);
world.addFish(new PVector(mouseX, mouseY));
} else if (mouseButton == RIGHT) {
world.addFlea(new PVector(mouseX, mouseY));
} else if (mouseButton == CENTER) {
world.addPlant(new PVector(mouseX, mouseY));
}
}
}
void mouseClicked() {
//print(0);
if (mouseX>100&&mouseX<120&&mouseY>7&&mouseY<19) {
world.reducePlant();
} else if (mouseX>100&&mouseX<120&&mouseY>27&&mouseY<39) {
world.reduceFish();
} else if (mouseX>100&&mouseX<120&&mouseY>47&&mouseY<59) {
world.reduceFlea();
} else {
if (mouseButton == LEFT) {
print(1);
world.addFish(new PVector(mouseX, mouseY));
} else if (mouseButton == RIGHT) {
world.addFlea(new PVector(mouseX, mouseY));
} else if (mouseButton == CENTER) {
world.addPlant(new PVector(mouseX, mouseY));
}
}
}
void keyPressed() {
if (key == 'i') {
if (isOpenIntroduce) {
isOpenIntroduce = false;
} else {
isOpenIntroduce = true;
}
}
}
class World {
ArrayList<Flea> fleas;
ArrayList<Plant> plants;
ArrayList<Fish> fishes;
World(int plantsNum, int fleasNum, int fishNum)
void update()
float getFishNum()
float getFleaNum()
float getPlantNum()
void addFish(PVector pvector)
void addFlea(PVector pvector)
void addPlant(PVector pvector)
void reduceFish()
void reducePlant()
void reduceFlea()
}
class Creature {
//生物类 所有生物的父类
PVector position; //位置
PVector acceleration; //加速度
PVector velocity; //速度
float lifetime; //寿命
float maxspeed; //速度
float maxforce; //转向力
float size; //大小
float r; //画图大小
float maxLifetime; //用来保存生物的最大生命和尺寸
float maxSize;
float health; //生命值
float maxHealth;
DNA dna;
DNA fatherDNA;
//设置不同行为的权重
float separateWeight;
float cohesionWeight;
float alignWeight;
float breedProbability; //繁殖概率
float matingProbability; //交配概率
float xoff;
float yoff; //控制随机移动速度
float periphery = PI/2; //视野角度
Boolean gender; //性别
Boolean isRut; //是否处于发情期
Boolean isPregnancy; //怀孕
color col; //颜色
Creature(PVector pos, DNA initDNA)
//更新
void update()
//移动
void move()
//画图
void display()
//添加力改变加速度
void applyForce(PVector force)
//三种群集规则
//分离 避免碰撞
//对齐 转向力与邻居一致
//聚集 朝邻居中心转向 (留在群体内)
void flock(ArrayList<? extends Creature> Creatures)
//寻找
PVector seek(PVector target)
// Cohesion 聚集行为
PVector cohesion (ArrayList<? extends Creature> creatures)
//分离行为
PVector separate (ArrayList<? extends Creature> creatures)
//对齐行为
PVector align (ArrayList<? extends Creature> creatures)
//设置发情期
void rut()
//寻偶
PVector mating(ArrayList<? extends Creature> creatures)
//觅食
PVector foraging(ArrayList<? extends Creature> creatures)
//繁殖
public Creature breed()
// 避免超出画板范围
void borders()
//判断死亡
boolean dead()
}
class Plant extends Creature {
//水草类
Plant(PVector pos, DNA initDNA)
@Override
void display()
@Override
Plant breed()
//移动
@Override
void move()
//判断死亡
@Override
boolean dead()
class Flea extends Creature {
//水蚤类
Flea(PVector pos, DNA initDNA)
//更新
@Override
void update()
@Override
void display()
@Override //加入寻偶行为
void flock(ArrayList<? extends Creature> Creatures)
//躲避动作
void moveElude(ArrayList<Fish> fishes)
//躲避
PVector elude(ArrayList<Fish> fishes)
@Override //有性繁殖
Flea breed()
//移动
@Override
void move()
//对齐行为
@Override //添加视野角度
PVector align (ArrayList<? extends Creature> creatures)
//吃水草
void eat(ArrayList<Plant> plants)
//判断死亡
@Override
boolean dead()
class Fish extends Creature {
//鱼类
Fish(PVector pos, DNA initDNA)
//更新
@Override
void update()
@Override
void display()
@Override //有性繁殖
Fish breed()
@Override //加入寻偶行为
void flock(ArrayList<? extends Creature> Creatures)
//捕食运动
void moveForaging(ArrayList<Flea> fleas)
//移动
@Override
void move()
//吃水蚤
void eat(ArrayList<Flea> fleas)
//判断死亡
@Override
boolean dead()
class DNA {
private HashMap<String, Float> genes=new HashMap<String,Float>();
DNA()
DNA(HashMap newgenes)
//复制
public DNA dnaCopy()
//交叉
public DNA dnaCross(DNA fatherDNA)
//变异
public void mutate(float m)
技术讲解
向量、力之类的比较基础,这里主要说一下遗传和自治智能体。
遗传
这部分的代码比较简单。只需要把遗传相关的属性和方法实现就可以了。
在DNA类中,使用 HashMap<String, Float> 用作储存基因型,键(String)保存基因型的名字,值(FLoat)保存基因间的差别,在Creature类中实例化DNA类并通过值来实现表现型。
之后事基因的复制、交叉和变异。
复制主要用在无性繁殖,直接复制DNA就可以了
//复制
public DNA dnaCopy() {
HashMap<String, Float> childGenes = (HashMap<String,Float>)genes.clone();
return new DNA(childGenes);
}
交叉则是有性繁殖,父亲把DNA传递给母亲,然后生成孩子的DNA
//交叉
public DNA dnaCross(DNA fatherDNA){
HashMap<String, Float> childGenes = (HashMap<String,Float>)genes.clone();
HashMap<String, Float> fatherGenes = fatherDNA.genes;
float lifetime = (fatherGenes.get("lifetime") + genes.get("lifetime"))/2;
childGenes.put("lifetime",lifetime);
float speed = (fatherGenes.get("speed") + genes.get("speed"))/2;
childGenes.put("speed",speed);
float size = (fatherGenes.get("size") + genes.get("size"))/2;
childGenes.put("size",size);
return new DNA(childGenes);
}
变异在有性繁殖和无性繁殖中都会出现
//变异
public void mutate(float m) {
for (String key : genes.keySet()) {
if (random(1)<m) {
genes.put(key, random(0, 1));
}
}
}
自治智能体
这部分是代码的核心,控制生物的行为。
生物的行为可以分成两类,一类是群集行为,一类是个体行为。
运动会受到不同权重,生物的视野和角度影响。
群集行为
群集行为包括聚集,分离,对齐。
水蚤的对齐行为权重更大,会与视野内的水蚤速度保持一致。
统计视野内其他水蚤的速度均值,处理后作为加速度。
//对齐行为
@Override //添加视野角度
PVector align (ArrayList<? extends Creature> creatures) {
float neighbordist = size * 2;
PVector sum = new PVector(0, 0);
int count = 0;
for (Creature other : creatures) {
//从一个生物到另一个生物的向量
PVector comparison = PVector.sub(other.position, position);
//距离
float d = PVector.dist(position, other.position);
//角度
float diff = PVector.angleBetween(comparison, velocity);
if ((diff < periphery) && (d > 0) && (d < neighbordist)) {
sum.add(other.velocity);
count++;
}
}
if (count > 0) {
sum.div((float)count);
sum.normalize();
sum.mult(maxspeed);
PVector steer = PVector.sub(sum, velocity);
steer.limit(maxforce);
return steer;
} else {
return new PVector(0, 0);
}
}
聚集则是计算视野内其他生物的位置的均值,生成从当前位置指向均值的向量处理后用作加速度
// Cohesion 聚集行为
PVector cohesion (ArrayList<? extends Creature> creatures) {
float neighbordist = r * 5; //视野 ????
PVector sum = new PVector(0, 0); // Start with empty vector to accumulate all positions
int count = 0;
for (Creature other : creatures) {
float d = PVector.dist(position, other.position);
if ((d > 0) && (d < neighbordist)) {
sum.add(other.position); // 将其他对象位置相加
count++;
}
}
if (count > 0) {
sum.div(count);
return seek(sum); //寻找邻居的平均坐标
} else {
return new PVector(0, 0);
}
}
分离统计远离其他生物的向量均值,可以用于避免生物间距离过近重叠。
//分离行为
PVector separate (ArrayList<? extends Creature> creatures) {
float desiredseparation = r*1.5; //分离视野
PVector steer = new PVector(0, 0, 0);
int count = 0;
// For every boid in the system, check if it's too close
for (Creature other : creatures) {
float d = PVector.dist(position, other.position);
// If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
if ((d > 0) && (d < desiredseparation)) {
// Calculate vector pointing away from neighbor
PVector diff = PVector.sub(position, other.position);
diff.normalize();
diff.div(d); // Weight by distance
steer.add(diff);
count++; // Keep track of how many
}
}
// Average -- divide by how many
if (count > 0) {
steer.div((float)count);
}
// As long as the vector is greater than 0
if (steer.mag() > 0) {
// Implement Reynolds: Steering = Desired - Velocity
steer.normalize();
steer.mult(maxspeed);
steer.sub(velocity);
steer.limit(maxforce);
}
return steer;
}
个体行为
个体的行为是求偶、觅食、躲避
当个体处于发情期会寻找视野内处于发情期的异性,找到后传递DNA,并结束发情期状态
//寻偶
PVector mating(ArrayList<? extends Creature> creatures) {
float neighbordist = r * 15;
if (isRut) {
for (Creature other : creatures) {
if (other.isRut && gender != other.gender) {
//都处于发情期且性别不同
//PVector comparison = PVector.sub(other.position, position);
//距离
float d = PVector.dist(position, other.position);
//角度
//float diff = PVector.angleBetween(comparison, velocity);
if ( (d < neighbordist) && (d>r)) {
return seek(other.position);
} else if (d<r) { //当两者足够靠近
//print(3);
isRut = false;
other.isRut = false;
if (gender) {
col = color(255, 0, 0);
other.col = color(0, 255, 0);
isPregnancy = true;
fatherDNA = other.dna;
} else {
col = color(0, 255, 0);
other.col = color(255, 0, 0);
other.isPregnancy = true;
other.fatherDNA = dna;
}
}
}
}
}
return new PVector(0, 0);
}
觅食是鱼的行为,主动寻找水蚤
//觅食
PVector foraging(ArrayList<? extends Creature> creatures) {
float neighbordist = r * 10;
for (Creature c:creatures) {
//从一个生物到另一个生物的向量
PVector comparison = PVector.sub(c.position, position);
//距离
float d = PVector.dist(position, c.position);
//角度
float diff = PVector.angleBetween(comparison, velocity);
if ((diff < periphery) && (d < neighbordist)) {
return seek(c.position);
}
}
return new PVector(0, 0);
}
躲避与觅食相反,水蚤主动躲避鱼
//躲避
PVector elude(ArrayList<Fish> fishes) {
float neighbordist = r * 10;
for (Fish f:fishes) {
//从一个生物到另一个生物的向量
PVector comparison = PVector.sub(f.position, position);
//距离
float d = PVector.dist(position, f.position);
//角度
float diff = PVector.angleBetween(comparison, velocity);
if ((diff < periphery) && (d < neighbordist)) {
PVector result = seek(f.position);
result = new PVector(-result.x, -result.y);
return result;
}
}
return new PVector(0, 0);
}
问题与思考
1、遗传
遗传算法是计算数学中用于解决最佳化的搜索算法,曾经使用过用来计算函数的最大值,这里考虑把生物的寿命作为适应度,活得越长适应度越大,这样迭代一段时间后就能够得到寿命最长也就是最适应环境的个体,但在尝试的过程中发现这样太理想化了。
一开始想要把各项参数都放在DNA中,例如不同行为的权重,繁殖率等,但是发现变量太多,效果不好。
在系统中只有水蚤和水草时,出现过一种情况是水蚤越来越大,但速度却越来越慢,我猜测因为没有给水蚤添加觅食行为,所以走得慢反而更容易生存,越大觅食范围越大,并且水草不会被直接吃掉,生存概率也更高。但是这种情况我后来并没有复现出来。
这里放了一种能够体现遗传进化的情况:
初始为水草:100 水蚤:20 无鱼
一段时间后的情况:
发现水蚤在往大的方向进化,速度并没有越来越慢。
这时候我意识到 适应度 的问题,我希望的适应度是寿命,但实际上想要让水蚤因为各项属性影响寿命是很困难的事,我只把参数修改一点点就可能导致生物灭绝或者过度繁殖,所以寿命的随机性其实挺高的。而真正的适应度其实是交配的概率,吃得多活得长,交配的概率高,但影响交配的概率最大的其实是体积,越大越可能和异性接触并交配。
2、动态平衡
正常的生态系统是具有自我调节的能力的,就像狼-兔-草模型中,兔子多了,狼也会多,草减少,这样兔子就会减少,一开始我也是想让鱼-水蚤-水草达到平衡状态,在只有水蚤-水草时还行,加入鱼后我发现这是不可能的,不添加外在条件时不可能的。
自然界中其实还存在一些规律,实际上当一种生物增多时,首先受影响的是它的下级,生态系统的调节是具有滞后性的,就像兔子多了,首先是草大量减少,然后才是狼多,需要一定的时间跨度,才能恢复平衡。但在我模拟的生态系统中,当存在鱼-水蚤-水草时几乎无法调节平衡,会持续地往极端发展,并最终全部灭绝。
当然这应该有我对生物的模拟不够真实的原因,并且环境太小,没有足够的时间和空间让模拟生态系统运行下去。
参考
参考案例:https://www.openprocessing.org/sketch/687983
参考书籍:《The nature of code》/《代码本色》
其他作品推荐
布料模拟
布料模拟
这个作品一下子就吸引了我,很简洁又真实,确实有布料运动的感觉,这个技术的实现让我想到曾经制作的愤怒的小鸟的弹弓。
布料的整体运动取决于部分的运动,每一个局部运动逻辑是相似的,但整体表现出来的效果又很棒。
融入动画技术的交互应用
博文地址
一开始实现的跳一跳看起来就很不错,运动、交互的效果看起来很舒服。后边还看到一些有趣的效果,processing实现的融入动画的漫画场景,很好看。最后还分享了一些动画,特别是这个水生物的自然形态模拟,很喜欢,我自己做的生态系统看上去特别丑,早点看到这个例子,或许能让我的作品更具艺术性。
《Magic Network》:一个小孩都能玩的神经网络交互系统
Magic Netwo
界面画风很符合主题,在学神经网络、人工智能的时候就经常感觉很抽象,很多东西的中间过程难以理解,这个作品非常好,即便是没有学过相关知识的人也可以操作,帮助理解神经网络。