After a small hiatus to deal with life, I’m back at it! There’s a bunch of ground to cover in this post; I’ve made some significant performance optimizations, some behaviour changes to the grass, and have also added water generation!
Alas, I wasn’t thinking to take screenshots while anything was in progress, so be forewarned that this will be more of a text-heavy update, with just a couple images of the latest progress.
First let’s talk about the performance improvements. This is probably the most boring bit as it’s almost entirely behind-the-scenes work, so I’ll try to keep it brief… ish.
The existing issue was that, in order to be aware of its surroundings, every piece of grass was checking for other game objects in a small square around it every single time it was updated by the program – roughly every frame. This caused the amount of computation to quickly ramp up as more and more grass was spawned, creating significant slowdown after running for even a minute or two. I tried experimenting with adding a delay to the grass calculations (pause for X frames before updating again), which did help to some degree, but ultimately didn’t solve the issue at scale.
I had a number of thoughts on how to deal with this (and suggestions from some friends I was bouncing ideas off of), but settled on creating a new script (I’ve called it a “brain”) that is in charge of controlling a collection of grass squares. When the program starts I spawn a number of brains with one grass square each, and every update cycle each brain script randomly chooses one of its grass squares to run its own updates, and the others remain idle. Each time a new square of grass is added, it’s placed under the control of an existing brain. This means that as the program runs, the number of grass updates stays consistent, rather than increasing with new grass growth.
This new approach creates some new issues that need solving – primarily that it artificially slows the growth of grass as time goes on – but the performance gains are very much worth it. As I write this post, I’ve had a simulation running for over 20 minutes with virtually no slowdown.
A blue addition to the family
As much fun as it has been tooling around with the grass (though as you’ll read later, I’m definitely still doing that), things won’t really get interesting until there’s more factors within the simulation. My next step here was to add bodies of water to give the grass a more meaningful way to gain energy.
My method for generating the water was to first place a small number of initial “seed” squares of water and then continue to add more water next to existing water semi-randomly. I limited the placement to the main cardinal directions (no diagonals) to keep the bodies of water contiguous. I’ll need to come up with something a bit more sophisticated eventually for things like rivers or streams that have more intentional shapes, but for now this does the job well enough.
When adding the water I ran into technical issue due to some code I had taken from the first game tutorial I built. Essentially, it placed random game objects by first creating a numbered list of all squares on the “board”, then picking from that list at random, placing the object there, and removing that position from the list. This is what I was using for initial grass placement.
It’s is a very clean and efficient method if we are only placing objects randomly from the list, but the water is now being placed in semi-specific positions. Since there was no efficient way to find a position in that list based on its coordinate, any alternate placement method (e.g. water next to water) would cause the list to no longer reflect the “board” state, making collisions possible.
The solution was relatively simple: I needed a placement method that could check whether or not a coordinate was “empty” before placing an object there. Luckily I had already created exactly that for the grass’s growth script, so I only had to make a more abstract version for the world generation to use. This let me scrap the “list” method entirely and streamline object placement overall. As with most things this isn’t without its drawbacks (e.g. since we don’t keep track of free squares, placing an object randomly on a very full screen becomes exceedingly difficult), but we’ll also toss this on the “good enough for now” pile.
Lastly, I visualized water “depth” by changing its colour based on how many water tiles surround it. This is purely cosmetic; if I ever want to do anything functional with water depth I’ll want something a bit more meaningful, but for the time being it’s much nicer looking than just flat blue.
Yes still more about grass
With some performance issues solved and water existing in the world, I was able to start making some significant updates to grass behaviour. The primary change being to how grass gathers energy.
Previously, grass would lose a fixed amount of energy each “update” and then gain back a semi-random amount based on how many grass and non-grass squares were surrounding it. Then if the energy reached a threshold it would try to grow, if it hit zero the grass would die. I manually tweaked these values so that the grass would live in some cases and die in others, but on the whole would generally live. While this successfully created something interesting to look at, it ultimately wasn’t very meaningful. There was enough variability to make it look vaguely organic, but grass that was in a “living” position would essentially never die, which was far from ideal.
Instead, grass now gains energy based on its proximity to water. This has some limits (that I’ll go into below), but is a big step towards the environment determining how the things in it behave. And going back to an old idea about crowding, the amount of energy grass loses is now increased when it is surrounded by more grass.
Another small change I made was to allow grass to grow up to 2 spaces away. This wasn’t in response to any particular challenge or goal, other than to give entities more behavioural options.
The result of the above changes was very satisfying. We see grass clustering around water sources, but not too aggressively. Growth now slows down when grass starts to fill up space, and we no longer see a trend towards large, uniformly dark green clumps. We also see grass start to die around the fringes when it tries to grow into spaces that are too far from water or that create too much competition.
Unfortunately, as mentioned above, there’s some limits to the new behaviour. The biggest issue is the strict limit to how far grass can exist from water due to how distance is currently being calculated.
The “standard” way to calculate distance is to scan the game area (or a sufficiently large portion thereof) for every single object that exists on the correct layer, then iterate through each one and do some math to figure out which is closest. This works in a lot of cases when dealing with a limited number of objects, but it my case it scales poorly.
Since grass and water exist on the same layer, the distance calculation becomes less efficient as more grass squares are grown. The current band-aid for this is to limit how far grass will look for water, which limits the potential number of grass squares it will have to check. The downside is that this makes an artificial threshold for where grass can live.
This is an issue that, going forward, will effect anything that needs to make decisions based on what it knows about the world, so I’m going to have to find a better long-term solution. My ideas right now involve giving objects a “memory” for certain things that will update when the target in question changes, rather than every time the object needs to make a decision that involves the target.
Some upcoming priorities are:
- Create an ambulatory animal entity that uses the grass for food.
- Update the “brain” script to allow splitting off after it gets to a certain number of controlled objects.
- Implement aging for living entities.
- Upload the project to Github. This was a request from a friend to check out the source, and figure it wouldn’t hurt to share it here as well.
- Create a proper to-do list. I may also make this public.