Maximizing Stability in GameMaker Studio 2

gabe
9 min readNov 16, 2020
This screen may cause you fear, but in time it will cause you delight! …maybe

While GameMaker Studio 2 has a number of unique qualities that distinguish it from other engines, one of the largest is that its language, GML, is dynamically typed. While this lends well to its prowess in rapid iteration and its friendly learning curve, this does come with a few considerable drawbacks.

Many mistakenly classify the difference between dynamic and static as “static has types, dynamic does not”. In reality, both have types, the predominant difference is when the type checking occurs. In a static language, type checking happens at compile time — or in other words, you will immediately be warned that you’ve made a type-error when you try to build your game. In a dynamic language, this check happens at runtime, meaning the error is not found nor reported until it actually occurs in-game.

For most games, type-related mistakes are going to take up the vast bulk of the bugs and errors your game runs into. The deferred-checking places a lot of responsibility in our hands while making games in a professional environment. Any game can be sunk by an unstable launch, and it’s difficult for us to detect all the possible bugs by ourselves. After all, the game will be played by (hopefully) countless players on a wide variety of hardware, stress testing the game in a way the developers rarely can do even with QA, which is a resource not many indies have access to (I’ll be referring to QA a lot in this article, but the advice still applies just fine if your “QA department” is just you!).

Errors Are Your Friends!

Most developers don’t view errors in a positive light. After all, they represent our mistakes and frustrations, appearing only when things are not going our way. We develop a natural tendency to avoid them as best we can, and measure our success in how long our game can run without being forced to close.

GameMaker itself contributes to this mentality, both in its design of being dynamically typed, and in specific choices of how its in-built functions operate. A great number of functions (as well as all the data structure getters) will return a default value if nothing is found. These default values can be undefined, noone, -1, or even 0 in some cases. The lack of consistency is… a topic for another day, but the primary point is that, often, functions can communicate more than one thing: the data they have successfully retrieved, or another value meant to tell you “this function call failed”.

This has its uses, but poses a threat to our stability. Let’s pretend that I am writing a function that should remove the first item in my player’s inventory, which is stored in a list.

This code will work in most cases but has a vulnerability. If our inventory is empty, then we’re going to fail to actually remove and return an item. Line 2 will assign undefined to item, and line 3 will do nothing at all.

This is where this “dual communication” can hurt us. Because GameMaker doesn’t crash when we get the invalid index (nor when we try to remove it), this is what our code actually is written to do.

This is definitely not what we wanted! If we are using this function somewhere in our codebase, we are expecting an item to be returned no matter what. This function is simple and (potentially) widely used, it shouldn’t come with the burden of possible failure, much less a failure that we have to remember. Any system written to rely on human memory is a system waiting to explode.

So, what should we do?

The Bad Option

Many users will try to usher the code back into a stable state in a situation like this, and I’ve certainly been guilty of the same thing. For a while, in a system built to manage a room’s grid, I would clamp the x / y used to read information out of the grid. This meant that if I accidently tried to read the data in cell [-1, 0], I would instead be given [0, 0]. Sure, my game won’t crash now, but this is a false victory. The answer to the question of “what’s at [-1, 0]" isn’t the data at [0, 0], it’s “nothing you moron, that’s illegal”.

I’ve illustrated this “bad option” above — instead of returning undefined, I’m now returning Item.Wood. The game won’t crash now — instead, we’ll just always give back a piece of wood if the inventory is empty. This is an exaggerated example, so I probably don’t have to explain why this is terrible. But the reason its so bad is parallel to any other situation!

By returning valid but incorrect information, I’m allowing the game to continue along in the present moment. However, eventually this data will get used, and since its not correct, one of three things can happen:

  1. The player will see a visual bug of some sort, and you’ll have little clue as to how or why it happened
  2. The game will crash due to invalid data, and you’ll have no way to know when the data got invalidated
  3. The data will slip into the users save data and thus corrupt it, making them lose all the hours they’ve invested into your game (and they won’t be happy)

The Good Option

In these circumstances, the bug has already occurred — anything we do to try to make the situation better will merely delay the inevitable. By crashing early, we can show ourselves exactly where the true mistake occurred and can prevent things from getting any worse.

This mentality of “crash fast and crash hard” can (and often should) be applied in many places! Errors can function not just as redundancy, but also a more truthful documentation of your code. After all, there’s nothing stopping a comment in your code from being completely incorrect or outdated. Errors, on the other hand, are real logic, and are much more likely to give a reader an accurate understanding of what path the code is following.

