Wow, you've now finished two complete Java games! You're probably feeling pretty good about your new Java game programming skills, as you should. Without putting a damper on things, keep in mind that you didn't have to worry much about bugs in those games. Actually, you did have to contend with scorpions and tarantulas, but I'm talking about programming bugs. As sobering as it might sound, I have to admit that the games had programming bugs in them prior to some heavy debugging sessions. Who knows, they might even have a few bugs now that managed to slip by. Knowing all this, it simply wouldn't be fair to teach you about game programming without covering the often dreaded issue of debugging.
Today's lesson focuses on debugging as it applies to Java game programming. As you go through today's lesson, keep in mind that bugs are a natural part of the development process; as humans, we simply are error prone. So you should embrace debugging as a necessary part of the development process and accept the fact that even your precious code will have bugs. I'll do what I can throughout today's lesson to help you develop skills that keep bugs to a minimum, but the rest is up to you.
The following topics are covered in today's lesson:
Before getting into any type of discussion regarding 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 computer going down in flames. Although the latter case is admittedly a little exaggerated (especially in Java programming), you should take bugs very seriously because they speak volumes about the quality (or lack of quality) of your game.
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 suggest 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 games:
A very common debugger feature is the capability to single-step 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 means to see exactly what code is being executed, along with the ability to 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.
Note |
Incidentally, a debugger is a software tool specifically designed to help you find bugs by letting you analyze your code as it is running. The Java Developer's Kit ships with a debugger called jdb, which you learn about a little later today in the "Choosing a Debugger" section. |
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 variables values 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 is the standard approach to finding bugs using a debugger.
Another fundamental debugging technique is that of using breakpoints.
A breakpoint is a line of code that you specify, which halts the execution of a program.
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.
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 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 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. If you haven't understood the point yet, 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.
Hopefully, I've closed the sale and you're set to employ some bug prevention in your code. Fortunately, most preventive bug measures are simple and take little extra time to implement. Unfortunately, compared to other languages, Java is fairly limited in regard to providing preventive debugging facilities. However, this fact is a little misleading because the nature of Java removes many of the bug creation opportunities available in other languages such as C and C++. For example, the assert mechanism is one of the most popular preventive debugging techniques in C/C++. assert allows you to check boolean conditions in debug versions of your programs. A primary usage of assert is to defend against the occurrence of null pointers. Because Java has no pointers, you can immediately eliminate the risks associated with this entire family of bugs. So, even though Java doesn't have a bug prevention facility similar to assert, there's no loss because in Java you can't create the bugs typically found using assert.
A good way to prevent bugs early in the development cycle is to test your code heavily as it is being developed. Of course, most programmers do indeed try out their code as they are writing it, so you're probably thinking that you perform enough testing as it is. However, the type of testing I'm talking about is a thorough test of your classes in an isolated manner. Think about it like this: If you heavily test your classes in isolation from other classes, don't you think the odds of bugs appearing when you connect everything will be lower? Furthermore, think of how much easier it is to test your classes early without having to contend with a bunch of complex interactions taking place between different classes.
My suggestion is to build a single method into each one of your classes that puts the class through a series of tests. Call the method test if you like, and make sure that it handles creating instances of the class using various constructors (if you have more than one), as well as calling all the methods that can be called in isolation. I know that, practically speaking, certain aspects of the class can only be tested in the presence of other classes, but that's all right; just test whatever you can.
In your test method, you probably want to output the values of certain member variables. Just output the results to standard output. If you are unfamiliar with using standard output, don't worry. You learn about using it for debugging later in today's lesson.
One useful preventive debugging mechanism used in C++ is exception handling, which also shares very solid support in Java.
Exception handling 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.
Unlike some other forms of preventive bug detection, however, exception handling also has a valuable place in release code.
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.
The following is some exception handling code that you've seen a lot in the sample applets throughout this book:
try {
tracker.waitForID(0);
}
catch (InterruptedException e) {
return;
}
In this code, the exception being handled is of type InterruptedException, which specifies that the current thread was interrupted by another thread. In some cases, this might not be a problem, but the code following this particular code is dependent on images successfully loading, which is indicated by the return from the waitForID method. Therefore, it's important that the thread is not interrupted. The only problem with this exception handler is that it doesn't output any information regarding the nature of the exception. Typically, you would have code here that prints information to standard output, which you learn about a little later today in the "Standard Output" section.
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.
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;
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" due to a derived class implementing a new variable of the same name. Take a look at Listing 14.1, which contains two classes: Weapon and Bazooka.
Listing 14.1. The Weapon and Bazooka classes.
class Weapon {
int power;
int numShots;
public Weapon() {
power = 5;
numShots = 10;
}
public void fire() {
numShots--;
}
}
class Bazooka : extends Weapon {
int numShots;
public Bazooka() {
super();
}
public blastEm() {
power--;
numShots -= 2;
}
}
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.
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.
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 large programming projects often causes problems that elude
us. That's all right. 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.
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. This technique
is especially useful now, because visual Java debuggers are still
rough around the edges.
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 attempt to place the println
call in an update loop, like the loop controlling the animation
in 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.
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.
The resulting output follows:
Incidentally, I placed this example code in the init
method in the Traveling Gecko game, which explains the call stack
results.
An important decision regarding how you finally decide to debug
your game is that of choosing a debugger. A debugger is an invaluable
tool in ridding your game of bugs, and it can directly determine
how much time you spend debugging. Therefore, you should make
sure to invest your resources wisely and choose a debugger that
fits your development style. Unfortunately, the third-party Java
debugger market is still in its infancy, so don't expect to have
lots of debuggers to choose from at this point. Nevertheless,
try to keep tabs on the latest Java development tools and how
they might impact your debugging.
A few third-party integrated development environments that include
built-in visual debuggers are available for Java. These are very
nice and usually include lots of cool features beyond the ones
you just learned about; definitely look into getting a full-featured
debugger if at all possible. You learn much more about Java development
environments, including debuggers, on Day 21, "Assembling
a Game Development Toolkit." For now, just keep in mind that
choosing a debugger that fits your needs is important in determining
how successfully you can rid your code of bugs. Fortunately, nearly
all debuggers perform the basic debugging functions of single-stepping,
supporting watch variables, and using breakpoints.
The Java Developer's Kit comes standard with a debugger (jdb)
that performs basic debugging functions such as those you learned
about earlier. It is a command-line debugger, which means that
it has no fancy graphics or point-and-click features but it does
get the job done. If you aren't ready to commit to a third-party
tool, by all means try out jdb. After you get comfortable with
jdb, you might find that it serves your purposes well enough.
Before you can use jdb, you need to compile your code so that
it includes debugging information. The Java compiler switch for
doing this is -g, which causes
the compiler to generate debugging tables containing information
about line numbers and variables.
Using the jdb debugger is a topic best left to the introductory
books on Java. However, there is a nice online tutorial for using
jdb to debug Java code on Sun's Java Web site, which is located
at http://www.javasoft.com.
Today you learned about crushing bugs in Java code. You not only
learned the importance of diagnosing and putting an end to bugs
in games, you learned some valuable tips on how to help prevent
bugs before they can even appear. You began the lesson with a
somewhat formal definition of a bug, followed by some debugging
basics. You then moved on to determining how to select a debugger,
and you finished up with a look at some common debugging strategies.
The debugging strategies you learned about today are in no way
comprehensive. The reality is that debugging is an art form involving
a lot of practice, intuition, and even heartache. You will no
doubt establish your own bag of debugging tricks far beyond those
I've suggested here. I encourage you to be as crafty as possible
when it comes to ferreting out pesky bugs!
If you think debugging puts a strain on your brain, try letting
the computer think for you. Hey, that just happens to be the topic
of your next lesson: artificial intelligence. But before you move
on to that, there's some celebrating to do. You're finished with
your second week of lessons!
Bug Detection
Standard Output
Note
Speaking of standard output devices, you might be wondering exactly where standard output goes when you are running an applet inside a Web browser such as Netscape Navigator. The truth is that nobody knows! (Just kidding!) Netscape Navigator provides a
console window where you can see everything that is being sent to standard output. To display this window, just look under the Options menu in Navigator and select Show Java Console.
Call Stack Trace
try {
int nums[] = new int[5];
for (int i = 0; i < 10; i++)
nums[i] = 6670;
}
catch (ArrayIndexOutOfBoundsException e) {
System.out.println("**Exception** : " +
e.getMessage());
e.printStackTrace();
}
**Exception** : 5
java.lang.ArrayIndexOutOfBoundsException: 5
at TravelingGecko.init(TravelingGecko.java:32)
at sun.applet.AppletPanel.run(AppletPanel.java:259)
at java.lang.Thread.run(Thread.java:294)
Choosing
a Debugger
Note
Some distributions of the JDK also include an alternate Java compiler called javac_g. If you have this compiler in your distribution (look in the java/bin directory), use it, because it compiles code without using some of the internal
optimizations performed by the javac compiler.
Summary
Q&A
Q | Where does the term "bug" come from? |
A | The term "bug" was coined by programming pioneer Grace Hopper back in the days when programming was performed using rudimentary hardware switches. As the story goes, a computer malfunctioned and someone noticed that a moth had gotten caught in one of the mechanical relays in the computer, keeping the relay from closing and making contact. From that time forward, programming errors were referred to as bugs. |
Q | Using watch variables, is it possible to watch an entire object at once? |
A | Yes, most debuggers provide a means of watching an entire object at once, just like any other variable. |
Q | How does Java's use of automatic garbage collection impact debugging? |
A | The garbage collection mechanism employed by Java, coupled with the inability to use pointers, removes a wide range of bug creation opportunities. Aside from removing the problem of dealing with null pointers, Java also alleviates having to contend with memory leaks, which are very common in C and C++. Memory leaks are chunks of memory that are allocated but inadvertently never deleted, effectively resulting in memory loss. |
The Workshop section provides questions and exercises to help you get a better feel for the material you learned today. Try to answer the questions and at least go over the exercises before moving on to tomorrow's lesson. You'll find the answers to the questions in appendix A, "Quiz Answers."