Work Posmortem: Bubble Reef ― Consider Refactoring

This is a postmortem written about a game I created for ADX Labs, LLC under their GameSmart brand as their employee.

After completing my first game with GameSmart, it was time to move on to the next one shortly after.  I was given domain to choose my next game now that I had familiarized myself with the tools.  I decided to stick with single-control, simple puzzle games as those tend to do well on mobile.

I had envisioned making a game similar to the Bubble Breaker puzzle game found on early PocketPC PDAs back in the mid 2000s, but themed to an aquarium where the bubbles are represented by clear air bubbles with colorful, tropical fish inside instead of colored marbles or simple spheres.  Tapping or clicking on a bubble with one or more like colors surrounding it pops all of the adjacent like-colored bubbles.  When the bubbles popped, the fish would be free and swim away.  If a column is fully cleared, all bubbles to the right of that column shift left until they can’t shift further.  The game ends when the player runs out of valid moves.  Simple enough!

The Bubble Breaker game as I remember it from Windows Mobile 2003.

Or so I thought.  What would end up being the longest game testing phase I have ever been through was a result of not knowing when to refactor broken implementation, and another hard-learned lesson!


Learning GML’s Quirks

Developing a game should always be about creating a strong, good-feeling core, and then embellishing or fleshing that core out as needed.  As I had a tendency to feature creep at the time, this was a good exercise to make sure I created a strong core that I knew, from prior experience, was fun enough for what was needed and could be elaborated upon.  I set out to create a grid handler.  I also forced the game to portrait mode as, from past experience, the game was far more suited for taller rather than wider playing fields.

Despite my experience with SlideCity, I still had more to learn about the way GML handled things.  I set out to learn as much as I could by doing, rather than just reading.

A bit of technical background (and, admittedly, a bit of complaining) on Game Maker follows.

First of all, forget everything you know about object-orientedness, variable scope, and variable typing that you learned with C# or Java, or even JavaScript/ES6.  Game Maker handles objects similarly to GameObjects in Unity, and when put into a scene (“room” in GM terminology) they are compartmentalized instances.  Seems sane so far.  However, there are no member functions or methods on these objects.  Everything in GM is event-driven, making GML, the scripting language used in GM, very imperative and more like a categorical todo list than a constructor, methods, and properties-utilizing run-of-the-mill object.  Here is where it gets a little messed up.

  1. Objects have inheritance, but instead of overriding methods/scripts, you override events. Since snippets of code are attached to events directly, if you have, say an objPowerup base object class, and then extend objRocket from objPowerup, and objRocket has code in its Step (execute every frame) event, you’ll override whatever objRocket was doing! This includes variable declarations, which can cause unexpected unknown reference errors.  It does not tell you this until you find out the hard way–not a flaw, just a gotcha for newcomers.  You can remedy this by calling event_inherited(), similar to super() in constructors in C# or Java or ES6.
  2. Calling a script is essentially includeing it as in other languages, as far as scope is concerned.  Consider that in C, #include tells the compiler to look for the header file named file.h, and essentially insert its contents at the point where the #include takes place.  This is how Game Maker’s scripts handle scope.  If you call a script* from an object’s code, the script’s execution essentially pretends as if it’s taking place at the point at which the script is called.  This means the script has access to all of the variables on the object that called it.  For example, if I have this code on an object:
    // do stuff
    var foo = 12;
    if (bar > 10) SomeScript();
    

    and then I have this code in SomeScript.gml:

    show_message(foo);

    an alert box will appear with the number 12 in it.  No need to pass things to parameters.  Great!  Unless you have a for loop with the iterator named i, and you call a script within it that also has a for loop with an iterator named i.  You’ll increment it twice and end up with an out-of-bounds error and a very hair-pulling bug.  This is a double-edged sword and took me a while to realize.

    You can sidestep these scope issues by using the var qualifier before your variable names to make them local to the script.

    * A script is a separate little snippet of code kept away from an Object, which is a thing that can exist in a scene (room).  Scripts are usually called like functions and can have a maximum of 16 parameters, and stored totally exclusive of objects until they are called.  All scripts exist in the global context and can be called anywhere.

  3. There are some awesome shortcuts that help you get around.  In Unity, if you want to take all objects in the scene of a particular type, you’d have to use:
    MyComponent[] components = FindObjectsOfType(typeof(MyComponent)) as MyComponent[];
    foreach (MyComponent c in components) {
        // ... do stuff here ...
    }
    

    With GML, you have a convenience called with:

    with (objBubble) {
        // ... you are now iterating over all objBubble objects that are active
        // do stuff here
    }

    This is undoubtedly the one feature I loved in Game Maker.  That, and the fact that you can use scripts as delegates, like in C#, or function pointers, like in C. The rest, though… not so much. Let’s keep looking.

  • You can’t initialize arrays the way you learned in school.  You want an array of numbers? Let’s initialize that in C#:
    int[] myArray = new int[5];

    Now let’s do it in GML:

    var myArray = 0; // initial definition
    myArray[0] = 0; 
    myArray[1] = 0; 
    myArray[2] = 0; 
    myArray[3] = 0; 
    myArray[4] = 0;

    Yes, that’s the canon way of doing it.  The forum post on it is particularly hilarious.

    “Can I initialize an array in one line?” – user
    “No.” – YYG developer

    You can make a script that acts like PHP’s array() function, but that’s a pretty big thing to miss from a language that imitates C syntax.  Or you can do even crazier things, like write a string parser that will automatically decipher and sort your array contents, but that is just asking for bugs.

  • Basic C-like syntax that most all languages have need not apply.  The above is a particularly egregious example, but let’s look at some smaller ones that will trip you up.
    1. It’s array_length_1d(arrayName), not arrayName.length.  GML is not inherently object oriented, no matter how you twist it, and everything is either a pointer (reference?) or plain ol’ data.  It’s imperative and functional pretending to be object-oriented–a loosely typed C-like language with some object-oriented constructs duct taped to the top.
    2. print()printf()?  No, it’s the more verbose show_debug_message().  A lot of function names are needlessly verbose, which comes down to preference, but if you’re like me and you find yourself print debugging regularly to self-check your code, it gets frustrating.
    3. Ternary operator: Want to set something based a single condition?  Let’s do it in C:
      set_color(isAlive ? c_green : c_red);

      Now let’s do it in GML:

      if (isAlive) {
      	draw_set_color(c_green);
      } else {
      	draw_set_color(c_red);
      }

      Admittedly, some may like this better because ternaries are a bit hard to read if you don’t use them regularly.  I, however, like being able to succinctly one-line brief checks such as the above when needed.

