I have always been heavily inspired by games that feature evolution, such as Spore and EVO: Search for Eden. Ever since I started practicing the hobby of game development, there has been an idea in my mind's background that making an evo game would be cool. But the possibilities are so great that I never knew where to start. There are so many ways to make an evolutionary game! Which approach should I take - rather, which approach do I like most? These decisions gave me analysis paralysis and made me stop and ponder. I pondered so much that I never made a move.
But some months ago, a certain video made the rounds around the YouTube algorithm: "Simulating Natural Selection", by Primer. In the video, Primer creates a simulated natural environment with some creatures (the blobs, as he calls them), that can mutante and pass on their genes to future generations. All in all, it's a basic genetic algorithm. But it's so well presented and cool and his results are so marvelous that the video single-handedly made me look (once again) at my 'evogame notes'. I was very inspired.
How surprised I was when Sebastian Lague made a little video on the subject, also inspired by Primer's video: "Coding Adventure: Simulating an Ecosystem". So I wasn't the only one! That made me feel a little bit validated, and so I proceeded with my notes and ended the analysis paralysis by making a strong decision: I would limit myself to copying what Sebastian did in his video. If after that I had any more ideas, no problem. But I'd start with something simple, only copying something already done. That's how people learn, right? And after all, I don't have much experience writing genetic algorithms or eco-simulations or anything like that. Part of the analysis paralysis was the thought that I could implement any of those ideas - but if learning was a challenge in and of itself, then maybe I should start with something small.
Hence, this project: replykatie (the title is just a weird way to write "replicate", and also reminds me of Kadoaties, a petpet that's forever stuck in the gutters of my subconscious).
I'll be doing it in Unity, because that's the engine I'm most comfortable with.
[devlog]
So, that's the initial idea for this project. But then, you might ask, what is this? This text, where does it come in? Well, it's just a little something that I thought would be cool: a devlog for the project, where I could record some thoughts and experiments from time to time. Why? Why not!
Also, it's written in english. English isn't my first language (I'm brazilian, so that would be portuguese), and even though I understand it fluently (both read and spoken), I still stutter sometimes when speaking or writing. I can stutter my way in a conversation no problem, the message gets sent and I can get myself understood, but in writing it's a whole other story! I don't have nearly as much experience writing in english as I do with speaking. That's all to say: experience writing in english would come in handy, and this devlog could serve that purpose. That's that on that.
[[the project itself]]
[25/09/2019]
Because I'm starting this devlog 2 weeks after starting the project, there's some catch up to do. I'll try to explain in some detail how the systems work for now.
I used Unity's tilemap features to draw a crude map (very reminiscent of NeoQuest II, in my mind...) where I plan all the project to take place.
Some of my friends looked at it and said: "nice! is that procgen (procedurally generated)?" no!!!! That's beyond the scope of this project, at least for now. It's just a little something that I made up in a few minutes using assets that I had on hand.
With the map out of the way, I started working on real meat of the project: the bunnies. And oh boy, do I have to tell you about the bunnies.
When I started this project, I thought that most of the time would be spent figuring out how genetic algorithms work, and learning all about evolutionary stuff. Never in a million years would I imagine that I'd spend so much time trying to figure out how to simulate an animal environment.
Because the bunnies have to do stuff! Of course they have, how could they not?? I know that Primer's video uses blobs and simple fitness functions (I think he uses a "day" model or something like that), but that's so not cool! Sebastian knows where it's at, doing the bunnies reproduce, eat, drink and all that jazz. That's what I want to do!
For us to do that, we need to figure out how to model a creature's behaviour. After much pondering, and then discussing it with some of my friends, I arrived at a solution that left me satisfied: processes.
(Just as an aside: part of what makes this project so much fun for me is that I intentionally only work at it when I'm at the college lab. It's not the best place to work (many distractions), but when people see what I'm working on, they always want to know more about it. And the challenges in this project ("how to model the behaviour of a creature?") are so inherently interesting and discussable that people almost always get excited and offer their own ideas. It's been a long time since I've felt this (since... what? VCS?), and makes me look forward to those moments. I don't know if that's gonna continue when I moe on to other parts of the project (like the evolutionary stuff), but for now it's pretty nice.)
This is how the process-based behaviour works:
A creature is always running some process. E.g.: drinking water, searching for food, idly moving.
Processes have priorities (loosely based on Maslow hierarchy of needs). If a creature is searching for food but suddenly sees a predator (foxes!), it should run the heck away because its safety has higher priority than its hunger (humans used to hunt deer by outrunning it, I'm pretty sure I've read that somewhere).
Processes can be canceled via interruptions. When a creature sees a predator, an interrupt with high priority is generated.
If a creature is running a process with priority N, and an interruption is generated with priority M such that N >= N, then the interruption is discarded and the process keeps running. That is to say: a process can only be replaced when it ends, or when another process with higher priority is requesting execution.
A creature has necessities (hunger, thirst, etc) that change over time. They are represented as continuous values, and range from -1 to 1.
Necessities change at different rates: thirst may need to be addressed more frequently than hunger (although that's correct, I don't know if that would make for a good simulation. But in theory, the model needs to be able to address that).
A creature has sensors:
Internal sensors are always pooling necessities and trying to create new processes via interruptions. The priority of those interruptions are proportional to the respective necessities. The more hungry a creature is, the more it is pressured to start searching for food. These sensors are executed in Unity's "Update" function.
External sensors are always attentive to the environment. When a predator arrives on the scene, it's the external sensors that say: "look out!", generating interruptions with high priority. These sensors would be implemented via Unity's regular "OnTriggerStay".
Each process has chain states (the name isn't the best, but I have a penchant for weird names so...). I think I can best explain this with an example: whenever a creature sees some food, it generates an interrupt of the kind "go to food", even though it isn't necessarily searching for it. If the creature isn't hungry, then that interrupt would have low priority, and wouldn't be acted upon. But if the creature was running the process "search for food" for whatever reason, then "go to food" needs to run immediately (after all, that's what the creature was looking for!), regardless of its priority. That happens because the process "search for food" has "go to food" as a chain state: in other words, "search for food" is vulnerable to "go to food" interrupts. It's just a way for creating dependency between processes. I don't know if it's kinda hacky or not, but it makes sense in my head.
Oof, I think that's pretty much all the rules I have for now! I did something kinda rare and tried to think of them all before implementing them. I'm way more used to thinking while I'm programming, so taking a step back and planning my approach is very refreshing. That's also one of the goals of this project, and one of the reasons why I'm intentionally only working on this project when I'm on the college lab. That way, I can stop myself from getting too absorbed...
But having that said, I actually implemented those rules, and they worked surprisingly well!
So, that's them bunnies walking around and eating when hungry and idling when not... I only implemented hunger up until now, because I figured the other necessities would be kinda similar. Before I forget, those necessities are: hunger, thirst, reproductive urge (to borrow the terminology that Sebastian Lague uses) and safety (running away from predators). Actually I don't know if I'm gonna implement thirst because I don't think looking at the bunnies drinking water is going to be so interesting. But who knows.
I also added a way for the bunnies to die when they are too hungry:
That is being done using the extremely complex formula "health = hunger". If health is less than -0.5f, the creature dies.
And that's all I have to show you. The next step is going to be adding reproduction, so we can get a little bit closer to putting the "evo" in "replykatie". I have some ideas as of how to model that system, but we can talk about that later.
See you next time!
[29/09/2019]
Hi gang! Today I want to talk to you about the newest additions to replykatie: reproduction (flowers and bunnies), and natural death.
The reproduction of flowers is pretty simple, and I didn't want to complicate it. Flowers have two attributes pertaining to reproduction: a "spread radius" and a "spread time". The "spread radius" is the area around the flower inside of which it can spread its pollen, and generate more flowers. The "spread time", on the other hand, is basically a number of seconds the flower must wait until it can reproduce again.
This was pretty easy to code, and I compensated for that quickness by adding a "size" to the flowers, proportional to their lifespan - basically, flowers start small, and grow over time. I also thought it would be cool if the "nourishment" of a flower was proportional to their size, so I did that too.
Bam - flowers start to proliferate.
I feel like I need to point this out - if the flowers are going to have their own population and reproduction rate and all that, I could analyze the difference in population between the bunnies and the flowers. There's no need to add a predator (like the fox in the Sebastian Lague video), since the flowers would also restrict the bunny population (as bunnies die of famine). For that to work, I'd have to tweak the flower spread attributes, which could be problematic... But then, I'd also have to do that with the fox, so might as well do it with the flowers. At least in the beginning.
With the flowers out of the way, I started working on the reproduction of the bunnies.
Oh boy, let's start with "reproductive urge". I initially thought that "urge" would be a necessity just like "hunger" or "thirst", but bunnies don't die of libido, so what would happen is that when "urge" arrived at max, it would've stayed that way until the bunny in question satisfied their bunny necessities. That is not a problem. The problem is that while necessities like "hunger" can be satisfied by unilateral actions (the bunny goes to the flower and eats it), "reproductive urge" must have two parts involved. It takes two to tango. So, which bunny would start the action?
I think the most natural answer would be "the male one". If you look at how dogs and cats do it (specially dogs), it's pretty clear that male dogs keep annoying the female ones until they cede. I didn't research how bunnies do it (the script is still "Creature.cs", after all), but I assumed (perhaps incorrectly) that it would be similar.
So, for the male bunnies, "reproductive urge" would be a necessity just like "hunger", which it would be able to satisfy by going to female bunnies and reproducing. In theory, this could work - it wouldn't even be necessary for the female bunny to have "reproductive urge". But this made me feel bad (every time I sat to code the reproduction, I felt embarrassed for the simplifications - don't even get me started on "Copulate()"), so I made the reproductive urge of the female bunnies hbehave according to a sinusoidal, as if it were seasonal. This way, reproduction could only occur if the female bunnies were willing.
Maybe there were better ways to do it, but I think it's good enough, at least for now.
I also needed to decide some other stuff, like "gestation periods". When a bunny couple gets together, the female starts the gestation period (which is just a timer). During that timer, the female bunny moves slower, and when that timer gets to 0, another bunny pops up (with 50%-50% female-male random chance)
So, bunnies and flowers are reproducing. But something needs to stop them from crashing my computer with their rising populations! Death by hunger was already implemented (the bunnies killed the flowers by eating them, and the bunnies themselves died of famine), but I also saw it fitting to add natural death.
I started adding it to the flowers, but then I realized that the bunnies would also need the same feature, so I created a class called "Lifeform" from which they both would inherit. A "lifeform" has a "life expectancy" range (e.g.: from 20 to 35 seconds), a "death sentence" decided when it is born (e.g.: Random.Range(20, 35)) and a "lifetime" counting from 0 to "death sentence" (1 second per second). This worked beautifully. Natural death is a fade out, just like in real life.
God, look at these names... They are so silly! I like this project so much!
So everything kind of works, but I need to tweak the values so that the bunnies don't just like die of famine (they are pretty dumb and just lose track of where the flowers are). Maybe I'll need to change some other stuff, like the pathfinding. I don't know yet.
But that's it for now. Stay tuned!
[11/10/2019]
Hey guys! Whats up?
So, first I want to start with a little recap of where we left this project. We implemented flowers that reproduced themselves, and rabbits that acted based on their hunger and reproductive urge. The rabbits' actions were determined by a system akin to a computer, based on processes and interrupts. Everything that I would consider 'fundamental' for the simulation of a simple ecosystem was kind of working. Here is a video of a certain simulation:
So, as you can see, the ecosystem isn't achieving any kind of balance. The rabbits eat all the flowers, and then starve to death - a process that I thought was called "overpredation", but apparently that word doesn't exist, and "overpopulation" is used instead. For the record, the simulation displayed in the video above isn't an exception - that happens in almost every simulation. (The only exception is when the rabbits starve to death, but there is a hidden last flower that they couldn't find. What happens next is that this last flower reproduces itself and takes over the land).
So, what should I do? I showed the simulation to a bunch of friends, and they offered many different comments.
Some said I should add a predator (like a fox) to control the rabbit population. But I don't think this would solve the issue, as I'd just be tossing the problem to an upper layer - who would guarantee that the fox population wouldn't explode, just like the bunnies are doing now? I think it would be cool to add predators, but not as a solution to the overpopulation issue.
Others told me to tweak the values so the natality and mortality rates would match. They directed me to models like Lotka-Volterra, and told me to model my system according to those equations, or instead make my own. But this would go against my initial objective of just seeing how the ecosystem would balance itself! I'd be forcing an equilibrium, and I didn't want to do this. Shouldn't the system provide its own solution? I mean, how does nature solve this? (A biology friend told me nature doesn't solve anything - all balance is temporary, and inevitably chaos will prevail. But at least it's temporary! I don't even have that.)
A third group of friends said that I shouldn't simulate the flowers as a population, but as a fixed food source that the bunnies would compete for. There would always be, say, 30 flowers on the map, and when one of those flowers was eaten, it would respawn in 5 seconds. And that's it. Flowers wouldn't spread anymore, and their population would be constant.
Now, I didn't initially like this last approach: it's too much of a simplification. What fun is an ecosystem that doesn't behave like one? But the more I thought about it, the more I became partial to that idea. I mean, what I'm doing now is a simplification, too. When a rabbit eats a flower, the flower doesn't die forever - it regenerates, at least a few times, much like a tree whose fruits are eaten. It could even be argued that my current model and the "constant flowers" model are equally simplified. So, simplification wasn't a good argument.
What really sold me on the idea was the fact that without a stable population, it'd be very hard to get evolution to occur. How could genes be carried over time, if with the rabbit population dying after few generations? Because evolution was one of the main inspirations for starting this project, I decided to shelf this "dynamic flowers" model (which I named "popdy", or "population dynamics") and started working on a "constant flowers" model.
In terms of programming, this change wouldn't result in too much rework. I'd have to disable some things like flower reproduction, of course, but the "constant" aspect would be fairly easy to do: when a flower dies, it waits a few moments (defined by TIME_BETWEEN_RESPAWNS) and then creates a copy of itself. As simple as that.
Here is a gif of that working (I made these "constant flowers" have the color red, to differentiate themselves from their "dynamic" counterparts. I also added a population counter in the top left of the UI):
So, as you can see, it's working nicely! The next step would be to add the genetic stuff: mutation, crossover, etc. So, stay tuned!
(16/10/2019)
Hey guys! Let me tell you what I've been up to.
I started implementing the genetics stuff like we talked about, but then something happened which made me reprioritize some tasks. I'm talking about the simulation's performance.
If the rabbits' population exceeded 50, the framerate dropped to 10 (normally it would be 30). When the population reached 100, Unity would stop. This annoyed me a little bit, because we aren't even in the hundreds! Every ecosystem simulation that I know of involves hundreds, maybe thousands of creatures. Maybe with such a small population, we wouldn't ever see interesting creatures emerge.
This left me a bit worried, so I started trying to optimize some things.
Unity's Profiler tool indicated that most of the workload was coming from the "OnTriggerStay2D" callback in each of the creatures. If a flower is in field of view of a rabbit, the rabbit would take notice, and if it was hungry it would move in the foods' direction. This would happen because of the "OnTriggerStay2D". If we had 50 rabbits, every physics update (10 times per second) this function would be run 50 times, which isn't exactly optimized.
So, what I did was to implement my own physics update, using CircleCast to update the objects in the field of view of a certain rabbit. And this physics update would run only once per second.
With this change, now I could have a population of 150 rabbits with 10 fps. This isn't exactly ideal, but it's definitely better. So I left it like this and moved on, at least for now.
And then I started working on genetics.
For now, only two attributes would be defined by genes: speed and field of view. These don't have obvious disadvantages (hunger rate isn't proportional to movement), but as we shall see, there is more than meets the eye.
At first, there is the problem with representation. Normally in a genetic algorithm (as I understand it), you have some kind of discrete representation - like TAGCAGC - that gets transformed into your phenotype - for example, TAG might mean "a speed of 2" and CAGC "a field of view of 3". But how to encode real values into discrete representation? Obviously I could do something similar to a floating-point binary representation like the one defined by IEEE 754, but the problem is that flipping a single bit might mean a change of 1 or of 100, depending on the bit flipped. In other words, changes aren't incremental, or smooth. I know that discrete values have something called a Gray code, but I couldn't find online an analog for real values.
This is one of my simulations: at the top right, you can see the mean for "speed" and "field of view" for the rabbit population.
As you can see, there is a period in the start of the simulation (until 0:40) in which natural selection favors those with low speed. This happens because when a rabbit is hungry, it starts "random walks" looking for food, and if it doesn't find it, it dies of starvation. Because the map has a lot of "dead areas" between the flower clusters, if a rabbit finds itself in one of those areas and can't find its way back to a flower cluster, it will most surely starve. Of course, this doesn't happen with low speed rabbits, because they can't leave the area in which they were born - and if they are born in flower clusters, they will certainly be able to find food and pass on its genes. Also because of this, field of view isn't that useful - food is always close.
But there is a catch: if you never leave the flower cluster, you are basically competing for food with your family. Because their speed is so low, they most certainly won't find other flower clusters, and so it is inevitable that their population in that cluster will be maxed out at a certain level.
At some point, a rabbit will be born with a particularly high speed - high enough for it to reach another cluster in its lifetime. It won't compete with its family, and won't die of hunger. So apparently it's a very lucky rabbit! But the problem is: it won't pass on its genes, unless it actually finds a female in that new cluster, or if it finds its way back to its original cluster and mates there. Eventually, though, these rabbits have the advantage over their conservative ancestors.
And this is what happens at the long-term (0:40 onwards): rabbits with high speed dominate, and for them, a large field of view actually matters: as they are always traveling, they are exploring a larger space and need to observe the food, which can be far.
And that's it for my analysis! This was pretty fun, and there are many things I could do to evolve this simulation:
add more attributes to be genetically defined (gestation period, attractability, energy efficiency, etc);
try new environments;
split the rabbits into two populations, and then merge them. Which one would win?
add more creatures;
make the creatures behavior also change over time (reinforcement learning?);
optimize the code so I can add even more creatures;
make the project tile-based;
etc.
I started this project with the objective of imitating Primer's and Sebastian Lague's video, and I've done just that. But now, there are so many ways to proceed that I don't know which one to choose! I don't know what would be cooler.
For now, I think I'm satisfied with the state of this project. I think I'm gonna let this one lay low for a while and then revisit it in a future time.