First Foray into Unity Performance Optimization
In the last update to Prismatic Maze, I spent some time digging into performance optimization really seriously for the first time since its initial release.
I was getting something between 30-60fps generally on my test machine but there were stutters and spikes and I couldn't make some changes (like adding larger maps or dynamic lighting or more dynamic camera control) without making those issues worse.
All of that without really feeling like I was putting that much onto the screen or doing that much with it - no fancy lighting, no fancy shaders, just a few moving parts...
Clearly, there is much room for improvement.
Before I get into any details, let's look at a before & after comparison.
The chart below shows a before and after comparison from Unity's performance monitor.
On the 'before' side, the blue spikes represent the blue block of the 'update' cycle where all of the logic described below was running.
On the 'after' side, you still see blue spikes where that logic is running, but note the actual logic is much faster now in the blue block below.
Also note that in the 'before' side, spikes jump up pretty regularly through 60fps up to and sometimes past 30fps - those are frame drops.
On the 'after' side, there are still dips under 60fps but it's much more consistent and performant, even after adding some additional complexity.
I'm still learning to use Unity and most of my previous experience had much different performance requirements than video games do, so most of the performance issues in the game are the result of inexperience on my part.
When I dug into what was causing things to slow down, I discovered a few simple settings and design mistakes that could be corrected relatively easily.
After the low hanging fruit, though, the stutter problem was a bit more complicated.
It is my current understanding that a game built in Unity is something like a simulation that takes a screenshot of its current state every time a frame is pushed to the screen.
The game engine is designed to take care of this for you - make some objects that have game world coordinates in them, set them up to update themselves each frame & boom you've got moving stuff.
On top of that, there are a lot of things to help out moving or scaling those objects in various ways, built-in stuff to handle collisions, lighting and physics, graphics batching features to get your scene rendered efficiently, etc.
So, the game loop is something like how any video system works in that the primary loop is pushing an image to the screen so many times per second.
How it differs from a vhs player or dvd player or video codec is what it's doing to determine what is on that image - instead of reading an image from some media or taking a keyframe previously read and appying a diff after uncompressing data, it advances the simulation and renders the results.
It turned out that the primary cause of the stutter problem was that I had put too much logic into calculating and loading tiles when your avatar moves around.
In this game, the entire maze is built dynamically and it's shown dynamically as you move around in it, which limits some of the automatic optimization options.
I was using a dynamic pathfinder to check if the move from one tile to another was valid as a safeguard against clipping through walls.
I was running some rather complex loops to look down corridors in order to determine which new tiles to expose when moving around.
I was loading tiles into memory around the player dynamically.
I was doing all of that on the update cycle when the player stepped from one tile to the next.
That worked but it took too long and it slowed everything down.
So, I ripped all of that logic apart.
I changed the visibility and move validation checks to use cached lists of pre-computed moves that were more expensive to create but stayed valid until changes were made.
I added logic to rebuild these when necessary and used faster lookup patterns when checking for them.
I dug through the logic to remove unnecessary checks and redesigned longer-running processes to run across multiple update cycles.
In game, you can see the multiple-update-cycle change in the game when you move to a new tile that exposes multiple tiles down a long corridor - it used to pop in all the newly exposed tiles at once but now they pop in quickly in sequence.
I know it's a small thing, but I'm really proud of these changes.
It's a significant improvement & many lessons learned.
I really want to make my games super smooth and of course I also want to make them vibrant, lush and beautiful so there is much left to learn and improve on.
So exciting ;)