Now, please note, I’m not writing this to post to rag on Game Maker–just making note of the many things that tripped me up and resulted in further slowdown on development.  It’s a perfectly competent and powerful tool, as many have shown, it just isn’t for me as far as language conventions go.  The above things are just a handful of the stuff that tripped me up coming from Unityland.


Core Established: Mostly

I had, at this point, spent a few weeks working on the game, and I had the grid laid out, bubbles popping, bubbles falling, but I hadn’t quite gotten columns moving over to the left.  Nevertheless, I went ahead and wrote the ‘endgame’ condition which looks for no remaining moves (i.e. no like-colored bubbles touching), just to have a full cycle of some kind implemented.  I was satisfied with this, but this was likely the most satisfaction I was going to get out of the game for a while.

An example of the bubbles in my test case, spitting out their grid positions in world-space.

Sometimes, the bubbles would snag on one another.  They wouldn’t fall, they wouldn’t slide down or left when needed.  I would find myself checking collisions from both involved parties, having them agree that a collision occurred, and then doing the same thing a frame later to try and catch latent or unexpected collisions.  This proved to be an overkill, overengineered solution, because there were–at this point–many functions I didn’t know existed that would have been instrumental in simplifying things.

An unfortunate case where column #4 thinks it’s fallen all the way to the bottom, but it hasn’t.  Hitboxes are shown in faded red.

Bugs such as that plagued the game.  Eventually I got so thorough and overengineered with my checking and re-checking and auditing that the bubble collision check code exceeded 300 lines.  Three hundred!  That’s excessive for a simple AABB check.  I just assumed that was how Game Maker was going to be.  Obviously, I learned otherwise later on, but at the time, I was still relatively fresh to the engine, having only one prior published title with it.


QA Hell

Finally, it was time to start testing the game, where it was stable enough to push to QA.  The test results came back with variants of the premature collision bug over and over and over.  I could never get it worked out.  I even tried the brute force method (pseudocode as follows):

  1. Iterate over bubbles currently tagged “dirty” (in motion)
  2. For each bubble in the scene, check its collision against the bubbles tagged dirty
  3. For each bubble tagged dirty, check it against all other bubbles tagged dirty that are also no longer in motion
  4. In either case, if a collision is detected, tag that bubble as no longer in motion
  5. If a bubble falls below the ‘floor’ threshold, revert it back above the threshold and also rewind all prior movements for bubbles above it

