Skip to content
Skip to content

If this post is helpful for you, please help LTPF out by subscribing to Levelling The Playing Field & receive new posts directly to your mailbox!

Subscribe to & support LTPF!

Nuclear Throne Daily Runs & Why Assumptions Are Bad Programming

tha_rami
tha_rami
8 min read
Nuclear Throne Daily Runs & Why Assumptions Are Bad Programming

Yesterday, we launched one of the most important Nuclear Throne updates yet. In Update #55, we introduced Daily Runs, a challenge every player can take once a day in a pre-generated level. This way, players can compete with players around the world on the same level, but only get one shot at getting a high score. Obviously, this adds a lot of replayability and challenge to the game for those who enjoy that kind of challenge, without changing the game for those that don’t like to compete with others.

Sadly, there was a problem: for many players, the HUD just kind of disappeared halfway through the game. That doesn’t make for fair competition.

For all of you that think of going into programming, the lesson today is *never make assumptions*. I am obviously not the main programmer on Nuclear Throne, so for me a lot of the code, structure and specifics are not as natural as they are to J.W. Since J.W. was mostly busy today, I decided to take a shot at hotfixing the issue.

The Issue
The core issue was that the HUD was disappearing at random moments, without any clear indication as to why that happened. When we started hunting the bug, the first thing we focused on was trying to create a reproducable method of triggering the bug. Using the developer cheats, we’d try walking around in different worlds and the like. The only way we could get the bug to happen frequently was the ‘trailer cheat’. It was originally created for our trailer creator, Kert Gartner, who needed a way to record video while not being quite amazing at Nuclear Throne. Not only does it bring you to the next world, it spawns a lot of radiation, a big weapon chest and some other stuff – so you immediately look badass for the trailer recording.

The Pause Button
We concluded that one of the ways the bug could be triggered was by pausing the game during the end of the level transition. That was a clue – from what we could tell, the bug would only occur while transitioning between levels. We had something to chase.

So we focused on was figuring out whether the code that draws the HUD was still being executed after the bug.

In Nuclear Throne, the HUD is drawn by two functions: scrDrawHUD, which draws popup text and pickup prompts, which then also calls scrDrawPlayerHUD for each active player. scrDrawPlayerHUD then draws the HUD for the player with the player number provided – number 1 for player 1 and 2 for player 2. We checked in scrDrawHUD, and realised quickly that the HUD was still being drawn. We shifted focus: what if, despite the HUD being drawn, the player HUD was not being drawn? We went back, logged gameplay and realised that this was indeed the case. If we had just been more specific, logging scrDrawPlayerHUD instead of scrDrawHUD, we would’ve noticed that right away. Bad assumption.

The code that calls scrDrawPlayerHUD first checks with the ‘main controller’ if the player exists. Our ‘main controller’ object holds all sorts of important information about the player, including what the ID of the player instance is. Game Maker assigns objects and instances an ID in chronological order (the first created object is ID 0, the 2nd is ID 1, and so on).

If the ID number was higher than 0, we could expect the object to have been created (and thus gameplay to have started properly). If the game is over and the player returns to the main menu, we reset the ‘main controller’ and the value gets set back to -1. Each frame, we check if the value for each player is 0 or higher – if it is, the player was created, so the game has started, and we draw the HUD.

In other words, it would be a good idea to keep track of the value of that player ID in the main controller. We set up a realtime log so we could see the value change while we played, and we started playing in windowed mode. When the bug happened again, we noticed two things: the game suddenly forced itself back to full screen, and the player ID value had reset to -1.

Figuring out why the game goes fullscreen.
Confused, we moved on to see if maybe the surfaces were broken or uneven: that might force the game back to fullscreen.

Surfaces are basically render targets. Normally when we render images, sprites or anything, we do that to a buffer that gets drawn to the screen at the end of the frame. Surfaces are basically a sort of imaginary piece of glass we can draw things on, and then we can later overlay those on the screen. Every shadow in the game is drawn to a surface, and the darkness in dark worlds are also drawn on a seperate surface. Since we’ve had a longstanding bug of the darkness disappearing when resizing windows, we thought maybe the solution could be found here.