Consider the Impossible

Another issue that can easily seep into GameMaker games are “impossible errors”. I consider there to be two primary types of errors:

  1. Standard Errors: Errors the game could feasibly run into due to an oversight by the user — like in my example above, where the user could accidently call remove_first_item_from_inventory() while the inventory is empty.
  2. Impossible Errors: Errors that, under the assumption that the game has been prepared and built as intended, cannot ever occur.

This can be a bit of a blurry line in GameMaker, so let’s look at an example of an impossible error.

Can you guess which game is responsible for this article taking me ages to write?

In god_to_name() we can interpret the default case as impossible since there is only one member of the God enum, and we have covered it in the God.Zeus case.

This is obviously a bit of a lie though — for one, this is a dynamically typed language, so I can pass whatever I like into god_to_name() and immediately trigger that default case. Similarly, everyone knows that there are far more Greek Gods than just Zeus, and I’m likely to add more as development continues.

As it stands, this function will return undefined and we will end up in a similar situation that we were in earlier. We once again have the opportunity to crash fast and crash hard.

Now that I’ve added a crash in the default case, my addition of Hades on line 3 will cause my game to crash much faster to tell me that I have forgotten to update a section of the code that now needs a new addition. This error will make it much more likely for me to spot my mistake instantly when testing my game — not months later when it’s in a user’s hands.

Generally speaking, your game is just one giant state machine, with far more states than you are intending it to have. By writing in lots of crashes you are adding explicit definitions for states that would previously be undefined.

Long Term Benefits with Config Macros

One may assume that the result of writing with a crash-first style would result in a game that has fewer bugs but far more crashes, but this is not the case (I promise I wouldn’t be recommending it were that true).

We spend a lot of time making our games, and (ideally) a good chunk of time testing them as well. These crashes won’t reach our users — they’ll reach us! By making our errors more visible and clear, we’re making QA’s job easier, and making them far more effective.

That said, throwing errors all over the place may make developing the game a bit of a chore. After all, our game sits in a half-baked state for most of its life, and countless errors we write may be temporarily acceptable, but never appropriate for a shipped build.

We have three main categories of builds we want to be able to make:

Debug Builds

These are what we will use 99% of the time. In Debug mode we can allow for certain situations that are not legal in a deployed product. We also enable our debug tools that are vital for development, but not designed for users to use.

Testing Builds

These builds are intended to simulate a shipped build, but include whatever debug tools QA will need to properly test the game. Additionally, we may have some crashes that only fire in Testing. For example, let’s say we have a music system that selects a random track to play. Early in development when our music hasn’t been made yet, this system might often fail to find a “suitable” track. For the sake of not constantly crashing for an error we can’t currently fix, we can instead default to playing no track in our Debug and Ship Builds. However, in Testing, we can crash to make it abundantly clear that the issue exists. Testing is where we want to find bugs, even the small ones — they won’t be getting in the way of our work there.

Ship Builds

Finally, Ship builds are for the actual builds we deploy. They have all debug tools off and whatever other specific preparations are needed.

Config Macros

Using the configuration editor in GMS2 we can create our three separate build configs.

2.3’s config editor. Yes, they all have to derive from Default — not sure why.

Adding configuration macros is simple. First, let’s define three macros that tell us which build config we’re in.

Now, to tie them into our configs, we add the following syntax:

Now we can always check which mode we’re in at runtime!

A quick note: this is superior than simply adjusting a variable. Besides being a slightly more stable way to manage things, macros let us take advantage of dead code elimination in the YYC. Put simply, dead code elimination is the compiler’s ability to remove parts of our code that are impossible to run.

For example, if we are in a Testing build, all our DEBUG macros literally translate to false. This means that all of our debug code will not be present in the final exe whatsoever. This gives us a speed boost, but also notably would make it quite harder (if at all possible) for a user to modify the game so that your debug tools appear, if you care about that. That said, this isn’t a perfect science, I’d discourage you from storing your social security number in your source code. It also should be noted that this only pertains to YYC — not VM builds.

That’s All For Now!

Thanks for reading through the article, I hope it was helpful! Feel free to @ me on Twitter to discuss, and to let me know what topics you’d like me to talk about in the future!

…and here is the thumbnail, because apparently Medium requires it to be in the actual article. Woo!

--

--

gabe

aka lazyeye • game developer • fields of mistria, forager, infinite guitars