I just finished an amazing novel called The Bug, by Ellen Ullman, about a software bug that wreaks havoc at a tech startup in the mid-1980s. Among the many things Ullman captures beautifully about programming is the way an elusive bug makes you feel like you're going nuts. (The unlucky programmer assigned to fix it finds himself driven to madness by its seemingly demonic irrationality; the rest of the company nicknames it "The Jester." )
What's so disarming about a bug like that (besides the simple frustration of not being able to fix it) is that programming, in theory, is such a rationalist enterprise. It requires you to think in terms of concrete cause and effect, even as you build systems that are too complicated to really parse consistently on a rational level. Then, when it does something perceptibly "irrational," you feel as if the world has flipped upside down.
Of course, barring the rare hardware failure, the fault is not in our computers but in ourselves, or in whoever else was working on the damn project--though since I'm flying solo on Witcheye, I can't even blame the schmuck in the next cubicle. Still, even as the only hand behind the wheel, as I tackle bugs in my game, I often feel some of the same enraged disbelief that Ethan Levin feels in The Bug. I'm well equipped to write about this right now because I recently came up against a real bastard.
Each level in Witcheye contains a blue gem; each gem is hidden in tricky ways. Some require you to pull off difficult feats of dexterity. Others are tucked away in unlikely corners of the map. Still others require you to think outside the box and interact in a way you normally wouldn't. As I've designed more levels for the game, I've had to come up with more clever ways to hide the things.
World 3 of Witcheye is an ocean; in one of its levels, you face a miniboss, a diving bird who swoops down through the water to try to catch you. (This was actually inspired by footage of the Pyrenean desman in David Attenborough's incredible documentary series Life on Earth.) There are lots of games where you fight angry fish--including this one, actually--but not as many where you get treated like a fish by the other locals, so that seemed like a fun surprise.
What's so disarming about a bug like that (besides the simple frustration of not being able to fix it) is that programming, in theory, is such a rationalist enterprise. It requires you to think in terms of concrete cause and effect, even as you build systems that are too complicated to really parse consistently on a rational level. Then, when it does something perceptibly "irrational," you feel as if the world has flipped upside down.
Of course, barring the rare hardware failure, the fault is not in our computers but in ourselves, or in whoever else was working on the damn project--though since I'm flying solo on Witcheye, I can't even blame the schmuck in the next cubicle. Still, even as the only hand behind the wheel, as I tackle bugs in my game, I often feel some of the same enraged disbelief that Ethan Levin feels in The Bug. I'm well equipped to write about this right now because I recently came up against a real bastard.
Each level in Witcheye contains a blue gem; each gem is hidden in tricky ways. Some require you to pull off difficult feats of dexterity. Others are tucked away in unlikely corners of the map. Still others require you to think outside the box and interact in a way you normally wouldn't. As I've designed more levels for the game, I've had to come up with more clever ways to hide the things.
World 3 of Witcheye is an ocean; in one of its levels, you face a miniboss, a diving bird who swoops down through the water to try to catch you. (This was actually inspired by footage of the Pyrenean desman in David Attenborough's incredible documentary series Life on Earth.) There are lots of games where you fight angry fish--including this one, actually--but not as many where you get treated like a fish by the other locals, so that seemed like a fun surprise.
To set things up, I thought I'd build in another surprise: you enter a locked room where you've been conditioned to expect a fight with a few enemies, and you see a few of the aforementioned angry fish. "Simple," you think to yourself. (Poor sod that you are!) Then, of course, the look on their goofy little fish faces changes from menacing to spooked, and they hightail it out of the room as the bird splashes in from above. Cute little bit of business, right? And it also gave me a perfect way to hide a blue gem--attach it to one of the escaping fish, so that you have to catch him before he gets away. When I coded the scene and tested it on my computer, it worked perfectly.
The first sign of a problem was that no one who tried it could find the gem. The levels are short, too, so there weren't that many places it could be. So at first, I smugly assumed that my testers just weren't thinking outside the box. (Prepare for dramatic irony, by the way.)
But playing the game myself on my phone, I noticed something strange. I could still catch the fish, but barely. Even hustling directly from the door to the fish, it often escaped--and that was with someone who knew what to do at the controls. Someone who didn't would readily assume that the scene wasn't meant to be interactive, and that the fish were meant to escape every time.
The first sign of a problem was that no one who tried it could find the gem. The levels are short, too, so there weren't that many places it could be. So at first, I smugly assumed that my testers just weren't thinking outside the box. (Prepare for dramatic irony, by the way.)
But playing the game myself on my phone, I noticed something strange. I could still catch the fish, but barely. Even hustling directly from the door to the fish, it often escaped--and that was with someone who knew what to do at the controls. Someone who didn't would readily assume that the scene wasn't meant to be interactive, and that the fish were meant to escape every time.
Had I changed the timing by accident? No--I headed back to my computer and caught the fish easily. Bad news, because bugs that turn up on the phone but not on the computer are the worst kind. Fixing a bug on the computer is relatively easy, since you can pause at any moment and get a very complete appraisal of the state of the game. On a phone, your tools are a lot more cumbersome--you have to run debugging tools on your computer while you play the game on the device, scanning through an overwhelming stream of data in search of whatever little thing has gone awry. Not only that, on the computer, you can change something, hit "play," and see your results almost immediately; on a phone or tablet, you have to wait for the entire thing to build, which can take a long time, then fuss with it some more and try again. Basically it's the difference between shooting a machine gun at a marked target under a spotlight, and shooting a slingshot at a bat in the dark and then wandering around trying to figure out if you hit it.
Anyway, I grudgingly made ready with the slingshot. The code for the fish itself was so straightforward (wait a second after the door locks, then head right at a steady speed) that I guessed the problem had to be with the water physics. Water slows you down; was it possible it was slowing you down more on the phone than on the computer? I put together a quick bit of test code that simply launched the eyeball forward at regular intervals and timed how long it took it to travel a certain distance, then ran the test on both the computer and the tablet. Sure enough, the computer version cleared the distance slightly faster.
My first instinct was that the problem had something to do with different frame rates on the phone. Frame rates are a real hobgoblin for game developers doing anything resembling physics. In the good old days of relatively simple processors, game developers could count on a device (like, say, a Nintendo Entertainment System) to behave the same way for every user, on every frame. That meant that timing could be consistent; if an enemy was coded to move one pixel every frame, you could be fairly sure that in one second of gameplay, it would end up 60 pixels away. And if it didn't, that would be because something else you were doing was slowing down the game.
On modern devices, nothing is quite as simple; there are a thousand reasons that your iPhone might take longer to render one frame than the next, and as a developer, you have very little control over them. So you have to accommodate that possibility all the time. This is pretty straightforward if things are moving at a steady pace (you just multiply the object's speed by the amount of time the frame took), but gets messy when you add in acceleration. At different frame rates, this can produce significantly different results; little differences compound themselves. With most of the game behaving the same on my test devices as on my computer, I'd pretty much ignored this fact and crossed my fingers, but here I was forced to sort it out.
After digging through some of the calculus I vaguely remembered from high school, I figured out how to apply acceleration in a way that was, well, not quite frame-rate independent, but closer. My two tests were now producing close to the same results.
Imagine my delight, then, when hours of this abstruse tinkering resulted in... the same problem. The fish was still damn near impossible to catch on the phone; the slight difference in physics hadn't been the issue at all. So what the hell was going on?
What followed was a series of increasingly unlikely long shots, each as fruitless as the last, as I pulled out what little remains of the hair on my head and started in on my beard. As failed attempts piled up, I started to feel that I was losing my mind. Too many variables. And what do I know about how phones work anyway? What do I know about what the Unity engine is really doing behind the scenes? I was a tiny boatman on a raging sea, at the mercy of colossal forces far beyond my ability to comprehend. (This is the kind of cosmic-tragic mindset one gets into in these moments.)
Anyway, I grudgingly made ready with the slingshot. The code for the fish itself was so straightforward (wait a second after the door locks, then head right at a steady speed) that I guessed the problem had to be with the water physics. Water slows you down; was it possible it was slowing you down more on the phone than on the computer? I put together a quick bit of test code that simply launched the eyeball forward at regular intervals and timed how long it took it to travel a certain distance, then ran the test on both the computer and the tablet. Sure enough, the computer version cleared the distance slightly faster.
My first instinct was that the problem had something to do with different frame rates on the phone. Frame rates are a real hobgoblin for game developers doing anything resembling physics. In the good old days of relatively simple processors, game developers could count on a device (like, say, a Nintendo Entertainment System) to behave the same way for every user, on every frame. That meant that timing could be consistent; if an enemy was coded to move one pixel every frame, you could be fairly sure that in one second of gameplay, it would end up 60 pixels away. And if it didn't, that would be because something else you were doing was slowing down the game.
On modern devices, nothing is quite as simple; there are a thousand reasons that your iPhone might take longer to render one frame than the next, and as a developer, you have very little control over them. So you have to accommodate that possibility all the time. This is pretty straightforward if things are moving at a steady pace (you just multiply the object's speed by the amount of time the frame took), but gets messy when you add in acceleration. At different frame rates, this can produce significantly different results; little differences compound themselves. With most of the game behaving the same on my test devices as on my computer, I'd pretty much ignored this fact and crossed my fingers, but here I was forced to sort it out.
After digging through some of the calculus I vaguely remembered from high school, I figured out how to apply acceleration in a way that was, well, not quite frame-rate independent, but closer. My two tests were now producing close to the same results.
Imagine my delight, then, when hours of this abstruse tinkering resulted in... the same problem. The fish was still damn near impossible to catch on the phone; the slight difference in physics hadn't been the issue at all. So what the hell was going on?
What followed was a series of increasingly unlikely long shots, each as fruitless as the last, as I pulled out what little remains of the hair on my head and started in on my beard. As failed attempts piled up, I started to feel that I was losing my mind. Too many variables. And what do I know about how phones work anyway? What do I know about what the Unity engine is really doing behind the scenes? I was a tiny boatman on a raging sea, at the mercy of colossal forces far beyond my ability to comprehend. (This is the kind of cosmic-tragic mindset one gets into in these moments.)
Deliverance came, as it so often does, after hours of banging my head against the wall, by total random chance. Feeling burnt out, I was just idly playing through the water levels again on my phone, when I noticed something odd.
In Witcheye, you swipe to move and touch to stop. Underwater, a buoyancy force is always pushing you up towards the surface and water drag is always slowing down your horizontal motion. Therefore, touching to stop wouldn't work on its own; you'd stop, but then you'd start drifting upwards. So the game applies a check to see if you're touching the screen before it applies the two forces.
When I was testing the game on the computer, I'd click with the mouse to simulate a touch, then move it in the direction I wanted to move. As soon as that mouse movement crossed a short distance threshold, the eyeball would launch off, but buoyancy and drag wouldn't be applied until I released the mouse button. I never noticed, because I reflexively released the mouse button at the end of every swipe.
On the phone, I did the same thing, but I tended to lift my finger from the screen a little faster than I lifted the mouse button, as a natural part of the swiping motion. So drag started affecting the ball sooner, slowing down the rate at which it crossed the room. The two water forces needed to be applied as soon as the ball was launched, not waiting until touch was released.
In Witcheye, you swipe to move and touch to stop. Underwater, a buoyancy force is always pushing you up towards the surface and water drag is always slowing down your horizontal motion. Therefore, touching to stop wouldn't work on its own; you'd stop, but then you'd start drifting upwards. So the game applies a check to see if you're touching the screen before it applies the two forces.
When I was testing the game on the computer, I'd click with the mouse to simulate a touch, then move it in the direction I wanted to move. As soon as that mouse movement crossed a short distance threshold, the eyeball would launch off, but buoyancy and drag wouldn't be applied until I released the mouse button. I never noticed, because I reflexively released the mouse button at the end of every swipe.
On the phone, I did the same thing, but I tended to lift my finger from the screen a little faster than I lifted the mouse button, as a natural part of the swiping motion. So drag started affecting the ball sooner, slowing down the rate at which it crossed the room. The two water forces needed to be applied as soon as the ball was launched, not waiting until touch was released.
This was an error in my own thinking, obscured by the fact that I was playing in a slightly different way on the two different platforms. My failure to detect it came down to a classic case of programmer tunnel vision: when you expect a program to behave a certain way, you don't always notice what it's actually doing. Sure enough, I discovered, if I swiped and just never released my contact with the screen, the ball fired off untroubled by water drag entirely, as if the water weren't there at all. Catching the fish was, of course, made trivial.
Once I had that figured out, the problem was easy to solve; I fixed the water code to handle drag appropriately, then changed the timing so that the fish was catchable without the unintended benefit of the broken water physics.
Once I had that figured out, the problem was easy to solve; I fixed the water code to handle drag appropriately, then changed the timing so that the fish was catchable without the unintended benefit of the broken water physics.
The feeling of aggravation at having spent a day fixing the results of my own shortsightedness was far outweighed by the ecstatic relief of solving the problem, and of the universe clicking back into logical functioning. I can't put it better than Ellen Ullman does in The Bug, so I'll leave the last word to her:
When I understood all this, I was elated, hysterical with happiness... Because the world suddenly felt right again--human, bounded, knowable. We did not live surrounded by demons. There were no taunting jesters, no vexing spirits. We lived in a world of our own making, which we could tinker with and control... The world of stories rejoined the world of machine-states. We were in tune: human and tool back on the same side. I was a thinking animal at the peak of my powers, I thought. By God, I wanted to pound my chest and bellow.