Evolution Aquerium
Idea
The basic idea of the project is to achieve and simulate biological creatures in an aquarium to see how they react in different scenarios
Core Technologies
- Javascript
- HTML5 Canvas
How It Works?
These Creatures are based onCraig Reynold's Steering Behaviors andFlocking System
It also implements Genetic Algorithm and mutations.
You can learn more about them on Daniel Shiffman's YouTube ChannelThe Coding Train
- Coding Challenge #69.1: Evolutionary Steering Behaviors
- Coding Challenge #124: Flocking Simulation
- Genetic Algorithm playlist The Nature of Code

AgentBuilder Class
I make use of the "builder" pattern to create different varieties of creatures with different traits. And AgentBuilder class helps me do this easily see how it works.
classAgentBuilder{constructor(type){this.acc=newVector(0,0)this.vel=newVector(0,-2)this.type= type}setPos(x, y){this.pos=newVector(x, y)returnthis}setRadius(r=5){this.radius= rreturnthis}setColor(color){return(this.color= color)}setDNA(dna){this.dna= dnareturnthis}// ... more setFunctionsbuild(){// returns a new BaseAgent based on the valuesreturnnewBaseAgent(this.pos.x,this.pos.y,this.radius,this.dna,this.color,this)}}
And now, the builder pattern is ready to be used. Let us see how we can build creatures with different traits.
// later onlet Predator=newAgentBuilder("PREDATOR").setRadius(10).setMaxSpeed(2).setMaxForce(0.05).setHealthDecrease(0.002).setColor([255,0,0]).setFoodMultiplier([0.5,-0.5])let Avoider=newAgentBuilder("AVOIDER").setRadius(5).setMaxRadius(8).setMaxSpeed(4).setMaxForce(0.2).setHealthDecrease(0.003).setColor([255,165,0]).setFoodMultiplier([0.5,-0.5])
BaseAgent Class
BaseAgent class handles all the logic for the update, render, and physics, it's the heart of the codebase. the basic idea is to give eachagent
some essential traits likehealth
,radius
,maxSpeed
,maxSpeed
etc etc.
see the full BaseAgent class'scode at github
classBaseAgent{constructor(x, y, radius, dna, color, builder){// for flocking behaviourthis.pos=newVector(x, y);this.acc=newVector(0,0);this.vel=newVector(0,-2);// i used builder pattern to create more variaty agents easilyif(builder===undefined) builder={};this.builder= builder;// traitsthis.age=1;this.health=1;this.radius= radius||5;this.maxSpeed= builder.maxSpeed||1.5;this.maxForce= builder.maxForce||0.05;// amout of health will decrease over timethis.healthDecrease= builder.healthDecrease||0.003;// amount of health will increase after eating foodthis.goodFoodMultiplier= builder.goodFoodMultiplier||0.5;// amount of health will decrease after eating poisonthis.badFoodMultiplier= builder.badFoodMultiplier||-0.4;this.color= color;this.hasReproduced=0;// randomly choose male or femalethis.sex=(random(1)<0.5)?'MALE':'FEMALE';// females will be a bit bigger than malethis.maxRadius= builder.maxRadius||((this.getGender()==='FEMALE')?15:10);// more code in between...// colors based on their genderif(!this.color&&this.getGender()==='MALE')this.color=[0,170,0];if(!this.color&&this.getGender()==='FEMALE')this.color=[255,39,201];}
Later on, I also gave them names, haha yes
...let names_female=['hanna','mona','cutie','sweety','sofia','rose','laisy','daisy','mia'];let names_male=['joe','jim','kim','keo','shaun','morgan','jery','tom','anu','brian','ninja','daniel'];// names based on genderthis.name=(this.getGender()==='MALE')?this.getRandomName(names_male):this.getRandomName(names_female);...
And now, let's talk only about the main meat of the code, not any other unnecessary code.so in the base class we have some methods
applies the flocking behavior
- defineFear()
The primary function which defines the fear behavior and we can also use this inversely
seeks the nearby food
applies the force which returns from eat()
Reproduction System checks for male and female agents, and if their radius is greater than 8 and they are close enough to each other, then they can reproduce with their specific DNA and creates a small Agent based on their DNA data and with some mutation.
Flock Class
Flock class takes an agent and do the calculations for flocking behaviors likeseparate
,align
,cohesion
,seek
.
classFlock{constructor(currentAgent){this.currentAgent= currentAgentthis.wandertheta=0}seek(target){}_returnSteer(sum){}wander(){}separate(agents){}align(agents){}cohesion(agents){}}
EcoSystem Class
EcoSystem class manages all theagents
andbehaviors
, basically it is like a state manager
it has some methods which are
adds all entities to the entities (food, poison) object
registers Agents to the state and also creates corresponding Arrays for each of them which you can use by calling ecoSys.groups[yourgivenname]
initializes the groups of population by the given amount
specifies the behavior of the agent declaratively
updates all the agents
classEcoSystem{constructor(){this.groups={}// agentsthis.entities={}// generic container (food, poison)this.agents={}// agent classesthis.behaviors={}// calculated behaviors}addEntities(names){}registerAgents(agents){}initialPopulation(init){}addBehavior(config){}batchUpdateAgents(list, foodPoison, weight, callback){}}
Setting up everything
And the last step is to assemble every part of the code to create these boids like creatures and gave each of them behaviors
// Globallet canvas= document.querySelector('#c');letWIDTH= canvas.width= window.innerWidth;letHEIGHT= canvas.height=600;let ctx= canvas.getContext('2d');letMAX_CREATURES=300;constREPRODUCTION_RATE=0.5;constENABLE_SUPER_DEBUG=false;// constants for flexibiltyconstCREATURE='CREATURE';constPREDATOR='PREDATOR';constAVOIDER='AVOIDER';constEATER='EATER';constFOOD='FOOD';constPOISON='POISON';functionload(){const ecoSys=newEcoSystem();// creates a Array which you can access with ecoSys.entities ecoSys.addEntities({FOOD:[],POISON:[]});// register classes it will also create corresponding Arrays// which you can use by calling ecoSys.groups[your_given_name] ecoSys.registerAgents({CREATURE: Agent,PREDATOR: Predator,AVOIDER: Avoider,EATER: Eater,});// initialPopulation have to use the same name// which you configure in registerAgents ecoSys.initialPopulation({CREATURE:150,PREDATOR:randomInt(5,10),AVOIDER:randomInt(10,20),EATER:randomInt(1,4),});let add= document.getElementById('addnew'); canvas.addEventListener('click',function(e){ ecoSys.add(add.value, e.offsetX, e.offsetY)})// ANIMATE LOOPfunctionanimate(){let grd= ctx.createRadialGradient(WIDTH/2,HEIGHT/2,0,WIDTH/2,HEIGHT/2,WIDTH); grd.addColorStop(0,"rgba(25,25,25,1)"); grd.addColorStop(1,"rgba(0,0,25,1)");// Fill with gradient ctx.fillStyle= grd; ctx.fillRect(0,0,WIDTH,HEIGHT);/** * likes food dislikes poison * run away form predators and eaters * cloneItSelf */ ecoSys.addBehavior({ name:CREATURE, like:FOOD, dislike:POISON, fear:{PREDATOR:[-4,75],EATER:[-2,100]}, cloneItSelf:0.0015,callback:function(){if(ecoSys.groups.CREATURE.length<MAX_CREATURES&&random(1)<REPRODUCTION_RATE){this.reproduce(ecoSys.groups.CREATURE);}}});/** * likes poison dislikes food * seeks and eats creatures * run away from eaters */ ecoSys.addBehavior({ name:PREDATOR, like:POISON, dislike:FOOD, likeDislikeWeight:[1,-1], fear:{EATER:[-10,50],CREATURE:[1,200,function(agents, i){ agents.splice(i,1);this.health+=this.goodFoodMultiplier;this.radius+=this.goodFoodMultiplier;}]},});/** * likes food dislikes poison * run away form predators, eaters, creatures */ ecoSys.addBehavior({ name:AVOIDER, like:FOOD, dislike:POISON, cloneItSelf:0.0005,// likeDislikeWeight: [1, -1], fear:{CREATURE:[-0.9,100],EATER:[-1,100],PREDATOR:[-1,100,function(){this.health+=this.badFoodMultiplier;}]},});/** * likes poison * emits food as waste compound * seeks creatures, predators, avoiders and EATS THEM */ ecoSys.addBehavior({ name:EATER, like:POISON, dislike:POISON, likeDislikeWeight:[1,1], fear:{CREATURE:[1.0,100,function(list, i){ list.splice(i,1);this.health+=this.goodFoodMultiplier;this.radius+=this.goodFoodMultiplier;}],PREDATOR:[1.0,100,function(list, i){ list.splice(i,1);this.health+=this.goodFoodMultiplier;this.radius+=this.goodFoodMultiplier;}],AVOIDER:[1.0,100,function(list, i){ list.splice(i,1);this.health+=this.goodFoodMultiplier;this.radius+=this.goodFoodMultiplier;}],},callback:function(){if(random(0,1)<0.05){addItem(ecoSys.entities.FOOD,1,this.pos.x,this.pos.y)}}});// UPDATE & RENDER ecoSys.render(); ecoSys.update();renderItem(ecoSys.entities.FOOD,'white',1,true);renderItem(ecoSys.entities.POISON,'crimson',2);requestAnimationFrame(animate);}animate();}window.onload= load;
And that's it, phew, that was a lot of work. But in the end, we have beautiful flocking creatures playing around and interacting with each other. have fun watching them all day. <3