A
s developers continue to embrace object orientation, the systems that power games are growing increasingly flexible, and inherently more complex. Such sys-tems now regularly contain many different types and classes; counts of over 1000 are not unheard of. Coping with so many different types in a game engine can be a chal-lenge in itself. A type can really mean anything from a class, to a struct, to a standard data type. This gem discusses managing types effectively by providing ways of query-ing their relations to other types, or accessquery-ing information about their type at runtime for query or debug purposes. Toward the end of the gem, an approach for supporting persistent objects is suggested with some ideas about how the method can be extended.Introducing the Dynamic Type Information Class
In our efforts to harness the power of our types effectively, we'll be turning to the aid of one class in particular: the dynamic type information (DTI) class. This class will store any information that we may need to know about the type of any given object or structure. A minimal implementation of the class is given here:
class dtiClass {
private:
char* szName;
dtiClass* pdtiParent;
public:
dtiClass();
dtiClass( char* szSetName, dtiClass* pSetParent );
virtual -dtiClass();
const char* GetName();
bool SetName( char* szSetName );
dtiClass* GetParent();
bool SetParent( dtiClass* pSetParent );
38
In order to instill DTI into our engine, all our classes will need a dtiClass as a sta-tic member. It's this class that allows us to access a class name for debug purposes and query the dtiClass member of the class's parent. This member must permeate the class tree all the way from the root class down, thus ensuring that all game objects have '~^J^__J) access to information about themselves and their parents. The implementation
ofdti-ONTHICO Class can be found in the code on the accompanying CD.
Exposing and Querying the DTI
Let's see how we can begin to use DTI by implementing a very simple class tree as described previously. Here is a code snippet showing a macro that helps us define our static dtiClass member, a basic root class, and simple initialization of the class's type info:
#define EXPOSE_TYPE \ public: \
static dtiClass Type;
class CRootClass {public:
EXPOSE_TYPE;
CRootClass() {};
virtual -CRootClass() {};
};
dtiClass CRootClass::Type( "CRootClass", NULL );
By including the EXPOSE_TYPE macro in all of our class definitions and initial-izing the static Type member correctly as shown, we've taken the first step toward instilling dynamic type info in our game engine. We pass our class name and a pointer to the class's parent's dtiClass member. The dtiClass constructor does the rest, setting up the szName and pdtiParent members accordingly.
We can now query for an object's class name at runtime for debug purposes of other type-related cases, such as saving or loading a game. More on that later, but for now, here's a quick line of code that will get us our class name:
// Let's see what kind of object this pointer is pointing to const char* szGetName = pSomePtr->Type.GetName();
In the original example, we passed NULL in to the dtiClass constructor as the class's parent field because this is our root class. For classes that derive from others, we just need to specify the name of the parent class. For example, if we were to specify a child class of our root, a basic definition might look something like this:
class CChildClass : public CRootClass {
EXPOSE TYPE;
// Constructor and virtual Destructor go here };
dtiClass CChildClass::Type( "CChildClass", &CRootClass::Type );
Now we have something of a class tree growing. We can access not only our class's name, but the name of its parent too, as long as its type has been exposed with the EXPOSE_TYPE macro. Here's a line of code that would get us our parent's name:
// Let's see what kind of class this object is derived from char* szParentName = pSomePtr->Type.GetParent()->GetName();
Now that we have a simple class tree with DTI present and know how to use that information to query for class and parent names at runtime, we can move on to implementing a useful method for safeguarding type casts, or simply querying an object about its roots or general type.
Inheritance Means "IsA"
Object orientation gave us the power of inheritance. With inheritance came polymor-phism, the ability for all our objects to be just one of many types at any one time. In many cases, polymorphism is put to use in game programming to handle many types of objects in a safe, dynamic, and effective manner. This means we like to ensure that objects are of compatible types before we cast them, thus preventing undefined behavior. It also means we like to be able to check what type an object conforms to at runtime, rather than having to know from compiler time, and we like to be able to do all of these things quickly and easily.
Imagine that our game involves a number of different types of robots, some purely electronic, and some with mechanical parts, maybe fuel driven. Now assume for instance that there is a certain type of weapon the player may have that is very effective against the purely electronic robots, but less so against their mechanical counterparts. The classes that define these robots are very likely to be of the same basic type, meaning they probably both inherit from the same generic robot base class, and then go on to override certain functionality or add fresh attributes. To cope with varying types of specialist child classes, we need to query their roots. We can extend the dtiClass introduced earlier to provide us with such a routine. We'll call the new member function IsA, because inheritance can be seen to translate to "is a type of." Here's the function:
bool dtiClass::IsA( dtiClass* pType ) {
dtiClass* pStartType = this;
while( pStartType ) {
if ( pStartType == pType )
return true;
else
pStartType = pStartType->GetParent();
return false;
If we need to know whether a certain robot subclass is derived from a certain root class, we just need to call IsA from the object's own dtiClass member, passing in the static dtiClass member of the root class. Here's a quick example:
CRootClass* pRoot;
CChildClass* pChild = new CChildClass();
if ( pChild->Type.IsA( &CRootClass::Type ) ) pRoot = (CRootClass*)pChild;
We can see that the result of a quick IsA check tells us whether we are derived, directly or indirectly, from a given base class. Of course, we might use this fact to go on and perform a safe casting operation, as in the preceding example. Or, maybe we'll just use the check to filter out certain types of game objects in a given area, given that their type makes them susceptible to a certain weapon or effect. If we decide that a safe casting operation is something we'll need regularly, we can add the following
^-—_1-^ function to the root object to simplify matters. Here's the definition and a quick example; the function's implementation is on the accompanying CD:
// SafeCast member function definition added to CRootClass void* SafeCast( dtiClass* pCastToType );
// How to simplify the above operation
pRoot = (CRootClass*)pChild->SafeCast( &CRootClass::Type );
If the cast is not safe (in other words, the types are not related), dien the value will evaluate to nothing, and pRoot will be NULL.
Handling Generic Objects
Going back to our simple game example, let's consider how we might cope with so many different types of robot effectively. The answer starts off quite simple: we can make use of polymorphism and just store pointers to them all in one big array of generic base class pointers. Even our more specialized robots can be stored here, such as CRobotMech (derived from CRobof), because polymorphism dictates that for any type requirement, a derived type can always be supplied instead. Now we have our vast array of game objects, all stored as pointers to a given base class. We can iterate
over them safely, perhaps calling virtual functions on each and getting the more spe-cialized (overridden) routines carried out by default. This takes us halfway to han-dling vast numbers of game objects in a fast, safe, and generic way.
As part of our runtime type info solution, we have the IsA and SafeCast routines that can query what general type an object is, and cast it safely up the class tree. This is often referred to as up-casting, and it takes us halfway to handling vast numbers of game objects in a fast, safe, and generic way. The other half of the problem comes with down-casting—casting a pointer to a generic base class safely down to a more spe-cialized subclass. If we want to iterate a list of root class pointers, and check whether each really points to a specific type of subclass, we need to make use of the dynamic casting operator, introduced by C++.
The dynamic casting operator is used to convert among polymorphic types and is both safe and informative. It even returns applicable feedback about the attempted cast. Here's the form it takes:
dynamic_cast< type-id >(expression)
The first parameter we must pass in is the type we wish expression to conform to after the cast has taken place. This can be a pointer or reference to one of our classes.
If it's a pointer, the parameter we pass in as expression must be a pointer, too. If we pass a reference to a class, we must pass a modifiable l-value in the second parameter. Here are two examples:
// Given a root object (RootObj), on pointer (pRoot) we // can down-cast like this
CChildClass* pChild = dynamic_cast<CChildClass*>(pRoot);
CChildClass& ChildObj = dynamic_cast<CChildClass&>(RootObj);
To gain access to these extended casting operators, we need to enable embedded runtime type information in the compiler settings (use the /GR switch for Microsoft Visual C++). If the requested cast cannot be made (for example, if the root pointer does not really point to anything more derived), the operator will simply fail and the expression will evaluate to NULL. Therefore, from the preceding code snippet, (f :,js*:*:*'% pChild would evaluate to NULL IfpRoot really did only point to a CRootClass object.
ON me a> If the cast of RootObj failed, an exception would be thrown, which could be contained with a try I catch block (example is included on the companion CD-ROM).
The dynamic_cast operator lets us determine what type is really hidden behind a pointer. Imagine we want to iterate through every robot in a certain radius and deter-mine which ones are mechanical models, and thus immune to the effects of a certain weapon. Given a list of generic CRobot pointers, we could iterate through these and perform dynamic casts on each, checking which ones are successful and which resolve to NULL, and thus exacting which ones were in fact mechanical. Finally, we can now safely down-cast too, which completes our runtime type information solution. The
/ c -., code on the companion CD-ROM has a more extended example of using the on m CD dynamic casting operator.
Implementing Persistent Type Information
Now that our objects no longer have an identity crisis and we're managing them effec-tively at runtime, we can move on to consider implementing a persistent object solu-tion, thus extending our type-related capabilities and allowing us to handle things ,- c ") such as game saves or object repositories with ease. The first thing we need is a bare-mtmco bones implementation of a binary store where we can keep our object data. An
exam-ple imexam-plementation, CdtiBin can be found on the companion CD-ROM.
There are a number of utility member functions, but the two important points are the Stream member function, and the friend « operators that allow us to write out or load die basic data types of the language. We'll need to add an operator for each basic type we want to persist. When Stream is called, the data will be either read from the file or written, depending on the values of m_bLoading and m_bSaving.
To let our classes know how to work with the object repositories we need to add the Serialize function, shown here:
virtual void Serialize( CdtiBin& ObjStore );
Note that it is virtual and needs to be overridden for all child classes that have additional data over their parents. If we add a simple integer member to CRootClass, we would write the Serialize function like this:
void CRootClass::Serialize( CdtiBin& ObjStore ) {
ObjStore « iMemberlnt;
}
We would have to be sure to provide the friend operator for integers and CdtiBin objects. We could write object settings out to a file, and later load them back in and repopulate fresh objects with die old data, thus ensuring a persistent object solution for use in a game save routine. All types would thus know how to save themselves, making our game save routines much easier to implement.
However, child classes need to write out their data and that of their parents.
Instead of forcing the programmer to look up all data passed down from parents and adding it to each class's Serialize member, we need to give each class access to its par-ent's Serialize routine. This allows child classes to write (or load) their inherited data before their own data. We use the DECLAREJSUPER macro for this:
#define DECLARE_SUPER(SuperClass) \ public: \
typedef Superclass Super;
class CChildClass
DECLARE_SUPER(CRootClass);
This farther extends our type solution by allowing our classes to call their imme-diate parents' versions of functions, making our class trees more extensible.
CRootClass doesn't need to declare its superclass because it doesn't have one, and thus its Serialize member only needs to cope with its own data. Here's how CChild-Class::Serialize calls CRootClass:Serialize before dealing with some of its own data (added specifically for the example):
void CChildClass::Serialize( CdtiBin& ObjStore ) { Super::Serialize( ObjStore );
ObjStore « fMemberFloat « iAnotherlnt;
}
A friend operator for the float data type was added to support the above. Note that the order in which attributes are saved and loaded is always the same. Code showing how to create a binary store, write a couple of objects out, and then repopu-late the objects' attributes can be found on the companion CD-ROM.
As long as object types are serialized in the same order both ways, their attributes will remain persistent between saves and loads. Adding the correct friend operators to the CdtiBin class adds support for basic data types. If we want to add user-defined structures to our class members, we just need to write an operator for coping with that struct. With this in place, all objects and types in the engine will know precisely how to save themselves out to a binary store and read themselves back in.
Applying Persistent Type Information to a Game Save Database
As mentioned previously, objects need to be serialized out and loaded back in the same order. The quickest and easiest method is to only save out one object to the game saves, and then just load that one back in. If we can define any point in the game by constructing some kind of game state object that knows precisely how to serialize itself either way, then we can write all our game data out in one hit, and read it back in at any point. Our game state object would no doubt contain arrays of objects. As long as the custom array type knows how to serialize itself, and we have all the correct CdtiBin operators written for our types, everything will work. Saving and loading a game will be a simple matter of managing the game from a high-level, all-encompassing containment class, calling just the one Serialize routine when needed.
Conclusion
There is still more that could be done than just the solution described here. Support-ing multiple inheritance wouldn't be difficult. Instead of storSupport-ing just the one parent pointer in our static dtiClass, we would store an array of as many parents a class had, specifying the count and a variable number of type classes in a suitable macro, or by extending the dtiClass constructor. An object flagging system would also be useful, and would allow us to enforce special cases such as abstract base classes or objects we only ever wanted to be contained in other classes, and never by themselves ("con-tained classes").
References
[Meyers98] Meyers, Scott D., Effective C++ 2ndEdition, Addison-Wesley, 1998.
[Wilkie94] Wilkie, George, Object-Oriented Software Engineering, Addison-Wesley, 1994.
[EberlyOO] Eberly, David H., 3D Game Engine Design, Morgan Kauffman, 1999-2000.
[WakelingOl] Wakeling, Scott J., "Coping with Class Trees," available online at www.chronicreality.com/articles, March 12, 2001.