I’m writing this because I thought it might be interesting to other programmers. I used this method for about ten years, so it’s pretty sound.
I don’t use debugger. Well, I start it sometimes in (mostly futile) hope of getting a better exception stack trace. Or to look at the generated machine code in disassembly when optimizing. Otherwise, I find it mostly useless.
I don’t use “debug” build. I always build “release” with debug information. So I’m testing the same code people will use to play the game. Debug info is for getting stack info on exceptions and in some other cases.
When I encounter an error (crash, wrong logic, glitches or whatever) I follow these steps:
- Thinking
- Reviewing
- Logging
- Debugging (or, rather, taking a break)
Each step’s purpose is to get some new info about the problem. If I get new info, I go back to the first step – thinking.
As you see, debugging is actually there, but I rarely get to it. Let’s look at the steps in more detail.
Thinking
Stop and think: what can cause this problem? Is it the result of the code I just wrote/changed? What did I do to cause it? Or was it there before? What subsystems is it limited to? When does it happen, in what conditions? And so on.
Just thinking about the problem often gives some new insights about what can cause it. When you do small incremental changes (and I try to work that way when possible), the changed code is small and easy to grasp with you mind, and the problem is likely there.
Reviewing
Look at your code. Review it. Do you remember it correctly? What is actually written there? (Strange things can happen, especially if you use copy/paste/modify a lot).
Look for things you didn’t think about. Look for things that differ from what you expect them to be. If you use third-party library/engine/whatever, look at the documentation for the things you use to make sure they do what you thing they do, and accept those arguments etc.
If you find something, go back to thinking.
Logging
The code is complex. It can produce emergent results (that’s why you use it in the game, right?) So you might need to look at some values it actually produces. That’s where logging comes in.
Logging is the code that writes some text message to some log. It can be a log file, console, stderr, socket, whatever. You must be able to view all log messages though, and searching and filtering them comes in handy too.
Why logging and not a debugger?
It gives you exactly the info you ask for.
It puts it in context, with other log messages. You can see when it happened in relation to other logged events. You can see how often it happened. You can see how the value changed over time.
You can easily filter what values you log when you have multiple objects and are interested in only one of them. Or only values above a threshold. Or both.
You can surround the code with log messages and find where the exception occurs even when you can’t get a stack trace (be sure to flush you log though).
You can do all this in the release build.
You can do it even when you code for console/mobile/set-top-box/toaster/whatever.
It’s easy to turn off when you release a build. And it can be useful when the problem happens on some other computer, not on yours.
Some pitfalls:
Make it easy to automatically flush the log after every message, it’s essential when diagnosing exceptions or freezing. Or flush it always (might be slow).
Make sure you actually log what you want to log (reviewing helps here). If you write wrong value to the log, it can mislead you for a while.
Visual aids
That’s not exactly logging, but it helps a lot in games. You can write bounding box coordinates to your log, but actually seeing the box on the screen with other objects is much more useful and effective.
Make it easy to add this form of “logging” to your code. Colored lines that fade out over time are useful, for example. You can add them anywhere in your code and see them on the screen for long enough (but not forever). You can make boxes, circles, spheres, arrows or whatever you want just writing a function that conveniently adds multiple lines based on the parameters you pass.
Visualizing AI state, targets, ranges etc is extremely useful too.
Debugging
If everything else fails, you can try the debugger. But what info can you get from it that the previous steps didn’t uncover? I guess doing more thinking or taking a break is better.