Debugging
With any project, there will be bugs and-this is the surprising part-you're going to have to debug them. Types of bugs can range anywhere from slight display problems, crashes due to exceptions, or the game simply not working as it was intended. You can put bugs into three different categories:
- Bugs due to design. For example, collision-detection code might not be designed to handle certain situations that weren't originally considered in the design. Because these bugs involve redesigning part of your game, they can be difficult to fix.
- Bugs due to human error. This could be just something missing in the code or a simple typo. These bugs are often easy to fix but can occasionally be difficult to track down.
- Bugs due to machine differences. What works on one machine (or virtual machine) might not work on another. These can be difficult to fix if you don't have access to the machine that exhibits the bug.
It might sound ridiculous, but the best way to debug is to not write bugs in the first place. Being careful with your code and design can take less time than trying to track down a strange bug or having to redesign huge sections of code later. Also, bugs due to machine differences can often be avoided by just paying close attention to potential exceptions and return values of methods in the API. These bug-prevention techniques will help limit the number of bugs in the first place:
- Don't "compile, run, and pray." Whenever you write a piece of code, look over it once to check for anything you missed. Look over it twice if you don't feel sure it will work. Try to do what it takes to be confident that your code will work before you run it.
- Pay attention to the API documentation. Make sure you understand the specification of a method, catch exceptions, check return values, and pass in legal parameters.
- Document your code. It's harder to debug something later if you can't tell what's going on. Documentation really helps clear it up. (I'm still not the best at this one.)
- Likewise, make the code as readable as possible. Use meaningful, descriptive variable names such as numberOfItems instead of short, ambiguous names such as num.
- Write unit tests, and run the game often. Unit tests are simple instructions that test one of your method's various ways to make sure it follows its specifications. You can then run these unit tests when you change a method, to make sure everything is still working properly. But you don't have to unit-test everything. Don't be tempted to spend most of your time writing tests-there's a game to create!
- Log errors, warnings, and information. This can help you track down bugs if any occur, and you can always turn off logging by default in the final release.
- Eventually, you'll get a sort of "Spidey-sense," knowing something is wrong just at first glance!
In my own experience, a lot of hard-to-find bugs commonly occur after writing or rewriting a lot of code at once (thousands of lines) without doing any runtime tests. If you can, write or rewrite only small portions at a time so you can test major code additions and changes incrementally. Of course, no matter what you do, bugs will creep in. When you get bugs, here are some tips on fixing them:
- If you're getting an exception, make sure you compile the code in debug mode and run the game from the console. This way, you'll get a stack trace so you can get the exact line number of the method that throws the exception. Because it's so easy to find the source of exceptions, this is one of the easiest problems to fix in Java.
- Use a debugger to step though the code, line by line.
- Write extra log statements to help narrow down the source of the bug.
- If you suspect thread deadlock, run the game from the console, wait for the deadlock to occur, and then return to the console and enter Ctrl+break on Windows (or Ctrl+\ on Mac OS X and UNIX). This shows the state of the threads, any locks acquired, and any deadlock detected.
Using println debugging is a quick-and-dirty way to narrow down a bug. For example, let's say you have a bug somewhere dealing with a couple of Point instances. You could print the values of each of the points to the console:
Point p1 = new Point(x1, x2); Point p2 = new Point(y1, y2); System.out.println("Two points: " + p1 + " and " + p2);
In this case, you use println debugging to make sure the point values are correct right after you create them. In this trivial example, the points are created incorrectly, and the code should instead look like this:
Point p1 = new Point(x1, y1); Point p2 = new Point(x2, y2);
This debugging technique can be useful for exceptions. Here, you print the stack trace for a thrown exception:
try { ... } catch (Exception ex) { // print stack trace and continue ex.printStackTrace(); }
Or, you can print a stack trace without throwing or catching anything:
new Throwable().printStackTrace();
Or, you can just print the first line (last method called) of a stack trace:
System.out.println(new Throwable().getStackTrace()[0]);
Here, the getStackTrace() method returns an array of StackTraceElements, and you print the first one. Of course, not all bugs lead to disastrous results or incorrect behavior. Some of them cause problems only in rare cases. For example, memory leaks could be one of those problems. Memory leaks are common in languages that require you to deallocate memory, but you shouldn't have that problem in Java because of the garbage collector, right? Well, that's not the only cause of memory leaks. Remember, the garbage collector collects only objects that no longer have references to them. So, another reason why you might get leaks is because the app is keeping objects around that are no longer needed, and that collection of unneeded objects grows over time. For example, you could put objects in a HashMap that stays alive during the course of the game. If new objects are continually added but unneeded objects are never removed, the memory used by the HashMap will grow, resulting in a leak. This is something to keep in mind. If you're trying to detect a leak and suspect it's because of something like this, you could periodically check the size of the HashMap (or other Collection class) to see how it changes during the course of a game. If its size grows in a way that it shouldn't, it could be a leak.
Debugging Java2D Issues
If you suspect your game has bugs related to graphics issues on Windows machines, first make sure the machine has the latest video drivers. If the problem persists, try disabling various Java2D enhancements using these command-line flags:
-Dsun.java2d.d3d=false -Dsun.java2d.ddoffscreen=false -Dsun.java2d.noddraw=true
You can find out more of what Java2D is doing by logging everything that is drawn, from lines to rectangles to images. This can help you both debug and track down performance problems. Use this command-line flag:
-Dsun.java2d.trace=<options>
Here, <options> is a comma-separated list of these options:
- log logs each Java2D draw operation when it is invoked.
- timestamp adds a timestamp (in milliseconds) of the time of each operation.
- count displays a count of each invoked operation when the app exits.
- out:<filename> sends output to the specified file rather than the console.
- help displays the usage of this flag.
- verbose displays a summary of the options used.
For example, you could use this flag to log the count of each operation to the file log.txt:
-Dsun.java2d.trace=count,out:log.txt
This would give a general idea about the Java2D operations used in a game. Here's an example output from the tile-based game in , "Creating a 2D Platform Game":
85339 calls to sun.awt.windows.Win32BlitLoops:: Blit("Short 565 RGB DirectDraw with 1 bit transp", SrcOverNoEa, "Short 565 RGB DirectDraw") 31698 calls to sun.awt.windows.Win32BlitLoops:: Blit("Short 565 RGB DirectDraw", SrcNoEa, "Short 565 RGB DirectDraw") ... 118723 total calls to 18 different primitives
Only the two highest-used primitives are produced here-18 primitives are shown altogether. For these two blit operations, the first parameter is the type of the source image, the second parameter is the composite type, and the third parameter is the type of the destination image. The composite type is like the types in the AlphaComposite class, except with NoEa as a suffix to specify that the image has "no extra alpha" information.
Logging
As mentioned here and in , "Multi-Player Games," logging can help diagnose problems and provide warnings and information about how a game executes. And, of course, if you log to a file, you can study the log later. Using a logger also allows you to easily control the granularity of the log information so you can see only warnings and severe messages instead of seeing all information. Also, you can redirect the log output in various ways, such as to the console, to a file, or to a network stream. Logs don't necessarily have to be simple text sentences. They can also be data that is later externally parsed into another format. For example, if you are trying to debug the creation of an internal 2D geometric map, you could log the structure of the map as SVG data and then use a SVG viewer to get a visualization of the data. Apache log4j was used in , and in , "Artificial Intelligence," you rolled your own logger using an in-game message queue to log the decisions bots make. Besides using these methods and println logging, the java.util.logging package is included with J2SE 1.4; we'll give a quick overview of that next. First, you need your own Logger object:
static final Logger log = Logger.getLogger("com.brackeen.javagamebook.tilegame");
This statement gets the Logger named com.brackeen.javagamebook.tilegame, or creates a new one if it doesn't exist. You can use this statement in every class in your code, and it will return the same Logger object. You can log statements at various levels, like this:
log.info("Loading resources"); log.warning("No MIDI soundbank available."); log.severe("Dude, we can't find that file!");
Logging at different levels means you can get an idea of the severity of each log statement, and you can also choose to output information at only the level you want to see. For example, if you want to see only severe log information, use this code:
log.setLevel(Level.SEVERE);
The logger itself uses two objects to log your information: a Handler, which handles log statements, sending this to, say, a file or the console; and a Formatter, which formats the look of your log statements, optionally adorning it with things such as time stamps and class/method information. By default, a Logger uses its parent handler, which uses the ConsoleHandler and the SimpleFormatter. Here's an example of using your own handler and formatter:
// log to the console ConsoleHandler myHandler = new ConsoleHandler(); // use a minimal formatter myHandler.setFormatter(new Formatter() { public String format(LogRecord record) { return record.getLevel() + ": " + record.getMessage() + "\n"; } }); // only use our own handler log.setUseParentHandlers(false); log.addHandler(myHandler);
Here, you use a ConsoleHandler, make your own Formatter, and turn off use of the parent formatter. Of course, there are lots of possibilities for different handlers. For example, you might want to create a handler that sends severe log statements to an external server. This way, you can find out severe problems that other people have when they play your game. If you do something like this, however, be sure to pop up a dialog box, tell the user what data you want to send, and ask the user if it is okay to send this data to your server. Otherwise, people could get upset about their computers sending data, even if the data does not personally identify them or their computer. In a final game, you might want to just turn off all logging, like this:
log.setLevel(Level.OFF);
Notice that even if logging is turned off, objects created for the log record are still created, and any calculations for the log statement are still performed. For example, in this log statement, the distance is still calculated and converted to a string, which is then appended to the existing string:
log.info("Dude, the distance is: " + point.distance(x, y));
Obviously, if you were calculating the distance between two points several times a frame, it could impact performance. If you want to avoid things such as this, you can use a search-and-replace function to comment out every log statement in your Java files.