Not Another Postmortem


Well now the dust has settled and I've played a little over half the entries, it's probably time to write up my thoughts! The first thing I need to do is thank the community. I was actually considering dropping out at the last minute because I didn't have much of a game, but a few words of encouragement in the Discord server pulled me through! More on that later. 

To start with, I had what I consider to be an excellent idea. Limited shapes+ limited space - bubble shooter is an obvious choice. Only circles are needed, it's a very basic style of game (perfect for a jam), and filling up the screen fits nicely with "limited space". But it couldn't just be a bubble shooter, it needed some sort of twist! I've been loving Balatro recently (along with the rest of the world), and I was a big fan of Slay the Spire too. Mixing that into the bubble shooter formula could be a great way to inject a bit of life into the genre! I had (have) a billion ideas for different charms for the player to equip and bubbles to buy. Central to this crossover would be a slight twist to the bubble preview - a "hand" and a "deck". You'd be able to choose from 3 bubbles, pulled from your deck, which all get discarded when you launch a bubble. Spoilers: this is the only thing that made it in from this idea ๐Ÿ˜‚

The tech

Okay if I'm going to be able to cram enough effects in in 10 days, I am going to need to think hard about architecture. Jam style code is not going to cut it here, I need to be able to hook into all sorts of different gameplay events and mutate them in significant and interesting ways. This is a job for the command pattern! 

In simple terms, this pattern separates the intent to do something from the action itself. So, instead of one system telling another system what to do directly, it sends a "Command" (like LaunchBubbleCommand or MatchCommand) to a central bus. Then, other systems listen for these commands and react accordingly. This, unsurprisingly, got quite complex, even for a game as simple as this! 

The process

The first major hurdle was actually starting the damn thing. I knew the goal, I vaguely knew how to do everything once I got it all up and running, but there were big steps in-between that were missing. So I did what I assume anyone else would do - I ignored it and built an awesome level editor ๐Ÿ˜‚ Randomly generated levels would take too much time to tweak into fun levels, it's quite difficult to make them "fun", so I figured I'd crack out a few by hand. 

Once the level editor was done though, I did need a way to test it in an actual game, so I did have to start the real work eventually. It's quite intimidating though, I needed a command bus to publish the commands, an event handler so the game could visually react to what was going on, a deck and a hand, the list just kept growing and it all seemed to depend on other things being done first. I couldn't see if the command bus was working without having some gameplay, but I needed the command bus to implement gameplay… then I wouldn't be able to see what was happening without the events, and seeing progress is the main way I get through a game jam! I bit the bullet and got started though, pulling in Reflex to handle dependency injection because referencing all of the different parts was proving to be a bit of a nightmare. 

I ended up with 7 commands: start level, draw, select bubble, launch bubble, find matches, discard hand, end turn. There were 9 events to go along with those to update the game state visually, and each one had an associated system that actually implemented the game logic. That's a lot of files! It was actually a lot easier to manage than I expected, though, if anything needed to change I knew exactly where to go. This high "Separation of Concerns" meant any bugs that popped up got nailed very quickly as they could only be coming from one place, leading to a submission that has zero bugs! Everything works exactly as intended, which is cool. 

Optimisation 

I'm a big optimisation nerd, so I figured I would give a rundown of what I did to get the game running quickly on WebGL (except for the one hiccup which I'll talk about at the end!). As a PSA, this is all completely overkill for a game like this. If you have no idea what I'm talking about, don't worry about it ๐Ÿ˜

First, I don't really use any MonoBehaviour classes in Unity. Each bubble is renderer directly to the screen - currently using the immediate mode drawer from the Shapes asset, but in future this will be using unity's Graphics.RenderMeshIndirect to do them all with a single draw call! MonoBehaviour in Unity is a huge, bloated "everything" class that has a ton of overhead! I don't actually need any of that functionality, so beyond a single entry point (my "Bootstrap" script) and a couple of display classes - and even some of those were just me being lazy.

