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.
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.
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.
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.
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
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 to have you
drawn and quartered.
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 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 doubles .
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 who 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.
you want to go a step further and make copy constructors and assignment
operators private. This keeps programmers from assuming an 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.
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
Class Hierarchies: Keep Them Flat
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 to 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.
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 semicolons and braces.
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
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
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.
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.
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.
Virtual Functions Gone Bad
functions are powerful creatures that are often abused. Programmers
often create virtual functions when they don't need them or they create
long chains of overloaded virtual functions that make it difficult to
maintain base classes. I did this for a while when I first learned how
to program with C++.
a look at MFC's class hierarchy. Most of the classes in the hierarchy
contain virtual functions which are overloaded by inherited classes, or
by new classes created by application programmers. Imagine for a moment
the massive effort involved if some assumptions at the top of the
hierarchy were changed. This isn't a problem for MFC because it's a
stable code base, but your game code isn't a stable code base. Not yet.
insidious bug is often one that is created innocently by a programmer
mucking around in a base class. A seemingly benign change to a virtual
function can have unexpected results. Some programmers might count on
the oddities of the behavior of the base class that, if they were
fixed, will actually break any child classes. Maybe one of these days
someone will write an IDE that graphically shows the code that will be
affected by any change to a virtual function. Without this aid, any
programmer changing a base class must learn (the hard way) for
themselves what hell they are about to unleash. One of the best
examples of this is changing the parameter list of a virtual function.
If you're unlucky enough to change only an inherited class and not the
base class, the compiler won't bother to warn you at all; it will
simply break the virtual chain and you'll have a brand new virtual
function. It won't ever be called by anything, of course.
one point of view, a programmer overloads a virtual function because
the child class has more processing to accomplish in the same “chain of
thought.” This concept is incredibly useful and I've used it for nearly
ten years. It's funny that I never thought how wrong it can be.
overloaded virtual function changes the behavior of an object, and
gains control over whether to invoke the original behavior. If the new
object doesn't invoke the original function at all, the object is
essentially different from the original. What makes this problem even
worse it that everything about the object screams to programmers that
it is just an extension of the original. If you have a different
object, make a different object. Consider containing the original class
instead of inheriting from it. It's much clearer in the code when you
explicitly refer to a method attached to a contained object rather than
calling a virtual function.
happens to code reuse? Yes, have some. I hate duplicating code; I'm a
lazy typist and I'm very unlucky when it comes to cutting and pasting
code. It also offends me.
Try to look at classes and their relationships as if they are appliances and electrical cords.
Always seek to minimize the length of the extension cords, minimize the
appliances that plug into one another, and don't make a nasty tangle
that you have to figure out every time you want to turn something on.
This metaphor is put into practice with a flat class hierarchy—one
where you don't have to open twelve source files to see all of the code
for a particular class.
Use Interface Classes
classes are those that contain nothing but pure virtual functions. They
form the top level in any class hierarchy. Here's an example:
virtual void VAdvance(const int deltaMilliseconds) = 0;
virtual bool const VAtEnd() const = 0;
virtual int const VGetPosition() const = 0;
typedef std::list<IAnimation > AnimationList;
sample interface class defines simple behavior common for a timed
animation. We could add other methods such as one to tell how long the
animation will run or whether the animation loops; that's purely up to
you. The point is that any system that contains a list of objects
inheriting and implementing the IAnimation interface can animate them
with a few lines of code:
for(AnimationList::iterator itr = animList.begin(); itr != animList.end(); ++itr)
(*itr).VAdvance( delta );
classes are a great way to enforce design standards. A programmer
writing engine code can create systems that expect a certain interface.
Any programmer creating objects that inherit from and implement the
interface can be confident that object will work with the engine code.
Consider Using Factories
tend to build screens and other complex objects constructing groups of
objects, such as controls or sprites, and storing them in lists or
other collections. A common way to do this is to have the constructor
of one object, say a certain implementation of a screen class, “new up”
all the sprites and controls. In many cases, many types of screens are
used in a game, all having different objects inheriting from the same
In the book, Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et. al., one of the object creation patterns is called a factory.
An abstract factory can define the interface for creating objects.
Different implementations of the abstract factory carry out the
concrete tasks of constructing objects with multiple parts. Think of it
this way: a constructor creates a single object and a factory creates
and assembles these objects into a working mechanism of some sort.
an abstract factory that builds screens. The fictional game engine in
this example could define screens as components that have screen
elements, a background, and a logic class that accepts control
messages. Here's an example:
class SaveGameScreenFactory : public IScreenFactory
virtual IScreenElements * const BuildScreenElements() const;
virtual ScreenBackgroundSprite * const BuildScreenBackgroundSprite() const;
virtual IScreenLogic * const BuildScreenLogic() const;
The code that builds screens will call the methods of the IScreenFactory
interface, each one returning the different objects that make the
screen including screen elements such as buttons and sprites, a
background, or the logic that runs the screen. As all interface classes
tend to enforce design standards, factories tend to enforce orderly
construction of complicated objects. Factories are great for screens,
animations, AI, or any nontrivial game object.
more, factories can help you construct these mechanisms at the right
time. One of the neatest things about the factory design pattern is a
delayed instantiation feature. You could create factory objects, push
them into a queue, and delay calling the “BuildXYZ” methods until you
were ready. In the screen example, you might not have enough memory to
instantiate a screen object until the active one is destroyed. The
factory object is tiny, perhaps a few tens of bytes, and can easily
exist in memory until you are ready to fire it.
Use Streams to Initialize Objects
persistent object in your game should implement an overloaded
constructor that takes a stream object as a parameter. If the game is
loaded from a file, objects can use the stream as a source of
parameters. Here's an example to consider:
//... A better idea! Use a default constructor and an Init method.
AnimationPath(); Initialize (InputStream & stream);
Initialize (std::vector<AnimationPathPoint> const & srcPath);
//Of course, lots more code follows.
class has a default constructor, and two ways to initialize it. The
first is through a classic parameter list, in this case a list of AnimationPathPoints.
The second initializes the class through a stream object. This is cool
because you can initialize objects from disk, a memory stream, or even
the network. If you want to load game objects from disk, as you would
in a saved game, this is exactly how you do it.
you read the first edition of this book, perhaps you remember that this
section suggested you use input streams in the constructor of an
object, like this:
AnimationPath (InputStream & stream);
that was a horrible idea, and I'm not too big to admit it either. The
kind “corrections” posted on the web helped me catch this one. The
unkind ones I'll happily forget! Here's why it is a bad idea: a bad
stream will cause your constructor to fail. You can never trust the
content of a stream; it could be coming from a bad disk file, or even
from hacked network packets. Ergo, construct objects with a default
constructor you can rely on, and create initialization methods for
This article is excerpted from Paraglyph Press' Game Coding Complete, (ISBN
1-932111-91-3) by Mike McShaffry.