One of the big pieces of feedback from the Steam Release (yay!) has been complaints about the performance of the game, and the couple bad reviews it’s gotten so far have focused on that specific issue, so I’m trying to do a bit of a performance pass.
Thanks to the subject matter, this will probably be a dry, technical post. Be warned. Be afraid.
Species is always going to be a somewhat sluggish game due to the nature of it’s gameplay (the game gets more interesting as you bring in more creatures and push the hardware to it’s limits) and the restrictions of making an accurate evolution simulator (I can’t lower the fidelity of the simulation where you’re not looking lest I affect the evolution), but there are certainly things I can do.
Let’s see if we can find some of them with the magic of profiling…
(For the record, I’m using a piece of software called Codetrack to analyse the game, since my old profiler stopped working. I’m quite happy with it so far, so I guess that’s a recommendation)
This is a flame graph of where all the CPU time went during a short Species game. Don’t worry, I don’t expect you to analyse it yourself. Here’s the same graph with labels:
(You should be able to click the image to expand it and be able to actually read the labels. Or just keep reading, I’ll try to explain as we go)
On the left we have the time it took to go through startup and loading routines. For now we can ignore them, they don’t factor into the frame-to-frame CPU load.
The big red mountain in the middle: that’s where most of our time is going. That’s Creature AI, which is unsurprisingly one of the largest costs of the game.
What stands out most to me is the plateau on the top of the AI mound. The width of these bars indicates how much CPU they cost, so long, flat methods like “GetGeneticDistance” on the top there are prime candidates for optimization.
GetGeneticDistance does what it says on the tin: takes two genomes and works out how closely related they are. When we say things like “humans and apes share 98% of their genetic code”, GetGeneticDistance is the method that calculates and quantifies that 98% in Species.
It’s not surprising that it would be expensive: GetGeneticDistance has to compare 80+ genes of varying types and ranges. What is surprising is that it would be expensive there. Normally, GetGeneticDistance is used to find speciations and decide if two creatures can mate, but this is coming from FindOptimalFoodSource (which also does what it says on the tin: finds the food source (tree or creature) best suited for the current creature to target).
The reason GetGeneticDistance is called from FindOptimalFoodSource is, put simply, Empathy. Empathy prevents closely related creatures from considering each other as food sources unless there’s no other options, and how do two creatures know whether or not they are closely related? GetGeneticDistance.
I think we can all see where this is going. Empathy is computationally expensive. It’s gotta go.
Luckily, there are proxies in the game for genetic distance, so we don’t need to get rid of empathy entirely. A creature knows who it’s parents and children are, and who members of it’s own species are: it doesn’t need to know the exact genetic distance to understand that it’s own children are friends, not food.
That leads us to this image:
Better! Previously, it took around 6x as long to calculate creature optimality than it did tree optimality. Now, creature optimality is barely double tree optimality! And a big chunk of that is a ToString method I later realised I didn’t need and removed entirely.
So that’s a big victory. But y’know what? we can still do better.
Looking into the AI’s call stack a bit, you’ll find “CalculateFoodOptimality“. That’s this one:
It’s a big CPU cost, but I kind of expect this one to be. In spite of how dumb they often seem, a lot of work goes into giving creatures the ability to make seemingly rational decisions. There’s no way around it: that work needs to be done.
Even so, that’s still a lot of CPU going into it. It might be difficult to make CalculateFoodOptimality faster, but what if we called it less?
Creatures call this method whenever they make a new hunger-motivated decision, which is every time they finish off their current food source, or fail to reach it. But this means they’re calling it every time a corpse disappears, every time they finish eating from a tree, and most notably for our purposes, every time they graze off a ground pixel.
Focusing on that last one for now. Creatures don’t need to make a whole new food decision after they pick off every ground pixel: they just need to keep grazing from neighboring ground pixels. So, that’s what they’ll do! They’ll only make a new decision if they reach a ground pixel that doesn’t have any energy for them.
Aaaaand done. How’s our performance now?
Okay, now we’re getting somewhere! Compare the size of CalculateFoodOptimality to the bar to it’s right (“WalkToTarget“, in case you’re wondering). We’re spending a fraction of the time on it, simply by reducing how often it needs to be called during grazing!
There’s still more optimization to be done here, but today was a good day. 0.11.0.7 now has significantly better performance than 0.11.0.6 did: it’s hard to quantify exactly how much, but it should be easily noticeable!
I’ve also fixed a number of bugs and unexpected behavior (mostly boring crashes, not really worth a blog post), so the game should be quite a bit more stable, too!
I’ve already uploaded 0.11.0.7 to Steam, so if you want to see the end result of all this, load it up. Personally, I’m quite happy with this hotfix.
No sign at all? Do we at least know if we sent them forward or back? No? So what you’re telling me is they could appear at literally any time in the history of earth?
Oh well. Out of sight out of mind, I guess.