Next up: allocations. This is typically anywhere you'd use new in C#, you create a space in memory and when it goes out of scope, that space gets marked as unused. C# is nice and friendly for programmers, you don't have to worry about memory management because we have something called a Garbage Collector(GC)! However, on WebGL the GC runs at the end of the frame and if it has work to do it will cause a bit of stutter. In web games, if you want everything to run nice and smoothly, you want to avoid any new memory allocations during gameplay. To do this, you can create objects when the game loads and re-use them! In this game, I have a lot of collections (Lists, Queues, etc) - so the main way to save allocations here is to do new List<BubbleInstance>(512) during an initialisation step. This creates a new list that has a capacity in this case of 512 bubbles, and doesn't need to grow until you add more. The astute amongst you might realise that this is actually just Object Pooling! It's useful beyond the normal use case of enabling/disabling GameObjects.

I also use none of Unity's built in physics for this project! I strip out a big chunk of the built in functionality via the package manager and web stripping tool (which is a package you can use to remove unused bits of the engine). Instead, I wrote my own (allocation-free) system that steps through intended bubble positions until it hits an occupied grid space, then goes back a step to fill the one it just passed through. This is faster than PhysX and Box2D (the physics systems Unity includes) because I can make certain assumptions about the game that larger systems like that can't. E.g. every collision is circle vs circle, which is the most optimal collision type!

The result

As you are no doubt already aware, I totally ran out of time, with one hour of dev time left I still needed a scoring system, a level, a way for the player to lose, and a win state. I was ready to give up, but a few people in the Discord server made me realise it was only a jam - I don't need multiple levels, win state can just be "there's no more content" (which some people did comment on, thinking it was a bug), and score didn't have to be complex! I buckled down and got it done in time, with 10 minutes to spare thanks to a generous 4 hour deadline extension (thanks Goedware!). Because I'd spent so long on the architecture, it went from "not another bubble shooter" (there's a cool twist!) to "not another bubble shooter" (oh no not another one), but I've already had a bunch of great feedback, and there is a positive vibe of "this is satisfying" and a wave of nostalgia for this sort of game, even if there was a lot of feedback that mentioned the lack of content! This is a good sign, definitely signals that it will be a cool project once there is content and it actually gets to the vision I have in mind. 

There was one problem though. I do development on an expensive M4 MacBook. Not everyone has a device like that! I like to think of myself as a big optimisation nerd, so the game is very small (8mb, loads very quickly for a Unity game) and very fast - bubbles are rendered without GameObjects, I don't use any built in physics, and I made a big effort to avoid allocations because the garbage collector can cause lag spikes that you don't get any control over. I knew that. But towards the end of the jam, I wrote the match-3 popping logic and the system that drops any detached bubbles using… Linq ๐Ÿ˜ญ I was lazy and caused a minimum of 400kb of allocations in that one command. That led to a noticeable stutter on some devices in the jam version when it was collected at the end of the next frame - which just so happens to be when the mice satisfying animation plays, which makes it even more noticeable. I had no idea this happened, because on my machine it was fine… 16ms per frame is the target for 60fps, rendering takes 5ms, the game loop takes 0.2ms, that gives me 10ms per frame to play with for the heavy calculations, which is loads of time. My machine being more powerful gave me a false sense of security, and my "it'll be fine for a jam" quick implementation of the match 3 algorithm really ruined what was otherwise a very lean and fast game, and it is deeply shameful. 

Post Jam I fixed that, of course, with a new version that removes the allocations entirely and splits the match and drop code across two frames, so that should fix it for most devices. Testing just the removal of allocations seemed to work for most people, but one player did still have a bit of stutter! 

Overall then, I consider it a success. I've got the skeleton of a project I'm actually really excited about, and a bit of feedback about the main gameplay to mull over - it's a bit annoying to have to mouse down to the bottom of the screen to select a bubble, then back to the top of the screen to launch it, and repeat. Letting the player see the mouse position and choose which bubble to launch trivialises the bubble shooter gameplay, but special bubbles and more difficult levels should go some ways to addressing that. 100% worth pushing through to the end and submitting, as it always is with game jams!

Now, back to playing and rating the rest of your games. 

Files

bubble.zip Play in browser
11 days ago

Leave a comment

Log in with itch.io to leave a comment.