The good news: when something changes about the window context – so toggling from full screen to windowed, resizing, turning on or off AA – the surfaces are immediately lost. Chances were that the problem was quite simply that the window was being modified, the surface lost and thus the HUD (and the darkness) no longer drawn. We started looking around, quickly found the bug that caused the darkness to stop being drawn and then searched for the HUD surface.

The problem was: there is no HUD surface. The HUD is being drawn directly to the screen, like most of gameplay. We were back to square one, and already hours underway. All we had learned was that the screen changed, and the player ID was reset to -1.

We searched for code that modified the player ID, and found a number of places where that happens. We painstakingly rewrote code, optimized things and fixed dozens of little problems, but we couldn’t find any place where the player ID was modified. What if, instead of being modified, the controller was being reset? That’d be odd, because it’d mean all information would be lost or modified mid-game.

Or would it? We checked and it turns out most information required to run the game is stored in the Player instance itself, rather than in the ‘main controller’. The controller is used only at the end of the game, and when the two got disconnected you wouldn’t notice it until after restarting – when suddenly you’d be playing a different character, your score was incorrect or your daily challenge run was uploaded incorrectly.

In other words, it could be reset. That’d also explain the screen resize – the main controller is the very first object created at game bootup and sets the screen context. It was time to confirm that the controller was being reset, so we caused the game to display an error each time the main controller was created anew. Within moments, it became clear that that was indeed the culprit: the main controller wasn’t being deleted, it was being overwritten with a new copy of itself.

Big Weapon Chests
We obviously couldn’t just remove or work around that code: it’s being used to initialise the game, and it’d be impossible to change it. We needed to find what caused it to be created anew in the first place, rather than fixing the code in the creation of the controller. Luckily, it’s not hard to find out what object causes something to happen.

The debugger gave us a quite unexpected result: the object that caused the controller to be created anew was the Big Weapon Chest.

So we checked the Big Weapon Chest. There wasn’t a lot of code in there, and we caused the game to throw errors every time it was created. We quickly realised that this was every time we cheated to the next level, and the bug would only occur once every few levels. We were clearly on the wrong track – the chest might be part of the problem, but it wasn’t causing the problem. While the Big Weapon Chest would always be around when the bug occurred, it would be created as part of the cheat without any problems.

But why would the Big Weapon Chest sometimes create a new controller and overwrite the old one? It didn’t make any sense, and since we were stuck anyway, we decided to dig a bit deeper.

This is where I went for dinner, which – because I’d been working all day tracing this bug – also counted as breakfast.

We started looking into where Big Weapon Chests were created instead. Obviously, they were created when we cheated ahead, and under certain in-game circumstances. Specifically, Big Weapon Chests cannot be spawned directly – a normal weapon chest is changed into a big one when certain conditions are fulfilled.

Since the timing of the problem was hard, we decided to step through the code instruction by instruction, seeing what would happen when a big chest was created. The problem was that the code that is normally run when an object is created wasn’t run when the Big Weapon Chest would coincide with the bug. That was another clue: the Big Weapon Chest wasn’t created normally.

The solution!
So we started looking at the one place where the Big Weapon Chest was created through a non-standard method: the scrPopulateChests function, which fills the level with all sorts of chests, cannisters and the like after generation is complete. In it, a weapon chest can be transformed into a big weapon chest using a function called instance_change().

We decided to start ‘stepping through’ the code line by line from the point where the game decides to replace a normal weapon chest with a Big Weapon Chest. The code we were stepping through was the following:

with WeaponChest
{
if random(4) < GameCont.nochest
   {
      curse = 0
      with instance_change(BigWeaponChest,false)
         event_perform(ev_create,0)
      exit;
   }
}

This code is not super complicated. It basically does this:

