Table of Contents
Motivation
The goal of this simulation was to have actors, who have some kind of genetic properties, freely moving on a 2D plane. To pass on their genetics these actors have objectives they fulfill during their lifetime.
In this case, the actors are two types of fishes, that have certain movement rules, collect food to reproduce and pass on their genes to their offspring. The smaller fishes are herbivores, who eat the algae, represented as small dots; the larger fishes are the carnivores, who eat the herbivores.
For the movement, I took inspiration from boids. Boids have three parameters according to which they move: separation, alignment and coherence. Separation simulates avoiding running into another boid, alignment simulates how much the boids steer in the same direction and coherence simulates how much the boids want to be close to one another.
In this simulation I use the concepts of separation, alignment and coherence within species but also cross-species, e.g. for the fish to swim towards food or avoid enemies. I also decided that the carnivores do not hunt as a herd, so they do not have coherence or alignment towards each other but only separation among each other and coherence towards their food, the Herbivores. Herbivores have separation, alignment and coherence within each other as well as separation towards the carnivores and coherence towards food.
Implementation
Position
We can implement these steering rules as the acceleration, , of the fish. For each frame we calculate the new acceleration based on these rules then update the fish’s speed, , and position , accordingly.
so in this case and .
Now that we know how to get the position of the fish in each frame we need to think about how we come up with the acceleration. First of all, we notice that separation and coherence are almost the same mechanics but in different directions, meaning we only need to come up with one function that we can use for both. If we have a fish and the things he wants to go to or avoid we can just look at the closest one of these objects that the fish can see and then accelerate away from it or towards it:
function separationOrCoherence(
things: Thing[],
separationOrCoherenceConstant: number,
): Vector2 {
let minDeltaPosition = Vector2.NullVector()
let minDeltaLength = Number.POSITIVE_INFINITY
for (let i = 0; i < things.length; i++) {
const deltaPosition = position - things[i].position
const deltaLength = deltaPosition.length()
if (!thingCanBeSeen(deltaPosition) || deltaLength > minDeltaLength) {
continue
}
minDeltaLength = deltaLength
minDeltaPosition = deltaPosition
}
return separationOrCoherenceConstant / minDeltaLength * minDeltaPosition
}In this function, we are searching for the closest thing that’s visible to the fish and then accelerate or decelerate in its direction.
Now for the alignment, we again need to consider all things in the fish’s view but this time we want to get the average direction the things are traveling in:
function alignment(
things: Thing[],
): Vector2 {
const averageDirection = new Vector2()
for (let i = 0; i < things.length; i++) {
const deltaPosition = position - things[i].position
if (!thingCanBeSeen(deltaPosition)) {
continue
}
averageDirection.assignmentAdd(things[i].direction)
}
return alignmentConstant / averageDirection.length() * averageDirection
}It should be mentioned that we could use a more functional style of programming to get a cleaner look for these functions let’s say:
function alignment(
things: Thing[],
): Vector2 {
const result = things
.map(thing => Tuple(thing, position - things[i].position))
.filter(thing, deltaPosition => thingCanBeSeen(deltaPosition))
.map(thing, deltaPosition => thing)
return result.sum() / result.length()
}In my simulation, there are a few extra things that need to be calculated in the loop and it made for a significant performance decrease so I decided against it.
Genetics
The genetics are almost entirely removed from the fish’s movement, this makes the simulation considerably less realistic and we will need to revisit this later when we discuss the problems the simulation might have. I decided to give the fish sight, speed, stamina and food genetics. Now if we just let the fishes evolve it makes sense to assume that the fish with the genetics that are maxed out in every category will always be the one to most likely survive. This means we need to balance these out; I decided that a fish can either be good at speed or sight and food or stamina never both.
But let’s first discuss what the different genetic attributes do as the gene quality increases in its respective category:
| Category | Effect |
|---|---|
| Sight | The fish has a greater field of vision and can see objects that are further away. |
| Speed | The fish has less water resistance which makes it faster, in general fishes can go a little faster for a short time, during which his water resistance is decreased further. |
| Stamina | The fish can speed up for a longer amount of time. |
| Food | The fish can reproduce with less food and needs less food to do so and he gets hungry slower as well. |
The fishes have a number for speedSight and one for foodStamina, with values in the range of 0 to 3, low values favor speed and food respectively and high values are for the sight and stamina categories. We can see the average genes of the fishes in one of the bottom corners of the simulation from top to bottom these bars stand for:
Herbivore to algae count percent - left herbivore right algae.
Herbivore to carnivore count - left carnivore count right algae count.
Average sight/speed gene value among herbivores.
Average food/stamina gene value among herbivores.
Average sight/speed gene value among carnivores.
Average food/stamina gene value among carnivores.
To reproduce a fish needs to have a certain amount of food and then a new fish is spawned at his position, the genetic difference between the fishes is at most one on each slider, with 0 and 3 and being considered one apart as well.
If everything is set up right we should expect fishes with bad genetics to die and fishes with good genetics to survive, since the different types of fishes have different roles they fulfill in the simulation we might expect different types of genes to be the best.
Procedural Animation
The fishes are animated procedurally which is implemented as shown in this video. Each fish has a spine that consists of spine segments “dragged” behind the position of the fish. This works by first updating the position as detailed above and then pulling the rest of the spine behind by updating the spine segments’ position to always have fixed distances from each other:
function updateSpine(newPosition: Vector2) {
segmentPosition[0] = newPosition
for (let i = 1; i < segmentPosition.length; i++) {
const segmentDirection
= segmentPosition[i].subtract(this.segmentPosition[i - 1])
.normalized().mult(segementDistances[i - 1])
segmentPosition[i]
= segmentPosition[i - 1].add(segmentDirection)
}
}Problems
Unsolved
Genetics do not Converge
We do not find fishes of a specific genetic type to do better or worse in the simulation, this could be due to numerous factors. I think the most plausible explanation for this is that the movement of the fish is not very connected to their genetics, it does not matter whether or not it’s advantageous for the herbivores to swim in groups since it is hardcoded into their movement and will not change based on their genetics, in nature this behavior would be explained by groups looking like one big entity or being stronger together so that predators might be afraid to attack but this is also not the case in this simulation. Another good reason for the non-convergence is that there need to be certain less realistic adjustments for the sake of being visually more interesting. The last fish of either species cannot die since if all the herbivores die the carnivores will have no food and will die as well and we would be looking at an empty pool. If all the carnivores were to die the herbivore population would grow indefinitely since there is no food shortage in the simulation. Additionally, the simulation restarts if the number of fish is too large since it would start lagging.
Solved
Spinning Fish
Another interesting problem was that the fish began to spin. I later figured out that this was the case when the fishes were too slow since the acceleration could then be much larger than their velocity and thus change their direction too quickly, this was solved by introducing a minimum velocity that the fishes travel at, but we can still see the fishes shake slightly if they travel at minimum velocity sometimes.