To the uninitiated, this may just seem overly complex, but for those that do understand collision detect, you’ll notice that not only is this overcomplicated, but 1) it’s slow, 2) it’s checking variants of the same condition more than once, and 3) it’s ripe for race conditions.

All three of those vices contributed to the premature collision bug.  With my implementation of the game grid, it was unavoidable and would require a refactor–but at the time, I didn’t want to admit this!  I thought surely there was just an issue with my own implementation!  Surely I’m missing a detail.

Testing continued for three weeks in this state.  Self-doubt was setting in.  I would go home disliking what I’d worked on for the day.  The QA guy was getting quite tired of working on such a simple game with such fundamental flaws.  Eventually, an intervention occurred (in a positive way, mind you, not a reprimand).


The Intervention

My supervisor took notice that I was floundering with this code, and knew something was up.  I wasn’t sure where to ask for help because I was afraid of the inevitable: the grid implementation being borked beyond repair.  He coaxed me into asking for help from the senior web developer, who had ten years more experience than I did.  The game was still juuuust stable enough but the bug would still crop up intermittently, randomly, unexpectedly.  I left him with it overnight, somewhat relieved to see him playing it (and enjoying it, even in its buggy state) significantly as I walked out.  At least the design was sane enough to be fun!

I came back the following Monday, and to no (dreaded) surprise, he said the code was overengineered in a lot of places.  Instead of emburdening me with the task, he took about three to four days to himself to just re-build the code, and for that I’m infinitely thankful–donuts and coffee were bought for all participants afterwards, my treat. At the end he came back with a better grid implementation:

  1. Bubble objects act independently of one another, but answer solely to a grid controller that manages their position and spacing.
  2. Collision code is used only to keep them from overlapping, nothing else.  The grid controller handles the movement and matching.
  3. Everything was moved into script delegates, to compartmentalize code and make debugging faster.
  4. Everything was exposed as an adjustable parameter in the global namespace–usually an unhealthy practice in most other programming languages, but an absolute must for a game needing constant access to certain data.  Besides, the game wasn’t multithreaded, synchronization issues wouldn’t happen if race conditions are taken care of.

He did avoid doing a few things, namely making the “flex grid” system that I had before, which is to say the grid could be arbitrarily resized to any number of rows and columns–I had to implement that myself later, although this was easy and took maybe a day or two to get stable enough, regarding scaling, collision detection, and playability.

An example of the flex grid, where Expert difficulty (left) is a 10×12 grid, vs Easy difficulty (right) being a 9×10. Scaling is fully dynamic.

With this, I was able to complete polishing certain features, such as powerups, in-app purchases, and a tutorial.  The game was playable and fun, for once.

In this state, the game took only four days to test out of QA and be production-ready.


The Aftermath

Even after the game released, there was still stuff to be done.  Because I was so behind schedule, we opted to release the Classic game mode in which your goal is to clear the board as much as possible to score points, and lock off the Survival mode, which is identical to Classic except that any remaining bubbles that go unpopped solidified into fossils, and the board refills with fresh bubbles, where the player must clear the board while working around the fossils.  Powerups earned by making moves in the game can be used to remove seashells, and IAPs can be used to buy powerups as well–but they always come for free, naturally, by playing, and always drop every 10 moves at maximum, making players who rely on playing for free just as competitive if they are skilled enough.

A debug view of Survival mode earlier in development.

I took the time to develop the Survival mode, which actually took only about a week to get right.  Another week of QAing passed, and Survival was released.

What I Learned

At the time of writing, Bubble Reef is my highest rated game published at GameSmart, ranking a 4.8 out of 5 stars user rating out of 771 unique reviews and over 2.2 million plays.  To that end, despite its turbulent development spanning 3 and a half months (including post-release development), it is still a mobile-centric game that I am quite proud of.

Ultimately, what it comes down to is that you can’t be afraid to refactor important code.  No matter how integral or important it is, if you get to QAing and the entire game falls apart, you can’t ignore refactoring as an option.  Above all, I learned that the time wasted trying to polish a turd (bad code) is worse than the time spent making a quality framework.  Never conflate the two, and always look to see how you can improve the game–even if it means starting over in an important spot.