With the current WeaponChest,
{
   Roll a four-sided dice, and if the number is less than the amount of times players did not pick up a chest,
   {
      Uncurse this WeaponChest,
      Change it into a BigWeaponChest, deleting this chest directly instead of normally,
         Run the 'creation' event on the BigWeaponChest that we just changed from a WeaponChest,
      That's all!
   }
}

It ran through every of those lines perfectly, but at the event_perform(ev_create, 0) object, something odd happened. Instead of executing the ‘creation’ code of a BigWeaponChest, it executed the ‘creation’ code of our main controller. It reset all the values to their default values as they are in the main menu, and thus also our player ID to -1. That’s why the HUD wasn’t being drawn anymore.

But why did it do that? A quick scouring of the Game Maker documentation and a chat with Michael Dailly from YoYoGames provided the answer.

Unlike instance_create(), which creates an instance, instance_change() command doesn’t return a reference to the object created. That means that while:

with instance_create(BigWeaponChest)

will allow you to both create and modify a new BigWeaponChest,

with instance_change(BigWeaponChest)

should technically give an error and fail.

Instead, instance_change() always returns a value of zero. So, instead of what we thought the code did, this is what it actually does:

With the current WeaponChest,
{
   Roll a four-sided dice, and if the number is less than the amount of times players did not pick up a chest,
   {
      Uncurse this WeaponChest,
      Change it into a BigWeaponChest, deleting this chest directly instead of normally,
         Run the 'creation' event on the object that is stored in Object ID 0,
      That's all!
   }
}

Object ID 0 is the first object that is created in the game. In our case, as mentioned before, that’s our ‘main controller’. The BigWeaponChest reset all the values to the base values because we misunderstood the subtle difference between instance_create(), which we usually use, and instance_change(). Instead of creating a Big Weapon Chest, we were creating a new ‘main controller’!

The solution was relatively simple: just remove the word ‘with’.

with WeaponChest
{
   if random(4) < GameCont.nochest
   {
      curse = 0
      instance_change(BigWeaponChest,false)
         event_perform(ev_create,0)
      exit;
   }
}

which translates to

With the current WeaponChest,
{
   Roll a four-sided dice, and if the number is less than the amount of times players did not pick up a chest,
   {
      Uncurse this WeaponChest,
      Change it into a BigWeaponChest, deleting this chest directly instead of normally,
         Run the 'creation' event the current WeaponChest (which now happens to be a BigWeaponChest)
      That's all!
   }
}

Either way, this entire bug stemmed from a misunderstanding of how instance_change() works. We assumed it’d work the same way as instance_create() does, and it doesn’t. Well, there goes Wednesday – maybe I’ll try my hand at one of those Daily Runs myself.

Was this helpful?

Consider subscribing to Levelling The Playing Field! Every article will always be free-to-read, but subscribing helps me gauge interest in the effort & ensure that I'm using my limited time to help the most developers possible. After subscribing, you'll get every new post of game development advice delivered to your inbox as soon as it goes live. If you can afford it & want to support LTPF, please consider supporting the newsletter with a fully-optional Paid membership to help make useful industry knowledge available for free.

Subscribe to & support LTPF!

Related Posts

What is Ramadan?

As Ramadan is around the corner, I receive a lot of questions about the Muslim month of fasting on Twitter, Facebook and even in my mailbox, so I decided to re-publish this article with answers to some of the most commonly asked questions. Note that there are cultural differences between

What is Ramadan?

Far Cry 5 - Objective Review

After spending several days of work on painstakingly and objectively rating each of the 2674 objective specifications of Far Cry 5, I have finally reached a final and objective verdict for the game. Far Cry 5, objectively, gets a 7/10. For those who prefer the objectivity of a spreadsheet,

Far Cry 5 - Objective Review

Game Developers Conference 2018 - Thankfulness

I vividly remember the first time I took my first uncertain steps into Yerba Buena park almost a decade ago, looking for direction and guidance in the early steps of my industry career. I equally vividly remember arriving at San Francisco International Airport, dazed and confused from the long flight,

Game Developers Conference 2018 - Thankfulness