Game Debugging Basics
Before getting into a
serious discussion regarding mobile game debugging, let’s take a moment
to define exactly what a bug is. A bug is simply a coding error that
results in some unwanted action taking place in your game. This unwanted
action can vary from a score not being updated correctly to the user’s
phone going down in flames. Although the latter case is admittedly a
little exaggerated, you should take bugs very seriously because they
speak volumes about the quality (or lack of quality) of your games.
The concept of bugs has
been an accepted part of programming for a long time now. Although all
programmers strive for perfection, few are ever able to attain it. Even
those that do reach that nerd nirvana typically encounter significant
numbers of bugs along the way. The difference is that these programmers
anticipate bugs rather than imagine that their code is immune to bugs.
Therefore, the first rule in regard to debugging is to assume that bugs
are in your code and that it is your responsibility to hunt them down
and fix them to the best of your ability.
The issue of finding
and fixing bugs is especially important in games, because game players
are often very fickle. If a game does something screwy like trashing a
player’s score, the player will probably get frustrated and toss your
game. This makes it all the more important to be vigilant in finding
bugs before you release your game. Sure, you can always distribute a
patch to fix a bug in a release version, but it typically leaves game
players with a less than high opinion of your development ethic.
Before getting
into specific debugging strategies, let’s go over a few debugging
basics. If you are already familiar with debugging in Java or in another
language, feel free to jump to the next section. The following are
three fundamental debugging techniques that you will find indispensable
when finding and fixing bugs in your mobile games:
Single-stepping code
Watching variables
Using breakpoints
The next few sections explore these debugging techniques in a little more detail.
Single-Stepping Code
A
very common debugger feature is the capability of single-stepping
through code. Single-stepping is the process of executing your code one
line at a time (in single steps). The significance of single-stepping as
a debugging technique is that it provides you with a way to see exactly
what code is being executed, as well as trace the flow of execution
through your program. Typically, single-stepping in itself isn’t
entirely useful; you usually combine it with another technique known as watching to see what happens to variables as you step through code.
Watching Variables
Watching is a technique that involves specifying certain variables in your code as watch variables.
A watch variable is a variable whose contents you can see while code is
executing in a debugger. Of course, in the context of a program running
at normal speed, watch variables don’t help much. But if you watch
variables as you single-step through code, you can gain lots of insight
into what is happening. Very often, you will find that the values of
variables are changing unexpectedly or being set to values that don’t
make sense in the context of what you thought the code was doing. This
type of insight into the inner workings of your code can lead you
directly to bugs. Single-stepping combined with watch variables provides
a standard approach to finding bugs with a debugger.
Using Breakpoints
Another
fundamental debugging technique is that of using breakpoints. A
breakpoint is a line of code that halts a program’s execution. To
understand the usefulness of breakpoints, imagine that you are
interested in a line of code in the middle of a program. To get to that
line of code in the debugger, you would have to single-step for hours.
Or you could set a breakpoint on that line and let the debugger run the
program like normal. The program then runs in the debugger until it hits
the breakpoint, in which case the program halts and leaves you sitting
on the specified line of code. At this point, you can watch variables
and even single-step through the code if you want. You also have the
option of setting multiple breakpoints at key locations in your code,
which is very useful when dealing with complex execution flow problems.
Game Debugging Strategies
Although
debugging tools have come a long way since the early days of
programming, the ultimate responsibility of eliminating bugs still rests
squarely on your shoulders. Think of debuggers and standard debugging
techniques simply as a means of helping you find bugs, but not as your
sole line of bug defense. It takes a diversified arsenal of knowledge,
programming practices, debugging tools, and even some luck to truly rid
your mobile games of bugs.
Debugging can almost be
likened to a hunt: You know there is something out there, and you must
go find it. For this reason, you need to approach debugging with a very
definite strategy. Debugging strategies can be broken into two
fundamental groupings: bug prevention and bug detection. Let’s take a
look at both and see how they can be used together to help make your
games bug free.
Bug Prevention
Bug prevention is the
process of eliminating the occurrence of bugs before they have a chance
to surface. Bug prevention might sound completely logical, and that’s
because it is. However, surprising numbers of programmers don’t employ
enough bug prevention strategies in their code, and they end up paying
for it in the end. Keep in mind the simple fact that bug detection is a
far more time-consuming and brain-aching task than bug prevention. I’m
all for bug prevention as a primary way to eliminate bugs.
Think of bug prevention
versus bug detection as roughly parallel to getting an immunization
shot versus treating a disease after you’ve contracted it. Certainly the
short-term pain of getting the shot is much easier to deal with than
the long-term treatment associated with a full-blown disease. This
metaphor is dangerously on the money when it comes to debugging, because
bugs can often act like code diseases; just when you think you’ve got a
bug whipped, it rears its ugly head in a new way that you never
anticipated.
Getting Explicit with Parentheses and Precedence
One area prone to bugs is
that of operator precedence. I’ve been busted plenty of times myself
for thinking that I remembered the precedence of operators correctly
when I didn’t. Take a look at the following code:
int a = 37, b = 26;
int n = a % 3 + b / 7 ^ 8;
If
you are a whiz at remembering things and you can immediately say
without a shadow of a doubt what this expression is equal to, then good
for you. For the rest of us, this is a pretty risky piece of code
because it can yield a variety of different results depending on the
precedence of the operators. Actually, it only yields one result, based
on the correct order of operator precedence set forth by the Java
language. But it’s easy for programmers to mix up the precedence and
write code that they think is doing one thing when it is doing something
else.
What’s the
solution? The solution is to use parentheses even when you don’t
technically need them, just to be safe about the precedence. The
following is the same code with extra parentheses added to make the
precedence more clear:
int a = 37, b = 26;
int n = ((a % 3) + (b / 7)) ^ 8;
Avoiding Hidden Member Variables
Another potentially
tricky bug that is common in object-oriented game programming is the
hidden member variable. A hidden member variable is a variable that has
become “hidden” because of a derived class implementing a new variable
of the same name. Take a look at Listing 1, which contains two classes: Weapon and Bazooka.
Listing 1. The Weapon and Bazooka Classes
The Weapon class defines two member variables: power and numShots. The Bazooka class is derived from Weapon and also implements a numShots member variable, which effectively hides the original numShots inherited from Weapon. The problem with this code is that when the Weapon constructor is called by Bazooka (via the call to super()), the hidden numShots variable defined in Weapon is initialized, not the one in Bazooka. Later, when the blastEm() method is called in Bazooka, the visible (derived) numShots
variable is used, which has been initialized by default to zero. As you
can probably imagine, more complex classes with this problem can end up
causing some seriously tricky and hard-to-trace bugs, especially in
game code.
The solution to the
problem is to simply make sure that you never hide variables. That
doesn’t mean that there aren’t a few isolated circumstances in which you
might want to use variable hiding on purpose; just keep in mind the
risks involved in doing so.
Making the Most of Exception Handling
One useful
preventive debugging mechanism in Java is exception handling, which is a
technique focused on detecting and responding to unexpected events at
runtime. An exception is something (usually bad) that occurs in your
program that you weren’t expecting.
To handle exceptions in your game code, you enclose potentially troublesome code within a try clause. A try
clause is a special Java construct that tells the runtime system that a
section of code could cause trouble. You then add another piece of code
(a handler) in a corresponding catch clause that responds to errors caused by the code in the try clause. The error event itself is the exception, and the code in the catch clause is known as an exception handler.
try {
// Do something that is capable of causing an exception
}
catch (Exception e) {
System.err.println(e);
}
In this code, the exception being handled is of type Exception,
which is the most general of all exceptions. In some cases you may want
to take action in response to an exception, as opposed to just printing
information about the exception to the standard error “device.”
Construction Cue
Java supports standard input and output devices, the latter of which can be used to display debugging information. System.err
is the standard error “device,” which is handy for printing messages
that are displayed in a special error window or on the command line from
which the J2ME emulator is run. The System.err.println() method prints a string to the standard error device. |
This
discussion of exception handling really only scratches the surface of
handling runtime errors (exceptions). I strongly encourage you to learn
more about exception handling and how to effectively use it.
Fortunately, a lot of information has been published about exception
handling in Java, so you shouldn’t have much trouble finding useful
references.
Bug Detection
Even if you rigorously
employ bug avoidance techniques, you will still have to contend with a
certain number of bugs. It’s just a fact of life that programmers make
mistakes, and the sheer complexity of many mobile game programming
projects often causes problems that elude us. Just embrace the notion
that you’re imperfect and focus your attention on tracking down the
mistakes. The point is that in addition to applying bug prevention
techniques as much as possible, you must learn how to track down the
inevitable bugs that will surface when you start testing your game.
Let’s look at a few techniques for hunting down bugs.
Taking Advantage of Standard Output
The age-old technique
for tracking down bugs is to print information to standard output. This
approach probably sounds pretty archaic, and in many ways it is, but if
you want a quick and dirty look into what’s going on in your game, it’s
often your best bet.
Employing the standard output technique is as simple as inserting calls to System.out.println()
at appropriate locations in your code. You can use standard output for
anything from looking at the value of variables to determining whether a
method is being called; just sprinkle those println() calls wherever you need them! The primary caveat to this approach is that you should be careful when placing the println() call in an update loop, such as the update() method, which controls the animation in MIDlet games. In this case, the println()
call might slow the game to a crawl simply because of the overhead
involved in printing text to the standard output device at such a rapid
pace.
Construction Cue
Standard
output is very similar to the standard error device. In fact, in many
cases they are one and the same. However, it is generally a good idea to
print error messages to the standard error device, and save standard
output for general debugging information. |
Tracing the Call Stack
An indispensable tool in tracking down hard-to-find bugs is the method call stack.
The method call stack is a list of the methods called to arrive at the
currently executing code. By examining the call stack, you can see
exactly which methods were called to get to the current piece of code in
question. This information often sheds light on a problem regarding a
method being called inadvertently.
You can view the call stack by calling the printStackTrace() method, which is a member of the Throwable class. Because printStackTrace() is a method in Throwable, you must have a Throwable object to look at the call stack. It just so happens that all exceptions are derived from Throwable, so any time you have an exception, you can view the call stack. Check out the following code:
In this code, the array nums is indexed out of bounds in the for loop, generating an ArrayIndexOutOfBoundsException. The exception is logged to standard output in the catch clause, along with a call to printStackTrace().