itch.io is community of indie game creators and players

Devlogs

Superbug Sunday Specifics #4 - Optimizing for Bugs

SUPERBUG
A browser game made in HTML5

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…

Superbug Sunday Specifics #4: Optimizing for Bugs 

(...what are we even optimizing?)

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!

Measuring 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:

  • Total frames for each second
  • Count of all active characters and projectiles for each second
  • Count of all characters, projectiles, and effects spawned for each second
  • Longest and shortest frame length on each level
  • Average framerate for the level (total frame count divided by level duration)
  • Level load time

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.

The Entity Unification Project

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:

  • The "Entity Core" - A central component that manages all other aspects of an Entity
  • The entity Motor - Optional but common; The "physics" side of an entity, handles movement updates and interactions with the entity Model. Character motors can take inputs, while Projectile motors are always moving.
  • The entity Model - The "visual" side of an entity, handles its model and visual effects. In charge of displaying changes in information made by the Motor and other components.
  • Health Components - For Health. Entities can have multiple health components, which distribute its health into "phases" - most notably used for the Superbug's multiple lives, but every enemy has at least one health component.
  • Entity State Machines - A kind of "shapeshifting" component that performs whatever logic it is told to run. Used for attack states and running some enemy behavior.

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:

  • Disabling Model updates if the Model is not currently visible - saving processing power on things that are off-screen.
  • Using the Entity Core as a mass-component cache - greatly reducing the number of times any component needed to be found, thus saving on important performance costs.
  • Fixing Model interpolation to appear smoothly as intended - less of a performance issue, and more of a longstanding pain because of the inability to affect both types of entities at once with my changes.
  • Pooling all Entities of each type into static lists, allowing us to do more efficient searches for Entities specifically - reducing the performance cost of AoE damage effects (and allowing us to do more flexible AoEs), targeting for enemies and projectiles, Line of Sight calculations for enemies, and more.
  • Blanket-fixing errors with projectile collision and character movement inputs, helping the game to feel more stable.

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...

The Great Rotation

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.

Download SUPERBUG
Leave a comment