Tuesday, March 10, 2020

Everything in GameMaker is an Index

GameMaker as an engine has a lot of positives. It's friendly to start out with, it has surprisingly good multi-platform support, and it has nice pixel-perfect rendering for 2D titles.  But it's got some real down-sides too, including the fact that GML, its scripting language, seems very organically grown. It has the loosest syntax rules of anything I've ever seen -- Want brackets?  Want semicolons?  Want parentheses?  Up to you, it's all optional!  Typing is loose, though types do exist, and helper methods exist for lists and dictionaries.  It doesn't have classes, only functions.

One of the less obvious quirks of GameMaker doesn't become apparent until you work with it for a while, though, and that's what I wanted to discuss in this article.  This is the fact that everything, under the hood, is an index.

We recently tracked down a bug in one of the projects we've been working on, and the summary was this: Sometimes when you destroy a barrel during gameplay, it'll crash.  There's a error message to go along with this indicating it's inside an Achievements related script, and the message simply says that the function expected a number.

Understand that 'sometimes' here is more like 'rarely'.  You can go destroy these things dozens of times without a problem, and in fact we had so much difficulty reproducing this that we'd basically given it up as just one of those things you end up letting slide.  We'd put in safety checks on all the achievement related methods making sure the arguments were typed correctly, but really there weren't that many inputs and it made no sense to us that a non-number was slipping into the list of achievement values.

Until I finally happened to catch the crash in a debugger.  Upon inspecting the list of achievements to see what non-number was present, I was surprised to discover that instead the list of achievements had transformed into a completely different list.  This list was shorter than the actual list of achievements, and was full of numbers ranging from single-digits up to around 250.  The not-a-number crash was the achievement save process running off the end of the too-short list and ending up with null values -- GameMaker doesn't do overrun checking.  Tracking the cause of this problem took all day, and included steps like writing wrappers for GameMaker's add-to-list methods that would log what object is doing the addition, writing detectors to report if the achievements array's length ever changed, careful review (again) of everything interacting with achievements at all, tons of gameplay trying to recreate the problem, etc.

In the end, here's what happens:

The player shoots a barrel.  The barrel shoots off in a direction, plays a particle effect and sound, and explodes.  When it does this it adds the particle effect to a list of effects called particle_list, and these get cleaned up later.  Sounds good, until you look at other uses of particle_list and realize that it's not a list, it's actually a dictionary.

But that's the thing, particle_list isn't a dictionary either, it's an INDEX to a dictionary.  It's pointing at, say, dictionary #7.  And when you call the AddToList method and tell it to add something to particle_list, it has no idea what you should have used AddToDictionary instead.  It goes and dutifully adds your value into list #7, and now you've edited an unknown list.

So, the barrel adds that particle effect to a list at particle_list's index, instead of a dictionary at particle_list's index.  Sometimes this accidentally-selected list is something harmless, sometimes not, but in the rare case of our crash it just happened to get added to a list of other lists, and that particular list-of-lists is used to do cleanup at the end of the level.

Now, understand that everything in GameMaker is an index.  This includes references to assets, references to instances of objects in the scene, references to the environment you're in, etc.  So adding the particle effect to that list of lists is just adding another index to a list of indexes.  The game doesn't have a ton of particle effects, so often the index for the in-world particle effect was a very low number.  Sometimes, that index was 1.

Upon launching the game, one of the first things it does is initialize the list of achievements and assign it to global.AchievementList.  This means that under the hood, the achievements list always had a very low index, in fact it was always 1 in my testing.

So, through coincidence of the particle effect's index, and through coincidence of the particle_list's current index, that barrel had just added the index of the achievements list to the list of lists to get cleaned up.  For the time being this is harmless, at least in the sense that you just end up with a done-playing particle effect being left in the scene.  However, eventually the player will finish the level, the cleanup script will run, and now the achievements list has been deleted.

Upon entering the next level, the game builds a series of lists for things like which enemies are allowed to spawn, which weapons can spawn, and so on.  Since the slot the achievements list had formerly occupied is now vacant, one of these lists ends up using its space.  As the global.AchievementList variable is just an index, it has no idea about any of this, it remains happily pointing at the now-replaced slot #1.  This is how the achievements list had been replaced with a different one.  The next time the player progresses any achievement and the game tries to write out the progress, it will crash when it overruns the now-incorrect list.  And, sometimes, it maybe won't crash because the list is long enough.

Something to understand here is that this bug can do so many things that don't result in a crash.  All of the various indices that GameMaker uses start their count at zero, so it's not really a stretch that the index of a particle effect is also a valid index for a list.  There will also be a sprite #1, an object #1, a sound #1, a dictionary #1, and so on.  Almost any erroneous mis-match of an index could technically be valid and so this bug remained silent for a very long time.  At times it likely resulted in things like incorrect objects being spawned into the level, or incorrect rewards being given.  All manner of "I saw a weird thing but it never happened again" bugs could come from this simple accidental usage of AddToList instead of AddToDictionary.

Beyond that, this is an easy error to make (and the variable name didn't help things, here).  It was also time consuming to diagnose, very disproportionate to how easily someone could make the mistake.  If you have a suspicion this is a problem, you'd need to manually audit usage of all your lists/dictionaries to confirm things were being used correctly.  Or create a wrapper for the addition functions that give you a breadcrumb trail, and begin tracking down the histories of usage, like we did.

This was an interesting issue to track down and something that really could only happen in GameMaker in my experience, and seemed worth explaining.  Hopefully it reminds any GameMaker users out there to be careful and be aware of the possibilities of this kind of mis-match.

2 comments:

  1. That sounds awful. Why are you using GM if you have to deal with stuff like that?

    ReplyDelete
    Replies
    1. People make big complex stuff in GameMaker. It's kind of interesting because it does legitimately start out easier than a lot of other engines, but by the time you've got a large project it's transitioned into hard mode in my opinion. Mostly because it doesn't have a lot of the safety barriers that you get with something like Unity. Things like access controls and type checking make a big difference, long term.

      Delete