Originally posted on Shamus’ Good Robot Devblog
So now we’re to the part of the project where we have to stop adding Fun New Things and fix the dumb crap and insane bugs we accidentally created earlier. This is my least favorite part of the project. Or any project. The equivalent for an author is once they’re done writing a book and they have to go back and proofread. Bo-ring!
Here are some of the baffling conundrums we’ve unraveled over the last few weeks.
Bug #1: Mysterious Level Exit
Description:
You’re flying around the level, minding your own business murdering robots, when suddenly the Good Robot acts like it just went through a level-exit doorway. The robot flies to the edge of the room, the screen fades out, and you’re suddenly on the next level, despite the fact that you weren’t anywhere near a doorway. You couldn’t even SEE a doorway.
Why this problem SUCKED:
This bug was a phantom. It was the Loch Ness Monster. It was Bigfoot. Nobody could replicate it, nobody could predict it. It would happen once in a hundred game sessions, which meant it never happened when you were looking for it. It would pop up when you were in the middle of something else, and it was so abrupt that by the time you realized it was happening you’d already been pulled through to the next level and couldn’t remember anything specific about the game state pre-transition. What room was I in? What was going on? Was there a door in the same room? Were any robots near it? Did I stupidly blunder through some gap in the level geometry without noticing?
What happened:
This bug highlights the dangers of mixing “design through coding” with “part-time coding”. It’s fine if you grow your project organically instead of working from a spec, particularly when trying to nail down elusive ideas like “fun” and “comprehensible”. If playtesting reveals that guns don’t feel good to shoot, you don’t send a terse message to the design team telling them to write you a new specification, because:
1) Writing specs is time consuming.
2) YOU are the design team, so you’d be sending the email to yourself.
You just sit down and mess around with the mechanics until things feel right, and you send the resulting changes to the rest of the team to get their feedback. The good thing is that this makes your design is very adaptable and fluid. The bad thing is that the code is going to be kind of rough and ad-hoc.
You’ll end up implementing a feature a little at a time, and the final structure will not be as organized or as logical compared to a system where you knew exactly what you wanted beforehand. This problem will be compounded if you’re dividing your time between coding and other things[1] and the code structure keeps getting flushed out of your memory by other short-term tasks.
The game was originally one long, continuous level with no breaks or loading screens. That was super-monotonous, so we cut the gameworld into clearly-defined gameplay sections. When you get to the end of one section (called a zone) you’ll find between 1 and 3 exit doorways to choose from.
Originally, you just bumped into a doorway to open it. It would stand open for a second, but if you flew away then it would slide closed again. If you crossed the threshold of the door (which is a simple line on a 2D plane) and the door was open, then clearly you had just passed through the door. The game grabs control of your character and has them fly through the tunnel, leaving the camera behind while the screen fades out.
The threshold of the door is invisible, but you can think of it as an infinite line like so…
Note that when I say “crossed the threshold of the door”, I mean you passed from one side of the door to the other. Doors can face any of the four directions, depending on how the level is structured and where the final room ends up. The important thing is that doors always appear on the outer wall of the level.
Fine. That worked well enough. After a week or so we got some feedback: Testers pointed out that it felt kind of unsatisfying to have bullets ignore doors, and it felt dumb to slam face-first into a door to open it, bounce off, recover, and then pass through. It only took a split second, but the action was inherently annoying. It really did seem like you ought to be able to shoot a door to open it and then zip through unhindered. So that was added to may to-do list.
Then some weeks later I worked my way down to that point in my to-do list and made doors pop open when hit with player bullets. By that time I’d long since forgotten how the original door logic worked. I still had the old logic that crossing the threshold of an open doorway would pull you through, but now you could open them with bullets.
The logic was still, “If this door is open and the player crosses the threshold, then they must have gone through the door.”
This is incorrect logic, but this alone is not enough to trigger the bug. What needs to happen is this:
- If the player is using one of the small number of bouncy laser weapons in the game, and…
- those lasers happen to sail off-screen and into another room without running past their maximum number of bounces, and…
- they happen to end up in the last room where they hit a door, and…
- you happen to cross the threshold of the now-open door before it closes again.
I wouldn’t have designed the system with this goofy logic at the outset, but because the two halves were written weeks apart – with many other concerns and distractions in between – I ended up with this bug.
Bug #2: Help I Spawned Outside the Level!
Description:
The game should end if the player dies. However, if they have purchased a warranty at a vending machine, they will respawn in the first room of the current zone after death. (This consumes the warranty.) Very rarely, the player would instead respawn outside the level.
Why this problem sucked:
Actually, this one wasn’t too bad. It was rare, since it could only manifest when the player died while under warranty. But it was easy to find because my first guess was the correct one.
What happened:
Every zone begins with a circular room. In the middle of the room is a platform. This platform is intended to hold things like vending machines and the respawn station.
Those machines need to go on a flat surface. They look terrible if they’re floating out in the air. So when the zone is generated, it looks for a nice level spot in the first room, and when it finds one it puts the respawn machine there.
However, under certain circumstances that platform might not be level. The code intended to connect this circular room to the next room was supposed to do so by digging a tunnel. If it needed to dig straight up – that is, if the first room was directly below the second – then that digging process would take a bite out of the platform, making it uneven. There was still a patch of flat space there, but the machine-placement logic felt it wasn’t quite big enough. So the respawn machine wouldn’t get placed.
When you respawn, the game would look for the glowing green respawn machine. If that machine was missing, you’d end up placed at the world origin, which is always above and to the left of the gameplay area.
I dislike having the entry room be identical for every zone, and if we had more time I’d come up with a variety of layouts. But it’s too late in the project, and as this bug shows, messing with the room logic can create hard-to-find edge-cases. This close to release, we don’t want to risk creating any hard-to-find bugs.
Bug #3: Ridiculous Score Bug
Description:
For some reason, some players end the game with a score that’s about two orders of magnitude too high. Their score will be around two million instead of in the ten thousands range.
Why this problem SUCKED:
Once again, this bug played hide-and-seek. After you play the game a few hundred times and once you change the scoring system a half dozen times, the score sort of loses all meaning. “Twenty thousand on level four? I can’t remember if that’s high or not.” So I’ve stopped looking at the score, even though it’s always right there in the corner of the screen[2]. So then you’ll randomly look at your score and see it’s clearly out of reasonable bounds, but with no idea of when it went wrong or what might have triggered it. We’ve already sent preview copies to some game journos, and we can see on the leaderboards that a few of them (but not all of them!) have run into these bogus scores.
What happened:
Here is a case where you can’t find a bug because it’s disguising itself as a different kind of bug. When we saw the problem, Arvind and I both thought the same thing: “Uninitialized variable!”
Consider:
1 2 3 4 5 6 7 8 9 10 |
int FruitAdd () { int apple; int orange; int gary; apple = 10; gary = apple + orange; return gary; } |
On line 7 we assign the value of 10 to the variable “apple”. Then on the next line we assign the value of apple+orange to the variable “gary”. Except, we never put a value into “orange”! In programming terms, this means that orange has a “garbage” value. It could be anything. Orange refers to some specific piece of memory. At some point in the past, some other variable was maybe using that bit of memory to store some totally unrelated information. Then that variable’s life ended and the memory was made free again. Then it was given to orange.
To be clear, the compiler will warn you about this if you do something as blatant as my code snippet above. But there are other, subtler ways for this to happen that the compiler can’t catch. So we both assumed score was being assigned some garbage number of points. Sometimes this would be harmless, but other times a roll of the dice would result in this garbage value being very high, and thus this bug.
But, no. That’s NOT what this bug is.
It was our artist Ross that discovered the bug. Which is good, since it was Ross that caused the bug. Well, he didn’t so much “cause” it as “blunder into it”, since this problem is actually caused by a design flaw that intersected with some undocumented features.
Cause 1:
The game didn’t always have a score. In the early days of the project, your only goal was survival. When we added the score, I needed a way to quickly ascertain how well it was working and how it “felt”, and I didn’t want to have to edit dozens and dozens of robots to do it. You can blow a lot of hours messing with point values, tweaking them until they feel right. And that’s not my job.
So as a way of bootstrapping the score system, I had it use hitpoints instead. If the artist hasn’t yet assigned a score value, then it would use the robot’s hitpoints for score. This seemed like a good starting point for score values anyway, since tougher robots would be worth more. This would quickly get a rough version of the score system working so everyone could try it out and discuss our thoughts at the next meeting.
The idea was that I could leave the robot definition file alone, and Ross would go through and assign score values later. (Which he did.)
Cause 2:
We’ve got several “mother” type robots in the game that can spawn little minions. (And if we want, those minions can in turn spawn their own child minions, etc.) Ross needed to create a boss robot with invulnerable minions[3]. The minions won’t die until their mother dies.
We also have machines in the game. Like robots, machines have hitpoints. If hitpoints are zero, then bullets pass right through the machine. If hitpoints are negative, then bullets will hit the machine but it will be immune to damage. If hitpoints are positive, then the machine will blow up once it’s received enough damage. There are various situations in the game where all three of these make sense, and this system works pretty well.
But robots don’t use this system. You can’t make a robot immune to damage by giving it negative hitpoints. So when Ross found the reasonable thing didn’t work, he had to improvise. So he set the minion hitpoints to a ridiculously huge number.
Cause 3:
It’s standard practice in our game that when designing minions that can be endlessly spawned by a boss, they should be worth zero XP and zero score, so that a player can’t just sit there and farm them forever[4]. That would be very boring and you shouldn’t reward boring strategies because then players feel obligated to play the game in a way the ruins the fun. So these minions – even though they were invulnerable – were worth zero score and XP.
All together now:
The minions have some huge number of hitpoints and no score. Well, the program can’t tell the difference between “Ross never assigned me a score” and “this robot is worth zero points”. So when it sees a score value of zero it switches to using hitpoints for score. Ross probably (quite reasonably) assumed that since you can’t kill these minions, you won’t ever get points for them. But they die with their mother, and their points are awarded when that happens. This mother was a mini-boss in an optional level that you may or may not encounter, depending on what doors you choose at the end of each level.
So only some players would encounter this boss. And only some of those players would choose to engage this mini-boss. But if they did, then they would be given some ridiculous number of points for each of the minions, and their score would skyrocket.
So it wasn’t an uninitialized variable bug. It was several technically correct rules producing unexpected behavior.
Posted In: Games, Good Robot
Tagged: game development, good robot, video games