Safari Books Online is a digital library providing on-demand subscription access to thousands of learning resources.
One of the keys to writing good software is designing robust systems that can stand the test of time. Game programming is extremely volatile. A designer can change the whole game out from under you, requiring you to rewrite large chunks of your game. There's no way around this, because it's simply the nature of the beast. You can mitigate the effect of these kinds of changes by having a strong, flexible architecture.
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 capabilities 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 such as C or PASCAL. 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. Simplicity is good, but the down side is that it's easy to get yourself into trouble without realizing it. A great 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. While you can use structured exception handling to catch a failure in a constructor, it is a much better practice to write constructors that can't fail. Another example is the misuse of virtual functions. For example, a naive programmer might make every method in the class virtual, thinking that future expandability for everything was good. Well, he'd be wrong. On some platforms, virtual functions can be very expensive. 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 of an artist than a 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 do the following:
Avoid hidden code that performs nontrivial operations.
Keep your class hierarchies as flat as possible.
Be aware of the difference between inheritance and composition.
Avoid abusing virtual functions.
Use interface classes and factories.
Encapsulate the components of your system that are most likely to change.
Use streams in addition to constructors to initialize objects.
Copy constructors, operator overloads, and destructors are all party to the “nasty” hidden code problems that plague game developers. This kind of code can cause you a lot of problems when you least expect. 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 it to a remote computer and download a few megabytes of MP3 files, your teammates are going to have you drawn and quartered.
My advice is that you should try to avoid copy constructors and operator overloads that perform nontrivial operations. If something looks simple, it should be simple and not 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, like 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 like floats or doubles.
Game programmers love playing with neat technology, and sometimes their sense of elegance drives them to push nontrivial 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 4 × 4 matrix class. Overloaded operators are perfectly fine for classes like this because the clarity of the resulting code is especially important and useful.
One thing to watch out for is that the C++ compiler will magically generate functions in your class. It will silently generate a copy constructor, copy assignment operator, and destructor for you if you don't create them yourself. If you don't create any constructors, it will also generate a default constructor. These will all be public functions. This can cause unintended side effects if you're not aware of what's happening under the covers. To get around this, you can make copy constructors and assignment operators private, which 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 buffer.
A recurring theme 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. It's important to remember that you're not writing code for the compiler, you're writing code for other programmers. The compiler will be just as happy with clean code as it will with sloppy code. The same is not true for another programmer.
One of the most common mistakes game programmers make is that they either overdesign or underdesign their classes and class hierarchies. Getting your class structure well designed for your particular needs takes real practice.
A good rule of thumb is that each class should have a single responsibility in your code base and should have inheritance trees that are no more than two or three levels deep. As with anything, there are always exceptions to this rule, but you should strive to flatten your hierarchy as much as possible.
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 comes from the reluctance on the programmer's part to make new, tightly focused classes. In the source code that accompanies this 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 The Sims Medieval, there was a class that fell very neatly into the Blob category. Our Sim class became a dumping ground for every little extra timer, variable, and tracking bit that could be remotely tied to a Sim. Entire systems would be written inside this one class. By the end of the project, the Sim.cs file was 11,491 lines of code, and it was nearly impossible to find anything.
I try always to use a flat class hierarchy. Whenever possible, it starts 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.
Game programmers love to debate the topics of inheritance and composition. Inheritance is used when an object has evolved from another object, or when a child object is a version of the parent object. Composition 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. You might have a base control class to handle things like mouse and keyboard events, positioning, and anything else that all controls need to know how to do. When you create a control such as a button or check box, you will inherit from this control. A check box is a control. Then you might create a window that can contain a bunch of these controls. The window has a control or, in this case, many controls. You window is most likely a valid UI control as well, so it might be fair to say that that your window is a control, too. When you make a choice about inheritance or composition, 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 that the CPU doesn't give a damn if you inherit or compose. Your fellow programmers will care, so try to be careful and clear.
Virtual 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++.
Take 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.
An 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, would 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 himself what hell he is about to unleash. One of the best examples of this is by 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.
If you're using Visual Studio 2010 or above, you can take advantage of the keywords override and sealed. The override keyword tells the compiler that you are overriding a virtual function from the base class. It will generate an error if it can't find that function. The sealed keyword tells the compiler that subclasses aren't allowed to override the virtual function anymore. If you have a subclass that attempts to override it, it will generate an error. Here's a quick example of their usage:
class Base
{
public:
virtual void Go(void);
};
class Sub1 : public Base
{
public:
// If Base didn't declare this function with this exact signature,
// the compiler would kick out an error.
virtual void Go(void) override;
};
class Sub2 : public Sub1
{
public:
// If you create a new subclass inheriting from Sub2 and attempt
// to override this method, the compiler will kick out an error.
virtual void Go(void) sealed;
};
C# and other languages have been doing this for a long time now. I'm happy to see C++ starting to do the same.
Let the Compiler Help You
If you ever change the nature of anything that is currently in wide use, virtual functions included, I suggest you actually change its name. The compiler will find each and every use of the code, and you'll be forced to look at how the original was put to use. It's up to you if you want to keep the new name. I suggest you do, even if it means changing every source file.
When you decide to make a function virtual, what you are communicating to other programmers is that you intend for your class to be inherited from by other classes. The virtual functions serve as an interface for what other programmers can change. By overriding your virtual functions and choosing whether or not to call your implementations, they are changing the behavior of your class. Sometimes this is exactly what you intend. The Process class you'll see in Chapter 7, “Controlling the Main Loop,” has a virtual VOnUpdate() method that is meant to be overridden to allow you to define the behavior of your specific process.
Oftentimes, making an Update() function virtual is not the best way of doing things. For example, say you have a class that processes a creature. You have an update function that runs some AI, moves the creature, and then processes collisions. Instead of making your update function virtual, you could make three separate protected virtual functions: one for AI, one for movement, and one for collision processing, each with a default implementation. The subclass can override one or more of these functions, but not the update function. So subclasses can't change the order of operations, they can only change what happens at each step. This is called the template method design pattern and is very handy. In fact, I used it recently at work to allow subclasses to redefine how interactions are chosen and scored.
If you're on the other side and trying to extend a class by deriving a subclass from it and overriding some virtual functions, you should make sure that you're doing it for the right reasons. If you find yourself significantly altering its behavior, you should step back and consider if inheritance is the right solution. One solution might be composition, where you write a new class that has the other class as a member.
Try to look at classes and their relationships like 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 12 source files to see all the code for a particular class.
Interface classes are those that contain nothing but pure virtual functions. They form the top level in any class hierarchy. Here's an example:
class IAnimation
{
public:
virtual void VAdvance(const int deltaMilliseconds) = 0;
virtual bool const VAtEnd() const = 0;
virtual int const VGetPosition() const = 0;
};
typedef std::list<IAnimation *> AnimationList;
This sample interface class defines simple behavior that is common for a timed animation. You 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:
AnimationList::iterator end = animList.end();
for(AnimationList::iterator itr = animList.begin(); itr != end; ++itr)
{
(*itr).VAdvance( delta );
}
Whenever possible, you should have systems depend on these interfaces instead of the implementation classes. Two different systems should never know about each other's implementation classes. Interface classes act like a gate into a particular system in the engine. Outsiders are only able to call the interface functions to interact with the system; they don't know or care how it gets done.
Rewriting Your Graphics Engine Without Killing Your Game
When I was at Super-Ego Games, we landed a deal with Sony to make Rat Race on the then-unreleased PlayStation 3. None of us had ever made a console game, and the engine was very PC-centric. We devised a scheme we called the Render Skin. This was a layer of abstraction where all graphics and sound functionality would live. The entire thing was made up of a series of interface classes that wrapped some piece of functionality. The appropriate implementation classes were instantiated at runtime based on compiler flags. Once we got this system working, we were able to replace our old DirectX rendering system with a new rendering system that worked on the PS3 without keeping the designers or gameplay programmers blocked. None of the code that called into the Render Skin knew or cared which engine it was using, so the graphics programmers could port everything over without stepping on anyone's toes.
Another great benefit of using interface classes is they reduce compile time dependencies. The interfaces can be defined in a single #include file, or a very small number of them, and because they hide all the disgusting guts of implementation classes, there's very little for the compiler to do but register the class and move on.
Games tend to build complex objects, such as controls or sprites, and store 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 parents.
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. A factory creates and assembles these objects into a working mechanism.
Imagine 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
{
public:
SaveGameScreenFactory();
virtual IScreenElements * const BuildScreenElements() const;
virtual ScreenBackground * const BuildScreenBackground() 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 like controls, 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.
What's 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 was 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.
Factories and interfaces work hand-in-hand. In the previous example, each of the objects being returned by the factory is an interface, so the calling code is decoupled from the implementation of these objects. In other words, the system that's using the IScreenElements object doesn't need to know which specific screen element is being instantiated. All it needs to know is what the interface is. You can freely swap this with any other object that comforms to the same interface.
Whenever I'm designing a new system, I'm always looking for the parts that are the most likely to change. I try to isolate those pieces as much as I can so that when they change, it has little or no effect on the rest of the engine. Your goal is make it easy to modify and extend functionality so that when a designer comes to you and says “let's change this feature so that it does something else instead,” you don't go insane rewriting huge chunks of your game.
For example, let's say I want to build an AI system. I want to create a number of different creatures with different behaviors. I could simply write all of these bahaviors in a big hard-coded function, or I could encapsulate these different behaviors into objects that can be reused on different creatures. Each creature can have some set of behaviors that defines its overall AI. Since you have your behaviors separate from each other, you can modify each one without worrying about how it will affect the other ones.
You can take this concept a step further and separate the code that chooses which behavior to run next. Not only can you mix and match behaviors, but you can also mix and match the transitions between those behaviors. Any of these components can change without affecting any other component in your system. This is exactly what I did on Drawn to Life for the enemy AI.
Another thing that often changes is your rendering system. We've chosen to use Direct3D in this book because of its accessibility, but that doesn't mean you can't use OpenGL. In a real game engine, you typically have multiple build configurations for different platforms, each with a different renderer. That's exactly what we did for The Sims Medieval. It used DirectX for the PC build and OpenGL for the Mac build.
Learning to spot the things that are likely to change is something that comes with experience. In general, any major system you build should be as decoupled as possible from every other major system. Interfaces, factories, and other techniques are the tools to enable you to do this.
There is an amazing book called Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et al., which I mentioned previously in this chapter. Many of these design patterns, such as the Observer pattern and the Strategy pattern, are aimed at decoupling different components in software. I highly recommend that you check this book out. It's one of those books that should be on every programmer's bookshelf.
Any persistent object in your game should implement a method that takes a stream as a parameter and reads the stream to initialize the object. If the game is loaded from a file, objects can use the stream as a source of parameters. Here's an example to consider:
class AnimationPath
{
public:
AnimationPath();
Initialize (std::vector<AnimationPathPoint> const & srcPath);
Initialize (InputStream & stream);
//Of course, lots more code follows.
};
This 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 a disk, a memory stream, or even the network. If you want to load game objects from a disk, as you would in a save game, this is exactly how you do it.
Some programmers try to do stream initialization inside an object's constructor:
AnimationPath (InputStream & stream);
Here's why this is a bad idea—a bad stream will cause your constructor to fail, and you'll end up with a bad object. You can never trust the content of a stream. It could be coming from a bad disk file or hacked network packets. Ergo, construct objects with a default constructor you can rely on and create initialization methods for streams.
Exercise Your Load/Save System
Test your stream initializers by loading and saving your game automatically in the DEBUG build at regular intervals. It will have the added side effect of making sure that programmers keep the load/save code pretty fast.