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.
ToChapter's lesson focuses on debugging as it applies to Java game
programming. As you go through toChapter'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 toChapter'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 toChapter'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 toChapter 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
Chapters 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.
Isolated Test Methods
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 toChapter's lesson.
Exception Handling
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 guide:
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 toChapter 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.
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;
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" 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.
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 Chapter 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.
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.
Using the jdb debugger is a topic best left to the introductory
guides 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.
ToChapter 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 toChapter 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!
The term "bug" was coined by programming pioneer Grace Hopper back in the Chapters 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 toChapter. 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."
Learn how to use the jdb debugger that comes with the Java
Developer's Kit.
Try using breakpoints and single-stepping through some of
the code in the Scorpion Roundup sample game.
See whether you can find any bugs in Scorpion Roundup, or
for that matter in any of the sample code in the guide. If you
manage to find anything, be sure to e-mail me and let me know
how disappointed you are that I could be such a slacker to ship
bug-ridden code! My e-mail address is located in the author bio
at the front of the guide.