Posted September 30, 2024 by Plumicorn Digital
#devlog
Heyo– September’s nearly over, which means it’s time for another Superbug Sunday Specifics! I’m changing this monthly devlog to be the last Sunday of each month now instead of the first, because I like that vibe more.
The last few devlogs have been about new features and design choices, so let’s have a change of pace and discuss performance instead. While Superbug isn’t exactly a performance-intensive game to begin with, I don’t want that to let us get away with being wasteful. In an ideal world, with how simple Superbug is, I’d love to run the game on anything I can think of - a fridge, a phone, a potato, you name it. We would be fools not to try! So let us begin.
You might wanna grab a snack and a drink, because this SSS is about to get looooonnggggg…
Before we get into the details of optimization, let's set the stage. I’ll begin with some exposition on different types of frames; if you already know what’s the difference between an Update and a Fixed Update, you can skip to after the next GIF.
For the uninitiated, most modern games (SUPERBUG included) run on two main types of frames: The first one, which most people will think of when they hear “frames” in a gaming context (ie FPS or Frames Per Second), is a type of update that runs as fast as the computer can handle it. This type of update (hereby a “frame”) is usually reserved for relatively low-cost computation and user-facing game elements, like input handling and graphics updates. Physics and other intense computation is instead in a special type of frame called a fixed update (or a “tick”) which happens on consistent intervals (in the case of Superbug, and by default for Unity games: 50 times per second).
Using ticks behind the scenes has the great advantages of limiting the performance cost of things like game physics, as well as making those calculations much more consistent, without having a visible negative impact on the running game. Ticks are almost always a performance improvement for computers that can render frames faster than they can tick, which is why this setup has become a standard for modern games.
This frame-tick structure is however not without its complications. Novice developers in Unity (including us at Plumicorn; remember this is basically our first game) might only delegate movement of their objects to physics-controlled Rigidbodies and be done with it. This is fine, except without any extra work, this means that objects will only visually update once per tick. In other words, SUPERBUG’s game elements were (generally) frame-locked to the game’s tick rate, 50 FPS (except for some inconsistencies).
To solve this issue, all physics-controlled objects now also have a separate model object. The physics “body” performs all of the physics and collision logic during the game’s ticks, while the model object interpolates its position and rotation to match the physics body in frames between ticks. Interpolation has the issue of causing models to lag behind the true position of the physics body, but 50 ticks per second is fast enough that you should never notice anything colliding far away from where their model is.
(Worth mentioning, Unity does have a native solution for interpolating Rigidbodies - but I think our solution performs better)
This all matters because, without interpolation, any extra FPS we could squeeze out of the game by optimizing it wouldn’t really make a difference. Now with that out of the way, let’s talk performance!
Before making our major changes, I set up a test to benchmark our performance on each new version. To do this, I made a special new run type I’m calling a “performance run”; this run type has a fixed level sequence, starting from the Tutorial level and doing every level in order. In the background, it records performance data, including:
While somewhat crude, this gives us enough information to see broad correlations and improvements as they occur. After the performance run is finished, it builds a summary and a data sheet we can use for analysis. Unity’s profiler also comes in handy to help diagnose and address performance costs on a finer level.
For these performance tests, I'm only going to look at the results for level 1, 2, 3, and 5, because the other levels tested received major changes. Here's a look at our first results:
As you can see, SUPERBUG's frame rate on my system averages at about 500-600 FPS, but varies wildly between 400 and over 800. Performance is heavily correlated with object spawns, which is something we'd like to begin to fix.
As I mentioned earlier, all entities including characters and projectiles have some common functionality. However, until recently this functionality was defined independently for both types of entities, which lead to some awful spaghetti-code with special case handling, but also generally making SUPERBUG’s codebase far less maintainable.
Thus, one of my big projects this Summer was what I call the Entity Unification Project: bringing characters and projectiles together, united under common systems. For those keeping track, this was the “crazy shit” I mentioned working on in the end of SSS#2. Needless to say, this took me a while, but it was all worth it.
Because characters and projectiles share these common components, we can combine them into a basic "entity" system that is made up of multiple pieces. All entities can be broken down into a mixture of these parts:
Before the Entity Unification Project, Projectiles had their own form of Core, Motor, and Models implemented (but not Health Components, those were the same). Combining common functionality with that of Characters reduces the maintenance strain on our codebase, allowing me to make such improvements as:
While the Entity Unification Project was somewhat of an arduous process, it ended up being massively worthwhile. On top of the ease of maintenance, it greatly sped up the pace of further development, including out next optimization project...
When we started the SUPERBUG project in December 2020, we built it by reusing some systems from a game demo I made prior; a 2D platformer game (it's not Oh, Wyrm!!). SUPERBUG was made with the intention of working up those systems so we could recycle them back into the original game. Because of that, we naive developers wanted to keep "jumping" as in the Y axis, meaning we decided to build SUPERBUG in Unity's X and Z coordinates. As a result, thanks to some limitations with Unity's 2D physics, we were forced to use 3D collision for SUPERBUG despite the gameplay being entirely 2-dimensional. In other words, we were wasting a lot of processing on an unused third dimension.
So this month, as I’ve been optimizing the game’s performance, I decided it was time for me to rotate the entire game - better now than later. It won’t really take away from our systems’ ability to be upcycled for future projects, as long as I build them in a way that can be easily changed to suit the needs of the project in question. Ultimately it wasn’t a difficult thing to fix, but it was incredibly tedious. I had to rotate ~8 levels (6, plus the tutorial level and a super special one I’ll talk about another time), plus every character model, and fix some math under the hood. The nice thing about fixing the code I fixed was that it was technically always broken, so in doing this project I addressed quite a few bugs, even if the performance improvement didn’t end up being much.
After finishing The Great Rotation, lets take a look at our final performance data.
After this project, my average performance has improved to 600-650 frames per second. The frame rate is much more stable overall, with less powerful dips in performance - although the correlation between object spawns and performance drops is still significant, that is something we can address with future changes. Anecdotally, the game feels much more consistent with collisions, and character and projectile movement feels less stuttery thanks to my fixes to interpolation.
Optimization of SUPERBUG is an ongoing process, that will continue as we move into the game's future. Performance optimizations like object pooling and delayed spawn grouping are on the table, as we have plans for new major features that will increase the game's performance demand drastically - features which I'm very excited to begin discussing in next month's devlog, SSS#5.
See ya on October 27th - With love, Borbo and friends.