Coding Tidbits and Style That Saved My Butt
When you pick up a game programming book, the last you likely want to do is read about programming style and specific coding techniques. After all, you probably want to dive right in and learn how to code up 3-D graphics or sound. Before getting to that, I'd rather show you some useful things you'll use throughout your entire code base. I'll also show you some things to avoid in your code, much of which is setup for working with other programmers. Your code should communicate clearly to other programmers at every opportunity. Something I've learned over the years is that the distance between exuberance and experience is paved with mistakes. This sometimes makes older programmers a little less likely to embrace new things.
In the first edition of this book, this chapter was called "dumb stuff all game programmers should know." It turned out that this stuff wasn't so dumb, or obvious. My goal in this chapter is to set the foundation for the coding techniques that I'll be presenting throughout this book. I've developed this style over the years watching really smart people, and the techniques have worked for me so I've keep them around.
But as you read this chapter, keep in mind that when it comes to programming style, programmers have different ways of doing things. For example, the techniques you use to program games on a PC platform, where you have more robust tools and plenty of memory to work with, might be different than the techniques you'll use to program on a platform such as the PS2. Using C or C++ also makes a huge difference in style. In other words, not every problem has a single solution and no single style fits all situations. Also every programmer, and programming team is different. They'll sometimes never agree on things and I don't expect you'll agree with everything I present in this chapter, nor this book.
Let me put it this way—if you find something you really hate, it means you have opinions different than mine, and you've formed those opinions through first hand pain and suffering. That's great; it means you're a programmer, and you and I can debate endlessly on the web about the best way to do things.
We'll start by looking at design practices that you should consider when writing a game and then we'll move on and look at specific programming techniques such as working with pointers, memory management, how to avoid memory leaks, and other goodies. In the last part of the chapter I'll provide you with a few coding tools taken from my own personal toolbox that I usually employ to develop games for companies such as Eidos, Microsoft, and Origin.
Smart Design Practices
Isaac Asimov's Foundation series invented an interesting discipline called psycho-history, a social science that could predict societal trends and macro events with great certainty. Each historian in the story was required to contribute new formulas and extend the science. As a programmer, your job is similar. Every new module or class that you create gives you the opportunity to extend the abilities and usefulness of the code base. But to do this effectively, you must learn how to think ahead and design code with the goal of keeping it in use for many projects and many years.
Designing good code in an object-oriented language can be more difficult than in a procedural language like C or PASCAL. Why? The power and flexibility of an object-oriented language like C++, for example, allows you to create extremely complicated systems that look quite simple. This is both good and bad. In other words, it's easy to get yourself into trouble without realizing it. A good example of this is the C++ constructor. Some programmers create code in a constructor that can fail. Maybe they tried to read data from an initialization file and the file doesn't exist. A failed constructor doesn't return any kind of error code, so the badly constructed object still exists and might get used. Another example is the misuse of virtual functions. A naïve programmer might make every method in a class virtual, thinking that future expandability for everything is good. Well, he'd be wrong. A well thought through design is more important than blind application of object oriented programming constructs.
You can make your work much more efficient by improving how you design your software. With a few keystrokes you can create interesting adaptations of existing systems. There's nothing like having such command and control over a body of code. It makes you more artist than programmer.
A different programmer might view your masterpiece entirely differently, however. For example, intricate relationships inside a class hierarchy could be difficult or impossible to understand without your personal guidance. Documentation, usually written in haste, is almost always inadequate or even misleading.
To help you avoid some of the common design practice pitfalls I'm going to spend some time in this chapter up-front discussing how you can:
- Avoid hidden code that performs nontrivial operations
- Keep your class hierarchies as flat as possible
- Be aware of the difference between inheritance and containment
- Avoid abusing virtual functions
- Use interface classes and factories
- Use streams in addition to constructors to initialize objects
Avoiding Hidden Code and Nontrivial Operations
Copy constructors, operator overloads, and destructors are all party to the "nasty" hidden code problem which plague game developers. This kind of code can cause you a lot of problems when you least expect them. The best example is a destructor because you never actually call it explicitly; it is called when the memory for an object is being deallocated or the object goes out of scope. If you do something really crazy in a destructor, like attach to a remote computer and download a few megabytes of MP3 files, you're teammates are going have you drawn and quartered.
My advice is that you should try to avoid copy constructors and operator overloads that perform non-trivial operations. If something looks simple, it should be simple and not something deceptive. For example, most programmers would assume that if they encountered some code that contained a simple equals sign or multiplication symbol that the it would not invoke a complicated formula such as a Taylor series. They would assume that the code under the hood would be as straightforward as it looked—a basic assignment or calculation between similar data types such as—floats or t doubles.
Game programmers love playing with neat technology, and sometimes their sense of elegance drives them to push non-trivial algorithms and calculations into C++ constructs such as copy constructors or overloaded operators. They like it because the high level code performs complicated actions in a few lines of code, and on the surface it seems like the right design choice. Don't be fooled.
Any operation with some meat to it should be called explicitly. This might annoy your sense of cleanliness if you are the kind of programmer that likes to use C++ constructs at each and every opportunity. Of course there are exceptions. One is when every operation on a particular class is comparatively expensive, such as a 4x4 matrix class. Overloaded operators are perfectly fine for classes like this because the clarity of the resulting code is especially important and useful.
Sometimes you want to go a step further and make copy constructors and assignment operators private. This keeps programmers from assuming the object can be duplicated in the system. A good example of this is an object in your resource cache, such as an ambient sound track that could be tens of megabytes. You clearly want to disable making blind copies of this thing because an unwary programmer might believe all he's doing is copying a tiny sound file.
A recurring theme I'll present throughout this book is that you should always try to avoid surprises. Most programmers don't like surprises because most surprises are bad ones. Don't add to the problem by tucking some crazy piece of code away in a destructor or similar mechanism.
Class Hierarchies: Keep Them Flat
One of the most common mistakes game programmers make is that they either over-design or under-design their classes and class hierarchies. Getting your class structure well designed fo your particular needs takes some real practice. Unfortunately, most of my experience came the hard way through trial and error. But you can learn from some of my mistakes and unique techniques that I've picked up along the way.
|Tales from the Pixel Mines|
My first project at Origin developed with C++ was Ultima VII. This project turned out to be a poster child for insane C++. I was so impressed by the power of constructors, virtual functions, inheritance, and everything else that once I got the basics down I went nuts and made sure to use at least three C++ constructs on every line of code. What a horrible mistake! Some Ultima VII classes were seven or eight levels of inheritance deep. Some classes added only one data member to the parent.our impotent attempt at extending base classes.
We created so many classes in Ultima VII that we ran out of good names to use. The compiler was so taxed by the end of the project that we couldn't add any more variables to the namespace. We used global variables to store more than one piece of data by encoding it in the high and low words rather than creating two new variables. By the end of the project I was terrified of adding any new code, because the compiler would fail to build the project having hit some crazy limitation.
On the opposite end of the spectrum, a common problem found in C++ programs is the Blob class, as described in the excellent book Antipatterns, by Brown, et. al. This is a class that has a little bit of everything in it, and is a product of the programmer's reluctance to make new, tightly focused classes. In the source code that accompanies my book, the GameCodeApp class is probably the one that comes closest to this, but if you study it a bit you can find some easy ways to factor it.
When I was working on Ultima VII we actually had a class called KitchenSink and sure enough it had a little bit of everything. I'll admit to creating such a class on one of the Microsoft Casino projects that I worked on that would have made intelligent programmers sick to their stomachs. My class was supposed to encapsulate the data and methods of a screen, but it ended up looking a little like MFC's Cwnd class. It was huge, unwieldy, and simply threw everything into one gigantic bucket of semi colons and braces.
Professionally I like to use a flat class hierarchy. I've also used this approach for the source code for this book. Whenever possible, it begins with an interface class and has at most two or three levels of inheritance. This class design is usually much easier to work with and understand. Any change in the base class propagates to a smaller number of child classes, and the entire architecture is something normal humans can follow.
Try to learn from my mistakes: Good class architecture is not like a Swiss Army Knife; it should be more like a well balanced throwing knife.
Inheritance vs. Containment
Game programmers love to debate the topics of inheritance and containment. Inheritance is used when an object is evolved from another object, or when a child object is a version of the parent object. Containment is used when an object is composed of multiple discrete components, or when an aggregate object has a version of the contained object.
A good example of this relationship is found in user interface code. A screen class might have the methods and data to contain multiple controls such as buttons or check boxes. The classes that implement buttons and check boxes probably inherit from a base control class.
When you make a choice about inheritance or containment your goal is to communicate the right message to other programmers. The resulting assembly code is almost exactly the same, barring the oddities of virtual function tables. This means the CPU doesn't give a damn if you inherit or contain. Your fellow programmers will care, so try to be careful and clear.
Page 1 of 4