Charles.river.game.Programming.gems.2.Oct.2001.ISBN.1584500549

551
GameGems I I Converted by Nema

description

Charles.river.game.Programming.gems.2

Transcript of Charles.river.game.Programming.gems.2.Oct.2001.ISBN.1584500549

  • GameGems I IConverted by Nema

  • Optimization for C++ GamesAndrew Kirmse, LucasArts [email protected]

    Well-written C+ + game s ar e ofte n mor e maintainabl e an d reusabl e tha n thei rplain C counterparts arebut is it worth it? Can complex C++ programs hopeto matc h traditiona l C programs i n speed ?

    With a good compiler and thorough knowledge of the language, i t is indeed pos-sible to create efficient games in C++. This gem describes techniques you can use tospeed up games in particular. I t assumes that you're already convinced of the benefitsof using C++, and that you're familiar with the general principles of optimization (seeFurther Investigations for these).

    One general principle that merits repeating is the absolute importance of profil-ing. I n th e absenc e of profiling, programmer s ten d to mak e tw o type s o f mistakes.First, the y optimize the wrong code. The great majority of a program is no t perfor-mance critical, so any time spent speeding it up is wasted. Intuition about which codeis performanc e critica l i s untrustworthyonl y b y direc t measuremen t ca n yo u b esure. Second , programmer s sometime s mak e "optimizations " tha t actuall y slow downthe code. This is particularly a problem in C++, where a deceptively simple line canactually generate a significant amount of machine code. Examin e your compiler's out-put, an d profil e often .

    Object Construction and DestructionThe creatio n an d destruction o f objects i s a centra l concep t i n C++ , an d i s th e mai narea where th e compile r generate s cod e "behin d your back. " Poorl y designed pro -grams ca n spen d substantia l tim e callin g constructors , copyin g objects , an d generat -ing costly temporary objects. Fortunately , common sense and a few simple rules canmake object-heavy code ru n within a hair's breadt h o f the spee d of C.

    Dela y construction of objects unti l they're needed.The fastes t cod e i s tha t whic h neve r runs ; wh y creat e a n objec t i f you're no t

    going to use it? Thus, in the following code:

    void Function(int arg)

    5

    G

    1.1m "'

  • Section 1 General Programming

    Object obj;if (arg *= 0)

    return;

    even when arg is zero, we pay the cost of calling Object's constructor and destruc-tor. If arg is often zero, and especially if Object itself allocates memory, this wastecan add up in a hurry. Th e solution, o f course, i s t o move the declaration o f objuntil after the //check.

    Be careful about declaring nontrivial objects in loops, however. If you delay con-struction of an object unti l it' s needed in a loop, you'l l pay for the constructionand destruction of the object on every iteration. It' s bette r to declare the objectbefore th e loop an d pay these cost s onl y once . I f a function i s calle d inside a ninner loop, and the function creates an object on the stack, you could instead cre-ate the object outside the loop and pass it by reference to the function.

    Use initializer lists.Consider the following class:

    class Vehicle{public:

    Vehicle(const std::string &name) // Don't do this!{

    mName = name;}

    private:std: : string mName;

    Because member variables are constructed before the body of the constructor isinvoked, thi s code call s the constructo r for the string mName, an d then call s the= operator to copy in the object's name. What's particularly bad about this exam-ple is that the default constructor for string may well allocate memory in fact,more memor y tha n ma y b e necessar y to hol d th e actua l nam e assigne d t o th evariable in the constructor for Vehicle. The following code is much better , andavoids the call to operator =. Further , give n more information (in this case , th eactual string to be stored) , th e nondefaul t string constructor can often be moreefficient, and the compiler may be able to optimize away the Vehicle constructorinvocation when the body is empty:

    class Vehicle{public:

    Vehicle(const std::string &name) : mName(name){}

    private:

  • 1.1 Optimization for C++ Games

    std::string mName;

    Prefer preincrement to postincrement.The problem with writing x = y++ is that the increment function has to make a

    copy of the origina l value o f y, incremen t y, an d the n retur n th e origina l value .Thus, postincremen t involve s th e constructio n o f a temporar y object , whil epreincrement doesn't . Fo r integers , there' s n o additiona l overhead , bu t fo r user -defined types, this is wasteful. You should use preincrement whenever you havethe option. Yo u almos t always hav e th e option in for loop iterators .Avoid operators that return by value.

    The canonical way to write vector addition in C++ is this:

    Vector operator+(const Vector &v1, const Vector &v2)

    This operator must return a new Vector object, and furthermore, it must returnit by value. While this allows useful and readable expressions like v = v 1 + z>2, thecost of a temporary construction and a Vector copy is usually too much for some-thing called as often as vector addition. It' s sometimes possible to arrange code sothat the compiler is able to optimize away the temporary object (thi s i s knownas th e "retur n valu e optimization") , bu t i n general , it' s bette r t o swallo w you rpride and write the slightly uglier, but usually faster:

    void Vector::Add(const Vector &v1, const Vector &v2)

    Note tha t operator+= doesn' t suffer from the same problem, a s i t modifies it sfirst argumen t in place, and doesn't need to return a temporary. Thus, you shoulduse operators like += instead of + when possible.

    Use lightweight constructors.Should the constructor for the Vector class in the previous example initialize its

    elements t o zero? Thi s may come in handy in a few spots i n your code, bu t i tforces every caller to pay the price of the initialization, whether they use i t or not.In particular , temporar y vectors an d member variables will implicitl y incur theextra cost.

    A good compiler may well optimize away some of the extra code, bu t why takethe chance? As a general rule, you want an object's constructor to initialize each ofits member variables, because uninitialized data can lead to subtle bugs. However,in smal l classe s tha t ar e frequentl y instantiated , especiall y a s temporaries , yo ushould be prepared to compromise this rule for performance. Prime candidates inmany games are the Vector and Matrix classes. These classes should provide medi-ods (o r alternat e constructors ) t o se t themselve s t o zer o an d th e identity , respec -tively, but the default constructor should be empty.

  • Section 1 General Programming

    As a corollary to thi s principle, you should provide additiona l constructors toclasses where this will improve performance. I f the Vehicle class in our secondexample were instead written like this:

    class Vehicle{ .public:

    Vehicle ()

    void SetName(const std: :string &name){

    mName = name;

    private:std: : string mName;

    we'd incur the cost of constructing mName, and then setting it again later via Set-Name(). Similarly , it' s cheape r t o us e cop y constructor s tha n t o construc t a nobject and then call operator=. Prefer constructing an object this way Vehiclevl(v2) to this way Vehicle vl; vl = v2;.

    If you wan t t o preven t th e compile r from automaticall y copying an objec t fo ryou, declare a private copy constructor and operator= for the object's class, butdon't implement either function. Any attempt to copy the objec t will the n resul tin a compile-time error. Also get into the habit of declaring single-argument con-structors as explicit, unles s you mean to use them as type conversions. This pre-vents the compiler from generating hidden temporary objects when convertingtypes.

    Preallocate and cache objects.A game will typically have a few classes that it allocates and frees frequently, such

    as weapons or particles. In a C game, you'd typically allocate a big array up frontand use them as necessary. With a little planning, you can do the same thing inC++. The idea is that instead of continually constructing and destructing objects,you reques t new ones and return old ones to a cache. The cache can be imple-mented as a template, so that it works for any class, provided that the class has adefault constructor. Code for a sample cache class template is on the accompany-ing CD.

    You can either allocate objects to fill the cache as you need them, or preallocateall of the objects up front. If, in addition, you maintain a stack discipline on theobjects (meanin g that before you delete object X, you first delete all objects allo-cated after X), you can allocate the cache in a contiguous block of memory.

  • 1.1 Optimization for C++Games 9

    Memory Management C++ applications generally need to be more aware of the details of memory manage-ment than C applications do . I n C , al l allocations are explici t though mallocQ andfreeQ, whil e C+ + ca n implicitl y allocat e memor y whil e constructin g temporar yobjects and member variables. Most C++ games (like most C games) will require theirown memory manager.

    Because a C++ game is likely to perform many allocations, it must be especiallycareful abou t fragmentin g th e heap . On e optio n i s t o tak e on e o f the traditiona lapproaches: either don't allocate any memory at all after the game starts up, or main-tain a large contiguous block of memory that is periodically freed (between levels, forexample). On modern machines, such draconian measures are not necessary, if you'rewilling to be vigilant about your memory usage.

    The first step is to override the global new and delete operators. Use custom imple-mentations o f diese operator s t o redirec t th e game' s mos t commo n allocation s awa yfrom mallocQ and into preallocated blocks of memory. For example, if you find that youhave at most 10,000 4-byte allocations outstanding at any one time, you should allocate40,000 bytes up front and issue blocks out as necessary. To keep track of which blocksare free, maintain a. free list by pointing each free block to the next free block. On allo-cation, remove the front block from the list, and on deallocation, add the freed block tothe front again. Figure 1.1. 1 illustrate s how the free list of small blocks might wind itsway through a contiguous larger block after a sequence of allocations and frees.

    used free used usedtfree free

    _ _ A .~ .~ T

    FIGURE 1.1.1 A linked free list.

    You'll typically find that a game has many small, short-lived allocations, and thusyou'll wan t t o reserv e spac e fo r man y smal l blocks . Reservin g man y large r block swastes a substantial amount of memory for those blocks that are not currently in use;above a certain size, you'll want to pass allocations off to a separate large block alloca-tor, o r just to mallocQ.

    Virtual FunctionsCritics o f C++ i n game s ofte n poin t t o virtua l function s a s a mysteriou s featur ethat drains performance. Conceptually, the mechanism is simple. To generate a virtualfunction cal l on an object, the compiler accesses the objects virtual function table,

  • 10 Section 1 General Programming

    retrieves a pointer to the member function, sets up the call, and jumps to the memberfunction's address . Thi s i s t o b e compare d with a function cal l i n C , wher e th e com -piler sets up the call and jumps to a fixed address. The extra overhead for the virtualfunction cal l i s di e indirectio n t o di e virtual function table ; becaus e the addres s o f thecall isn' t know n i n advance , ther e ca n als o b e a penalt y fo r missin g th e processor' sinstruction cache .

    Any substantial C++ program will make heavy use of virtual functions, so the ideais to avoid these calls in performance-critical areas. Here is a typical example:

    class BaseClass{public:

    virtual char *GetPointer() = 0;};class Class"! : public BaseClass{

    virtual char *GetPointer();>;class Class2 : public BaseClass{

    virtual char *GetPointer();}|void Function(BaseClass *pObj){

    char *ptr = pObj->GetPointer();}

    If FunctionQ i s performanc e critical , w e wan t t o chang e di e cal l t o GetPointerfrom virtual to inline. On e way to do this is to add a new protected data member toBaseClass, whic h i s returne d b y an inlin e version o f GetPointerQ, an d se t th e dat amember in each class:

    class BaseClass{public:

    inline char *GetPointerFast(){

    return mpPointer;}

    protected:inline void SetPointer(char *pData){

    mpData = pData;}

    private:char *mpData;

  • 1.1 Optimization for C++Games , . 11

    // classl and class2 call SetPointer as necessary//in member functions

    void Function(BaseClass *pObj){

    char *ptr = pObj->GetPointerFast();}

    A more drastic measure is to rearrange your class hierarchy. If Classl and Class2have only slight differences , i t migh t b e worth combining them int o a single class ,with a flag indicating whether you want the class to behave like Classl o r Class2 atruntime. With this change (and the removal of the pure virtual BaseClass), the Get-Pointer function in the previous example can again be made inline. This transforma-tion is far from elegant , bu t in inne r loops on machines with smal l caches , you'd bewilling to do much worse to get rid of a virtual function call.

    Although each new virtual function adds only the size of a pointer to a per-classtable (usually a negligible cost), the yzrtf virtual function in a class requires a pointer tothe virtual function table on a pet-object basis. This means that you don't want to haveany virtual functions at all in small, frequently used classes where this extra overheadis unacceptable. Becaus e inheritance generally requires the use of one or more virtualfunctions (a virtual destructor if nothing else), you don't want any hierarchy for small,heavily used objects.

    Code SizeCompilers have a somewhat deserved reputation for generating bloated code for C++.Because memory is limited, and because small is fast, it's important to make your exe-cutable as small as possible. The first thing to do is get the compiler on your side. I fyour compiler stores debugging information in the executable , disabl e the generationof debugging information. (Not e tha t Microsoft Visual C++ store s debuggin g infor-mation separate from the executable, so this may not be necessary.) Exception handlinggenerates extra code; ge t rid of as much exception-generating code as possible . Mak esure the linker is configured to strip out unused functions and classes. Enable the com-piler's highes t level o f optimization, an d tr y setting it t o optimiz e fo r size instea d o fspeedsometimes thi s actuall y produce s faste r cod e becaus e o f bette r instructio ncache coherency. (B e sure to verify that intrinsic functions are still enabled if you usethis setting.) Get rid of all of your space-wasting strings in debugging print statements,and have the compiler combine duplicate constant strings into single instances.

    Inlining i s ofte n th e culpri t behin d suspiciousl y large functions . Compiler s ar efree t o respec t o r ignor e you r inline keywords, an d the y may well inlin e function swithout telling you. This is another reason to keep your constructors lightweight, sothat objects on the stack don't wind up generating lots of inline code. Also be carefulof overloaded operators; a simple expression like ml = m2 * m3 can generate a ton of

  • 12 Section 1 General Programming

    inline code if m2 and m3 are matrices. Get to know your compiler's settings for inlin-ing functions thoroughly.

    Enabling runtim e typ e informatio n (RTTI ) require s th e compile r t o generat esome static information for (just about) every class in your program. RTTI is typicallyenabled so that code can cal l dynamic_cast and determine an object's type. Conside ravoiding RTT I an d dynamic_cast entirel y i n orde r t o sav e spac e (i n addition ,dynamic_cast is quite expensive in some implementations). Instead , when you reallyneed to have different behavior based on type, add a virtual function that behaves dif-ferently. This is better object-oriented design anyway. (Note that this doesn't apply tostatic_cast, which is just like a C-style cast in performance.)

    The Standard Template LibraryThe Standard Template Library (STL) i s a set of templates tha t implement commondata structure s an d algorithms , suc h a s dynami c array s (calle d vectors) , sets , an dmaps. Usin g the ST L ca n sav e you a great dea l o f time tha t you'd otherwis e spen dwriting and debugging these containers yourself. Once again, though, you need to beaware o f the detail s o f your STL implementation i f you want maximum efficiency .

    In order t o allo w the maximum rang e o f implementations, th e ST L standard i ssilent in the area of memory allocation. Each operation on an STL container has cer-tain performance guarantees; for example, insertion into a set takes O(log n) time.However, there are no guarantees on a container's memory usage.

    Let's go into detail on a very common problem in game development: you wantto store a bunch of objects (we'l l cal l i t a list of objects, thoug h we won't necessarilystore it in an STL list). Usually you want each object to appear in a list only once, sothat you don' t have to worry about accidentally inserting the objec t into the collectionif it's alread y there. An ST L se t ignore s duplicates , ha s O(lo g n) insertion , deletion ,and lookupthe perfect choice, right?

    Maybe. While it's true that most operations on a set are O(log n), this notationhides a potentially large constant. Although the collection's memory usage is imple-mentation dependent , man y implementations ar e base d o n a red-blac k tree , wher eeach node of the tree stores an element of the collection. It' s common practice to allo-cate a node of the tree every time an element is inserted, and to free a node every timean element is removed. Dependin g on how often you insert and remove elements, thetime spen t i n th e memor y allocato r ca n overshado w an y algorithmi c saving s yo ugained from using a set.

    An alternativ e solution use s a n ST L vector to store elements . A vector is guaran -teed to have amortized constant-time insertion at the end of the collection. What thismeans in practice is that a vector typically reallocates memory only on occasion, say ,doubling its size whenever it's full . When using a vector to store a list of unique ele-ments, you first check the vector to see i f the element is already there, an d if it isn't ,you ad d i t t o th e back . Checkin g the entir e vector will tak e O(n ) time , bu t th e con -stant involved is likely to be small. That' s because all of the elements of a vector are

  • 1.1 Optimization for C++Games 13

    typically store d contiguousl y i n memory , s o checkin g th e entir e vecto r i s a cache -friendly operation. Checking an entire set may well thrash the memory cache, as indi-vidual element s o f th e red-blac k tre e coul d b e scattere d al l ove r memory . Als oconsider that a set must maintain a significant amount of overhead to set up the tree.If all you're storing is object pointers, a set can easily require three to four times thememory of a vector to store the same objects.

    Deletion from a set is O(log n), which seems fast until you consider that it prob-ably also involve s a call t o free(). Deletion from a vector i s O(n) , becaus e everythin gfrom the deleted element to the end of the vector must be copied over one position.However, i f the elements of the vector are just pointers, the copying can all be done ina single call to memcpyO, which is typically very fast. (This is one reason why it's usu-ally preferable to store pointers to object s i n STL collections, a s opposed to objectsthemselves. I f you store objects directly, many extra constructors get invoked duringoperations such as deletion.)

    If you're stil l no t convince d tha t set s an d map s ca n ofte n be mor e troubl e tha nthey're worth, consider the cost of iterating over a collection, specifically:

    for (Collection::iterator it = collection.begin();it != collection.end(); ++it)

    If Collection is a vector, then ++it is a pointer incrementone machine instruc-tion. But when Collection is a set or a map, ++it involves traversing to the next nodeof a red-black tree, a relatively complicated operation that is also much more likely tocause a cache miss, because tree nodes may be scattered all over memory.

    Of course, if you're storing a very large number of items in a collection, and doinglots of membership queries, a set's O(log n) performance could very well be worth thememory cost . Similarly , i f you're onl y usin g th e collectio n infrequently , th e perfor -mance difference may be irrelevant . You should do performance measurements t odetermine what values of n make a set faster. You may be surprised to find that vectorsoutperform sets for all values that your game will typically use.

    That's not quite the last word on STL memory usage, however . It' s important toknow if a collection actually frees its memory when you call the clear() method. If not,memory fragmentation ca n result . Fo r example , i f you star t a game with a n emptyvector, add elements to the vector as the game progresses, and then call clear() whenthe player restarts, the vector may not actually free its memory at all. The empty vec-tor's memor y could stil l b e takin g u p spac e somewher e i n th e heap , fragmentin g it .There are two ways around this problem, i f indeed your implementation works thi sway. First, you can call reserveQ when the vector is created, reserving enough space forthe maximum number of elements that you'll ever need. I f that's impractical, you canexplicitly force the vector to free its memory this way:

    vector v;// ... elements are inserted into v herevector().swap(v); // causes v to free its memory

  • 14 Section 1 General Programming

    Sets, lists , and maps typically don't have this problem, because they allocate andfree each element separately.

    Advanced FeaturesJust because a language has a feature doesn't mean you have to use it. Seemingly sim-ple features can have very poor performance, while other seemingly complicated fea-tures ca n i n fac t perfor m well . Th e darkes t corner s o f C++ ar e highl y compile rdependent make sure you know the costs before using them.

    C++ strings are an example of a feature that sounds great on paper, but should beavoided where performance matters. Consider the following code:

    void Function (const std: :string &str)

    Function ("hello");

    The call to FunctionQ invokes a constructor for a string given a const char *. Inone commercial implementation, this constructor performs a mallocQ, a strlenQ, anda memcpyO, and the destructor immediately does some nontrivial work (because thisimplementation's string s ar e referenc e counted ) followe d b y a freeQ- The memor ythat's allocate d is basically a waste, becaus e th e string "hello" i s alread y in the pro-gram's dat a segment ; we'v e effectivel y duplicate d i t i n memory . I f FunctionQ ha dinstead been declared as taking a const char *, there would be no overhead to the call.That's a high price to pay for the convenience of manipulating strings.

    Templates are an example of the opposite extreme of efficiency. According to thelanguage standard, th e compiler generates code for a template when the template isinstantiated with a particular type. In theory, it sounds like a single template declara-tion would lead to massive amounts of nearly identical code. I f you have a vector ofClassl pointers, and a vector of Class2 pointers, you'll wind up with two copies of vec-tor in your executable.

    The realit y fo r mos t compiler s i s usuall y better . First , onl y templat e membe rfunctions that are actually called have any code generated for them. Second, the com-piler is allowed to generate only one copy of the code, if correct behavior is preserved.You'll generally find that in the vector example given previously, only a single copy ofcode (probably for vector) wil l be generated. Given a good compiler, tem-plates give you all the convenience of generic programming, while maintaining highperformance.

    Some features of C++, such as initializer lists and preincrement, generally increaseperformance, whil e othe r feature s suc h a s overloade d operator s an d RTT I loo kequally innocent bu t carr y serious performanc e penalties . ST L collection s illustrat ehow blindly trusting in a function's documented algorithmic running time can leadyou astray. Avoid the potentially slow features of the language and libraries, and spend

  • 1.1 Optimization for C++Games 15

    some time becoming familiar with the options in your profiler and compiler. You'llquickly learn to design for speed and hunt down the performance problems in yourgame.

    Further InvestigationsThanks to Pete Isensee and Christopher Kirmse for reviewing this gem.Gormen, Thomas, Charles Leiserson, and Ronald Rivest, Introduction to Algorithms,

    Cambridge, Massachusetts, MIT Press, 1990 .Isensee, Peter , C+ + Optimizatio n Strategie s an d Techniques , www.tantalon.com /

    pete/cppopt/main.htm.Koenig, Andrew, "Pre- or Postfix Increment," The C++ Report, June, 1999.Meyers, Scott , Effective C++ , Second Edition, Reading , Massachusetts : Addison -

    Wesley Publishing Co., 1998 .Sutter, Herb , Gur u o f th e Wee k #54 : Usin g Vecto r an d Deque , www.gotw.ca /

    gotw/054.htm.

  • 1.2

    Inline Functions Versus MacrosPeter Dalton, Evans & [email protected]

    ien it comes to game programming, the need for fast, efficient functions cannotbe overstated, especially functions that are executed multiple times per frame.

    Many programmers rely heavily on macros when dealing with common, time-criticalroutines because they eliminate the calling/returning sequence required by functionsthat are sensitive to the overhead of function calls. However , using the tfdefine directiveto implement macros diat look like functions is more problematic than it is worth.

    Advantages of Inline FunctionsThrough th e us e o f inline functions , man y of the inheren t disadvantages o f macroscan easily be avoided. Take, for example, the following macro definition:

    #define max(a,b) ( (a) > (b) ? (a) : (b))

    Let's loo k at what would happen i f we calle d the macro with die following para-meters: max(++x, y). If x = 5 and j/ = 3, the macro will return a value of 7 rather thanthe expected value of 6. This illustrates the most common side effect of macros, thefact that expressions passed as arguments can be evaluated more than once. To avoidthis problem, we could have used an inline function to accomplish die same goal:

    inline int max(int a, int b) { return (a > b ? a : b); }

    By using the inline method, we are guaranteed that all parameters will only beevaluated once because they must, b y definition, follo w all the protocols and typesafety enforced on normal functions.

    Another proble m tha t plague s macros , operato r precedence , follow s fro m di esame problem presented previously, illustrated in the following macro:

    #define square(x) (x*x)

    If we were to call this macro with the expression 2+1, it should become obviousthat die macro would return a result of 5 instead of the expected 9. Th e problem hereis tha t the multiplication operato r has a higher precedence than the addition operator

    16

  • 1.2 Inline Functions Versus Macros 17

    has. Whil e wrappin g al l o f the expression s withi n parenthese s woul d remed y thi sproblem, it could have easily been avoided through the use of inline functions.

    The other major pitfall surrounding macros has to deal with multiple-statementmacros, and guaranteeing that all statements within the macro are executed properly.Again, let's look at a simple macro used to clamp any given number between zero andone:

    #define clamp(a) \if (a > 1.0) a = 1.0; \if (a < 0.0) a = 0.0;

    If we were to use the macro within the following loop:

    for (in t i i = 0 ; i i < N ; ++ii )clamp( numbersToBeClamped[ii] );

    the numbers would not be clamped if they were less than zero. Onl y upon termina-tion of the for loop when = = N would the expression if(numbersToBeClamped[ii] PlaySound ( hSound );

    We need to always include a virtual destructor in our abstract interfaces. Ifwe don't, C+ + will automatically generate a nonvirtual destructor, whichwill cause the real destructor of our specific implementation not to be called(and that is usually a hard bug to track down). Unlike normal member

    functions, we can't just provide a pure virtual destructor, so we need to createan empty function to keep the compiler happy.

    Abstract Interfaces as TraitsA slightly different way to think of abstract interfaces is to consider an interface as aset of behaviors. If a class implements an interface, that class is making a promise thatit wil l behav e i n certai n ways . Fo r example , th e followin g i s a n interfac e use d b yobjects that can be rendered to the screen:

    class IRenderable {public:virtual -IRenderable() {};virtual bool Render () = 0;

    We can design a class to represent 3D objects that inherits from IRenderable andprovides it s ow n metho d t o rende r itsel f on th e screen . Similarly , w e coul d hav e a

  • Section 1 General Programming

    terrain clas s tha t als o inherit s fro m IRenderable and provide s a completel y differen trendering method .

    class GenericSDObject : public IRenderable {public:

    virtual ~Generic3DObject() ;virtual bool Render();// Rest of the functions here

    };The render loop will iterate through al l the objects, an d if they can be rendered ,

    it calls their RenderQ function. Th e real power of the interface comes again from hid-ing the real implementation from the interface: now it is possible to add a completelynew type of object, and as long as it presents the IRenderable interface, the renderingloop wil l be abl e t o rende r i t like any other object . Withou t abstrac t interfaces , th erender loop would have to know about the specific types of object (generic 3D object,terrain, an d s o on ) an d decid e whether t o cal l thei r particula r rende r functions . Cre -ating a ne w typ e o f render-capable objec t would requir e changin g th e rende r loo palong with many other parts o f the code.

    We can check whether an object inherits from IRenderable to know if it can berendered. Unfortunately , tha t require s tha t th e compiler' s RTT I (Ru n Tim e Typ eIdentification) option be turned on when the code is compiled. There is usually a per-formance and memory cost to have RTTI enabled, so many games have it turned offin their projects. We could use our own custom RTTI, but instead, let's go the way ofCOM (Microsoft's Component Object Model) and provide a Querylnterface function[Rogerson97] .

    If the objec t i n questio n implement s a particula r interface , the n Querylnterfacecasts the incoming pointer to the interface and returns true. To create our own Query-Interface function, we need to have a base class from which all of the related objectsthat inherit from a set of interfaces derive. We could even make that base class itself aninterface like COM's lUnknown, bu t that makes things more complicated.

    class GameObject {public:

    enum GamelnterfaceType {IRENDERABLE,IOTHERINTERFACE

    virtual bool Querylnterface (const GamelnterfaceType type,void ** pObj ) ;

    // The rest of the GameObject declaration

    The implementatio n of Querylnterface for a plain game objec t would be trivial .Because it's not implementing any interface, it will always return false.

  • 1.3 Programming with Abstract Interfaces 25

    bool GameObject: :QueryInterface (const GamelnterfaceType type,void ** pObj ) {

    return false;

    The implementation of a 3D objec t class is different from tha t of GameObject,because it will implement the IRenderable interface.

    class 3DObject : public GameObject, public IRenderable {public:

    virtual -3DObject();virtual bool Querylnterface (const GamelnterfaceType type,

    void ** pObj ) ;virtual bool Render();// Some more functions if needed

    bool SDObject: :QueryInterface (const GamelnterfaceType type,void ** pObj ) {

    bool bSuccess = false;if ( type == GameObject:: IRENDERABLE ) {

    *pObj = static_cast(this);bSuccess = true;

    }return bSuccess;

    It is the responsibility of the 3DObject class to override Querylnterface, check forwhat interfaces it supports, and do the appropriate casting.

    Now, let' s look at the render loop, which is simple and flexible and knows noth-ing about the type of objects it is rendering.

    IRenderable * pRenderable;for ( all the objects we want to render ) {

    if ( pGameObject->QueryInterface (GameObject: : IRENDERABLE,(void**)&pRenderable) )

    {pRenderable->Render ( ) ;

    Now we're ready to deliver the last of the promises of abstract interfaces listed atthe beginning of this gem: effortlessl y adding new implementations. With such a ren-der loop, i f we give it new types of objects and some of them implemented the IRen-derable interface, everything would work as expected without the need to change therender loop. The easiest way to introduce the new object types would be to simply re-link th e projec t wit h th e update d librarie s o r cod e tha t contain s th e ne w classes .Although beyond the scope of this gem, we could add new types of objects at runtimethrough DLL s o r a n equivalen t mechanis m availabl e o n th e targe t platform . Thi senhancement would allo w us t o releas e ne w game object s or game updates without

  • 26 Section 1 General Programming

    the nee d t o patc h th e executable . User s coul d also us e thi s metho d t o easil y creat emodifications for our game.

    Notice that nothing is stopping us from inheriting from multiple interfaces. All itwill mea n i s tha t the clas s tha t inherit s fro m multipl e interface s i s no w providing al lthe services specified by each of the interfaces. For example, we could have an IColl-idable interface for objects that need to have collision detection done. A 3D objec tcould inherit from both IRenderable and ICollidable, but a class representing smokewould only inherit from IRenderable.

    A word of warning, however: while using multiple abstract interfaces is a power-ful technique , i t ca n als o lea d t o overl y complicated design s tha t don' t provid e an yadvantages ove r design s wit h singl e inheritance . Also , multipl e inheritanc e doesn' twork well fo r dynami c characteristics , an d should rathe r be use d for permanent char -acteristics intrinsic to an object.

    Even though many people advise staying away from multiple inheritance, this is acase where it is useful and it does not have any major drawbacks. Inheriting from atmost one rea l paren t class an d multiple interfac e functions should no t resul t in thedreaded diamond-shaped inheritance tree (wher e th e parents o f both our parents arethe same class) o r many of the other usual drawbacks of multiple inheritance.

    Everything Has a CostSo far, we have seen that abstract interfaces have many attractive features. However, allof these feature s com e a t a price . Mos t o f the time , th e advantage s o f using abstrac tinterfaces outweig h an y potentia l problems , bu t i t i s importan t t o b e awar e o f thedrawbacks and limitations of this technique.

    First, the design becomes more complex. For someone not used to abstract inter-faces, th e extr a classe s an d th e queryin g o f interfaces coul d loo k confusin g a t firs tsight. It should only be used where it makes a difference, not indiscriminately all overthe game; otherwise, it will only obscure the design and get in the way.

    With th e abstrac t interfaces , w e di d suc h a goo d jo b hidin g al l o f the privat eimplementations tha t the y actuall y ca n becom e harde r t o debug . I f all w e hav e i s avariable of type IRenderable*, we won't be able to see the private contents of the realobject it points to in the debugger's interactive watch window without a lot of tediouscasting. O n th e othe r hand , mos t o f the tim e we shouldn' t hav e t o worry abou t it .Because th e implementatio n i s wel l isolate d an d teste d b y itself, al l we shoul d car eabout is using the interface correctly.

    Another disadvantage is that it is not possible to extend an existing abstract inter-face throug h inheritance . Goin g bac k t o ou r firs t example , mayb e w e woul d hav eliked to extend the SoundSystemHardware class to add a few functions specific to thegame. Unfortunately, we don't have access to the class implementation any more, andwe certainly can't inherit from it and extend it. It is still possible either to modify theexisting interface or provide a new interface using a derived class, but it will all have tobe done from the implementation side, and not from within the game code.

  • 1.3 Programming with Abstract Interfaces 27

    Finally, notice that every single function in an abstract interface is a virtual func-tion. This means that every time one of these functions is called through the abstractinterface, th e compute r will have t o go throug h one extr a level o f indirection. Thi s i stypically not a problem with moder n computer s an d gam e consoles , a s lon g as weavoid using interfaces for functions that are called from within inner loops. For exam-ple, creating an interface with a DrawPolygonQ or SetScreenPointQ function wouldprobably not be a good idea.

    ConclusionAbstract interfaces are a powerful technique that can be put to good use with very lit-tle overhea d o r structural changes . I t i s importan t t o kno w how i t ca n b e bes t used ,and when it is better to do things a different way. Perfect candidates for abstract inter-faces ar e module s tha t ca n b e replace d (graphic s Tenderers , spatia l databases , A Ibehaviors), o r an y sor t o f pluggable o r user-extendabl e module s (too l extensions ,game behaviors).

    References[Gamma95] Gamma, Eric et al, Design Patterns, Addison-Wesley, 1995.[Lakos96] Lakos, John, Large Scale C++ Software Design, Addison-Wesley, 1996.[Rogerson97] Rogerson, Dale, Inside COM. Microsoft Press, 1997 .

  • 1.4

    Exporting C++ Classes from DLLsHerb Marselas, Ensemble [email protected]

    Exporting a C++ clas s fro m a Dynamic Link Library (DLL ) fo r use by anotherapplication i s a n eas y wa y t o encapsulat e instance d functionalit y o r t o shar ederivable functionality without having to share the source code of the exported class.This method is in some ways similar to Microsoft COM, but is lighter weight, easierto derive from, an d provides a simpler interface.

    Exporting a FunctionAt the most basic level, there is little difference between exporting a function or a classfrom a DLL. To expor t myExportedFunction from a DLL, the value _BUILDING_MY_DLL i s define d i n th e preprocesso r option s o f the DL L project , an d no t i n th eprojects tha t us e th e DLL . Thi s cause s DLLFUNCTION t o b e replace d b y__decbpec(dllexport) when building the DLL, and __deckpec(dllimport) when build-ing the projects that use the DLL.

    #ifdef _BUILDING_MY_DLLtfdefine DLLFUNCTION _declspec(dllexport) // defined if building the

    // DLL#elsetfdefine DLLFUNCTION _declspec(dllimport) // defined if building the

    // application#endif

    DLLFUNCTION long myExportedFunction(void);

    Exporting a ClassExporting a C++ class from a DLL is slightly more complicated because there are sev-eral alternatives. In the simplest case, the class itself is exported. As before, the DLL-FUNCTION macro is used to declare the class exported by the DLL, or imported bythe application .

    28

  • 1.4 Exporting C++ Classes from DLLs 29

    tfifdef _BUILDING_MY_DLLtfdefine DLLFUNCTION _declspec(dllexport)#elsetfdefine DLLFUNCTION _ declspec(dllimport)tfendif

    class DLLFUNCTION CMyExportedClass{

    public:CMyExportedClass(void) : mdwValue(O) { }

    void setValue(long dwValue) { mdwValue = dwValue; }long getValue(void) { return mdwValue; }long clearValue(void) ;

    private:long mdwValue;

    If the DLL containing the class i s implicitly linked (i n other words, th e projectlinks with the DLL's lib file), then using the class is as simple as declaring an instanceof the class CMyExportedClass. This also enables derivation from this class as if it weredeclared directly in the application. The declaration of a derived class in the applica-tion i s mad e normall y without any additional declarations .

    class CMyApplicationClass : public CMyExportedClass{

    public:CMyApplicationClass ( void )

    There i s one potential problem with declaring or allocating a class exported froma DLL in an application: i t may confuse some memory-tracking programs and causethem to misreport memory allocations or deletions. To fix this problem, helper func-tions tha t allocate and destroy instances o f the exporte d class mus t b e added t o th eDLL. All users of the exported class should call the allocation function to create aninstance of it, an d the deletion function t o destro y it. O f course, th e drawbac k to thisis tha t it prevents deriving from the exported class in the application. I f deriving anapplication-side clas s fro m th e exporte d clas s i s important , an d th e projec t use s amemory-tracking program, the n thi s program will either need to understan d what'sgoing on or be replaced by a new memory-tracking program.

  • 30 Section 1 General Programming

    #ifdef _BUILDING_MY_DLL#define DLLFUNCTION _declspec(dllexport)#else#define DLLFUNCTION _declspec(dllimport)#endif

    class DLLFUNCTION CMyExportedClass{

    public:CMyExportedClass(void) : mdwValue(O) { }

    void setValue(long dwValue) { mdwValue = dwValue; }long getValue(void) { return mdwValue; }long clearValue(void);

    private:long mdwValue;

    };CMyExportedClass *createMyExportedClass(void) {

    return new CMyExportedClass; }void deleteMyExportedClass(CMyExportedClass *pclass) {

    delete pclass; }

    Exporting Class Member FunctionsEven with the helper functions added , becaus e the class itsel f is being exported fromthe DLL, i t is still possible that users could create instances of the class without callingthe createMyExportedCLtss helper function. This problem is easily solved by moving theexport specification from the class level to the individual functions to which the usersof the class need access. Then the application using the class can no longer create aninstance of the class itself. Instead, it must call the createMyExportedCLtss helper func-tion t o creat e a n instanc e o f the class , an d deleteMyExportedClass when i t wishes t odestroy the class .

    class CMyExportedClass{

    public:CMyExportedClass(void) : mdwValue(O) { }

    DLLFUNCTION void setValue(long dwValue) { mdwValue = dwValue; }DLLFUNCTION long getValue(void) { return mdwValue; }long clear-Value (void);

    private:long mdwValue;

    };CMyExportedClass *createMyExportedClass(void) {

    return new CMyExportedClass; }void deleteMyExportedClass(CMyExportedClass *pclass) {

    delete pclass; }

  • 1.4 Exporting C++ Classes from DLLs 31

    It should also be noted that although CMyExportedClass::clearValue is a publicmember function, i t can no longer be called by users of the class outside the DLL, asit is not declared as dllexported. This can be a powerful tool for a complex class thatneeds t o mak e som e function s publicl y accessibl e t o user s o f the clas s outsid e th eDLL, ye t stil l need s t o have othe r public functions fo r use inside the DL L itself . Anexample of this strategy in practice is the SDK for Discreet's 3D Studi o MAX. Mos tof the classes have a mix of exported and nonexported functions. This allows die userof the SD K to acces s o r derive functionalit y as neede d fro m th e exporte d membe rfunctions, whil e enablin g the developer s o f the SD K to hav e thei r own se t o f inter-nally available membe r functions .

    Exporting Virtual Class Member FunctionsOne potentia l proble m shoul d b e note d fo r user s o f Microsoft Visua l C+ + 6 . I f youare attemptin g t o expor t th e membe r function s o f a class , an d yo u ar e no t linkin gwith th e lib file of the DL L that export s th e clas s (you'r e using LoadLibrary to loadthe DLL at runtime), you will get an "unresolved external symbol" for each functionyou reference if inline function expansion is disabled . This can happen regardless ofwhether th e functio n i s declare d completel y i n th e header . On e fi x fo r thi s i s t ochange the inline function expansion to "Only inline " or "Any Suitable." Unfortu -nately, thi s ma y conflict with your desire t o actuall y have inline function expansio ndisabled in a debug build. An alternate fix is to declare the functions virtual. The vir-tual declaration will cause the correct code to be generated, regardless of the setting ofthe inlin e functio n expansio n option . I n man y circumstances , you'l l likel y wan t t odeclare exported member functions virtual anyway, so that you can both work aroundthe potential Visual C++ problem and allow the user to override member functions asnecessary.

    class CMyExportedClass{

    public:CMyExportedClass(void) : mdwValue(O) { }

    DLLFUNCTION virtual void setValue(long dwValue) { mdwValue =dwValue; }

    DLLFUNCTION virtual long getValue(void) { return mdwValue; }long clearValue(void);

    private:long mdwValue;

    };With exported virtual member functions, deriving from the exported class on the

    application side i s th e same a s i f the exported clas s were declared completely in theapplication itself.

  • 32 Section 1 General Programming

    class CMyApplicationClass : public CMyExportedClass{

    public:CMyApplicationClass (void) { }virtual void setValue(long dwValue);virtual long getValue(void) ;

    SummaryExporting a class from a DLL is an easy and powerful way to share functionality with-out sharing source code. It can give the application all the benefits of a structured C++class to use , derive from, or overload, while allowing the creator of the class to keepinternal functions and variables safely hidden away.

  • 1.5

    Protect Yourself from DLL Helland Missing OS FunctionsHerb Marselas, Ensemble [email protected]

    Dynamic Lin k Libraries (DLLs ) ar e a powerfu l featur e o f Microsoft Windows .They hav e man y uses , includin g sharin g executabl e cod e an d abstractin g ou tdevice differences . Unfortunately , relyin g o n DLL s ca n b e problemati c du e t o thei rstandalone nature . I f an applicatio n relie s o n a DLL tha t doesn' t exis t on the user' scomputer, attemptin g to ru n i t will resul t in a "DLL Not Found" messag e that' s no thelpful to the average user . I f the DLL does exis t on the user' s computer, there' s noway to tel l i f the DLL is valid (a t least as far as the application is concerned ) i f it'sautomatically loaded when th e applicatio n start s up .

    Bad DLL versions can easily find their way onto a system as the user installs anduninstalls other programs. Alternatively, there can even be differences in system DLLsamong different Windows platform s an d servic e packs . I n thes e cases , th e use r mayeither get the cryptic "DynaLink Error!" message if the function being linked to in theDLL doesn't exist, o r worse yet, the application will crash. All of these problems withfinding and loading the correct DLL are often referred to as "DLL Hell." Fortunately,there are several ways to protect against falling into this particular hell.

    Implicit vs. Explicit LinkingThe first line of defense in protecting against bad DLLs is to make sure that the nec-essary DLLs exist on the user's computer and are a version with which the applicationcan work. This must be done before attempting to use any of their functionality.

    Normally, DLL s are linked to an application by specifying their eponymous libfile in the link line. This is known as implicit DLL loading, or implicit linking. By link-ing t o th e lib file , the operatin g system wil l automaticall y search fo r an d loa d th ematching DLL when a program runs. This method assumes that the DLL exists, thatWindows can fin d it , an d that it's a version with which the program can work.

    Microsoft Visual C++ also supports three other methods of implicit linking. First,including a DLL's lib file directly into a project is just like adding it on the link line.Second, i f a projec t include s a subprojec t tha t build s a DLL , th e DLL' s lib file i s

    33

  • 34 Section 1 General Programming

    automatically linked with th e projec t b y default . Finally , a lib ca n b e linke d t o a napplication using the #pragma comment (lib "libname") directive.

    The remedy to thi s situation o f implicit linking and loading is to explicitly loadthe DLL. This is done by not linking to the DLL's lib file in the link line, and remov-ing any #pragma comment directives tha t would link to a library. I f a subproject inVisual C++ builds a DLL, the link property page of the subproject should be changedby checking the "Doesn' t produce .LIB " option . B y explicitly loading the DLL, th ecode can handle each error that could occur, making sure the DLL exists, making surethe functions required are present, and so forth.

    LoadLibrary and GetProcAddressWhen a DLL is implicitly loaded using a lib file, the functions can be called directlyin th e application' s code , an d the O S loade r does al l the work of loading DLLs andresolving function references. When switching to explicit linking, the functions mustinstead be called indirectly through a manually resolved function pointer. To do this,the DLL that contains th e function must be explicitly loaded using the LoadLibraryfunction, and then we can retrieve a pointer to the function using GetProcAddress.

    HMODULE LoadLibrary(LPCTSTR IpFileName);FARPROC GetProcAddress(HMODULE hModule, LPCSTR IpProcName);BOOL FreeLibrary(HMODULE hModule);

    LoadLibrary searches for the specified DLL, loads it into the applications processspace if it is found, and returns a handle to this new module. GetProcAddress is thenused to create a function pointer to each function in the DLL that will be used by thegame. When an explicitly loaded DLL is n o longer needed, i t should be freed usingFreeLibrary. After calling FreeLibrary, the module handle is no longer considered valid.

    Every LoadLibrary call must be matched with a FreeLibrary call. This is necessarybecause Windows increments a reference count on each DLL per process when i t isloaded either implicitly by the executable or another DLL, or by calling LoadLibrary.This referenc e coun t i s decremente d b y calling FreeLibrary, o r unloadin g the exe -cutable o r DL L tha t loaded thi s DLL . Whe n th e referenc e coun t fo r a give n DL Lreaches zero, Windows knows it can safely unload the DLL.

    Guarding Against DirectXOne o f the problem s we have ofte n foun d i s tha t the require d versions o f DirectXcomponents ar e no t installed , o r the instal l i s corrup t i n some way. T o protec t ourgame against these problems, we explicitly load the DirectX components we need. Ifwe were to implicitly link to Directlnput in DirectX 8, we would have added the din-putS.lib to our link line and used the following code:

  • 1.5 Protect Yourself from DLL Hell and Missing OS Functions 35

    IDirectlnputS *pDInput;HRESULT hr = DirectInput8Create(hInstance, DIRECTINPUT_VERSION,

    IID_IDirectInput8,(LPVOID*) & pDInput, 0);

    if (FAILED(hr)){

    // handle error - initialization error}

    The explici t DLL loading case effectively adds tw o more lines o f code, bu t theapplication i s no w protected agains t dinput8.dll not bein g found , o r o f it bein g cor -rupt in some way.

    typedef HRESULT (WINAPI* DirectInput8Create_PROC)(HINSTANCE hinst, DWORD dwVersion, REFIID riidltf,LPVOID* ppvOut,LPUNKNOWN punkOuter);

    HMODULE hDInputLib = LoadLibrary( "dinput8.dll") ;if (! hDInputLib){

    // handle error - DInput 8 not found. Is it installed incorrectly// or at all?

    DirectInput8Create_PROC diCreate;diCreate = (DirectInput8Create_PROC)GetProcAddress(hDInputLib, "DirectlnputSCreate") ;if (! diCreate){

    // handle error - DInput 8 exists, but the function can't be// found.

    HRESULT hr = (diCreate) (hlnstance, DIRECTINPUT_VERSION,I ID_IDirect Inputs,(LPVOID*) &mDirectInput, NULL);

    if (FAILED(hr)){

    // handle error - initialization error

    First, a functio n pointe r typedef is create d tha t reflect s th e functio n Direct-lnputSCreate. Th e DL L i s the n loade d usin g LoadLibrary. I f the dinput8.dl l wa sloaded successfully , w e the n attemp t t o fin d th e functio n DirectlnputSCreate usingGetProcAddress. GetProcAddress return s a pointe r t o th e functio n i f it i s found , o rNULL i f the functio n canno t be found. We the n chec k to mak e sur e th e functionpointer is valid . Finally , we cal l DirectlnputSCreate through th e function pointe r toinitialize Directlnput.

  • 36 Section 1 General Programming

    If there were more function s tha t needed t o b e retrieve d fro m th e DLL , a func-tion pointer typedefand variable would be declared for each. It might be sufficient toonly check for NULL when mapping the first function pointer using GetProcAddress.However, a s mor e erro r handlin g i s usuall y no t a ba d thing , checkin g ever y Get-ProcAddress for a successful non-NULL return is probably a good thing to do.

    Using OS-Specific Features _Another issu e tha t explici t DL L loadin g can resolv e i s when a n applicatio n wants t otake advantage of a specific API function if it is available. There is an extensive num-ber of extended functions ending in "Ex" tha t are supported under Windows NT or2000, and not available in Windows 95 or 98. These extended functions usually pro-vide more information or additional functionality than the original functions do .

    An example of this is the CopyFileEx function, which provides the ability to can-cel a long file copy operation. Instead of calling it directly, kernel32.dll can be loadedusing LoadLibrary and the function again mapped with GetProcAddress. If we loadkernel32.dll and find CopyFileEx, we use it. If we don't find it, we can use the regularCopyFile function. One other problem that must be avoided in this case is that Copy-FileEx is really only a #define replacement in the winbase.h header file that is replacedwith CopyFileExA or CopyFileExW if compiling for ASCII or wide Unicode charac-ters, respectively.

    typedef BOOL (WINAPI *CopyFileEx_PROC) (LPCTSTR IpExistingFileName,LPCTSTR IpNewFileName , LPPROGRESS_ROUTINE IpProgressRoutine, LPVOIDIpData, LPBOOL pbCancel, DWORD dwCopyFlags) ;

    HMODULE hKerne!32 = LoadLibrary("kernel32.dH") ;if (!hKerne!32){

    // handle error - kernel32.dll not found. Wow! That's really bad}CopyFileEx_PROC pfnCopyFileEx;pfnCopyFileEx = (CopyFileEx_PROC) GetProcAddress(hKernel32,

    "CopyFileExA") ;

    BOOL bReturn;if (pfnCopyFileEx){

    / / use CopyFileEx to copy the filebReturn = (pfnCopyFileEx) (pExistingFile, pDestinationFile, ...);

    else

    // use the regular CopyFile functionbReturn = CopyFilefpExistingFile, pDestinationFile, FALSE);

  • 1.5 Protect Yourself from DLL Hell and Missing OS Functions 37

    The use of LoadLibrary and GetProcAddress can also be applied to game DLLs.One example of this is the graphics support in a game engine currently under devel-opment at Ensemble Studios, where graphics support for Direct3D and OpenGL hasbeen broke n ou t int o separat e DLL s tha t ar e explicitl y loade d a s necessary . I fDirect3D graphic s suppor t i s needed , th e Direct3 D suppor t DL L i s loade d wit hLoadLibrary and the exported functions are mapped using GetProcAddress. This setupkeeps the main executable free from having to link implicitly with either dddS.lib oropengl32.lib.

    However, the supporting Direct3D DLL links implicitly with dddS.lib, and thesupporting OpenGL DLL links implicitly with opengl32. lib. This explicit loading ofthe game's own DLLs by the main executable , and implicit loading by each graphicssubsystem solves several problems. First , i f an attempt to load either library fails, it' slikely that that particular graphics subsystem files cannot be found or are corrupt. Themain progra m ca n the n handl e th e erro r gracefully . Th e othe r proble m tha t thi ssolves, which is more of an issue with OpenGL than Direct3D, is that if the enginewere to link explicitly to OpenGL, i t would need a typedef and function pointer forevery OpenGL function it used. The implicit linking to the support DLL solves thisproblem.

    SummaryExplicit linking can act as a barrier against a number of common DLL problems thatare encountered unde r Windows, includin g missing DLLs, o r versions of DLLs tha taren't compatibl e with a n application . Whil e no t a panacea, i t ca n a t leas t put th eapplication in control and allow any error to be handled gracefully instead of with acryptic error message or an outright crash.

  • 1.6

    Dynamic Type InformationScott Wakeling, Virgin [email protected]

    As developer s continu e t o embrac e objec t orientation , th e system s tha t powe rgames ar e growing increasingly flexible, and inherentl y more complex. Suc h sys-tems now regularly contain many different types and classes; counts of over 1000 arenot 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 standarddata type. This gem discusses managing types effectively by providing ways of query-ing their relations to other types, or accessing information about their type at runtimefor query or debug purposes. Toward the end of the gem, an approach for supportingpersistent object s i s suggeste d wit h som e idea s abou t ho w th e metho d ca n b eextended.

    Introducing the Dynamic Type Information ClassIn our efforts to harness the power of our types effectively, we'll be turning to the aidof one clas s i n particular : th e dynami c typ e informatio n (DTI ) class . Thi s clas s wil lstore any information that we may need to know about the type of any given object orstructure. A minimal implementatio n o f the clas s i s give n 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

  • 1.6 Dynamic Type Information 39

    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 andquery the dtiClass member of the class's parent. This member must permeate the classtree al l th e way from th e roo t clas s down , thu s ensurin g tha t al l gam e object s hav e

    '~^J^__J) acces s to information about themselves and their parents. The implementation ofdti-ONTHICO Class can b e found in the code on th e accompanying CD.

    Exposing and Querying the DTILet's see how we can begin to us e DTI by implementing a very simple class tre e asdescribed previously. Here is a code snippet showing a macro that helps us define ourstatic dtiClass member, a basic root class, and simple initialization of the class's typeinfo:

    #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 i n al l of our class definitions an d initial-izing th e stati c Type membe r correctl y a s shown , we'v e take n th e firs t ste p towar dinstilling dynamic type info in our game engine. We pass our class name and a pointerto the class's parent's dtiClass member. The dtiClass constructor does the rest, settingup the szName and pdtiParent members accordingly.

    We can now query for an object' s class name a t runtime for debug purposes ofother type-related cases, such as saving or loading a game. More on that later, but fornow, 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 toconst char* szGetName = pSomePtr->Type.GetName();

    In th e origina l example , w e passe d NULL i n t o th e dtiClass constructor as th eclass's parent field because this is our root class. For classes that derive from others, wejust need to specify the name of the parent class. Fo r example, i f we were to specify achild class of our root, a basic definition might look something like this:

    class CChildClass : public CRootClass{

    EXPOSE TYPE;

  • 40 Section 1 General Programming

    // 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'sname, bu t the name of its parent too, a s long as it s type has been exposed with theEXPOSE_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 fromchar* szParentName = pSomePtr->Type.GetParent()->GetName();

    Now that we have a simple class tre e with DTI presen t and know how to use thatinformation t o quer y fo r clas s an d paren t name s a t runtime , w e ca n mov e o n t oimplementing a usefu l metho d fo r safeguardin g typ e casts , o r simpl y querying a nobject about its roots or general type.

    Inheritance Means "IsA"Object orientation gave u s th e 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. Inmany cases, polymorphism is put to use in game programming to handle many typesof objects in a safe, dynamic , and effective manner. Thi s means we like to ensure thatobjects ar e o f compatible type s befor e w e cas t them , thu s preventin g undefine dbehavior. It also means we like to be able to check what type an object conforms to atruntime, rathe r than having to know from compiler time, an d we like to be able to doall of these things quickly and easily.

    Imagine tha t ou r gam e involve s a numbe r o f different type s o f robots , som epurely electronic , an d som e with mechanica l parts , mayb e fue l driven . No w assumefor instance tha t there i s a certain typ e o f weapon th e player may have tha t i s veryeffective agains t th e purel y electroni c robots , bu t les s s o agains t thei r mechanica lcounterparts. Th e classe s tha t defin e thes e robot s ar e ver y likel y t o b e o f the sam ebasic type , meanin g the y probabl y bot h inheri t fro m th e sam e generi c robo t bas eclass, and then go on to override certain functionality or add fresh attributes. To copewith varyin g type s o f specialist chil d classes , w e nee d t o quer y thei r roots . W e ca nextend the dtiClass introduced earlier to provide us with such a routine. We'll call thenew member function IsA, because inheritance can be seen to translate to "is a typeof." Here's the function:

    bool dtiClass::IsA( dtiClass* pType ){

    dtiClass* pStartType = this;while( pStartType ){

    if ( pStartType == pType )

  • 1.6 Dynamic Type Information 41

    return true;

    else

    pStartType = pStartType->GetParent();

    return false;

    If we need to know whether a certain robot subclass is derived from a certain rootclass, we just need to call IsA from the object's own dtiClass member, passing in thestatic 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 ca n see tha t th e resul t of a quick IsA check tells u s whether we ar e derived,directly or indirectly, from a given base class. Of course, we might use this fact to goon and perform a safe casting operation, as in the preceding example. Or, maybe we'lljust use the check to filter out certain types of game objects in a given area, given thattheir type makes them susceptible to a certain weapon or effect . I f we decide that asafe castin g operatio n i s somethin g we'll nee d regularly , w e ca n ad d th e followin g

    -^_1-^ functio n t o th e roo t objec t t o simplif y matters . Here' s th e definitio n an d a quic kexample; the function's implementation is on the accompanying CD:

    // SafeCast member function definition added to CRootClassvoid* SafeCast( dtiClass* pCastToType );// How to simplify the above operationpRoot = (CRootClass*)pChild->SafeCast( &CRootClass::Type );

    If the cast is not safe (in other words, the types are not related), dien the value willevaluate t o nothing , an d pRoot will b e NULL.

    Handling Generic ObjectsGoing back to ou r simple game example , let' s conside r how we migh t cope with somany different types of robot effectively. The answer starts off quite simple: we canmake us e o f polymorphism an d just stor e pointer s t o the m al l i n on e bi g arra y o fgeneric base class pointers. Even our more specialized robots can be stored here, suchas CRobotMech (derive d fro m CRobof), becaus e polymorphis m dictate s tha t fo r an ytype requirement , a derived type can always be supplied instead . No w we have ou rvast array of game objects, al l stored as pointers to a given base class. We can iterate

  • 42 Section 1 General Programming

    over them safely, perhaps calling virtual functions on each and getting the more spe-cialized (overridden ) routine s carrie d out by default . Thi s take s u s 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 routinesthat can query what general type an object is, an d cast it safely up the class tree . Thi sis often referred to as up-casting, and it takes us halfway to handling vast numbers ofgame object s i n a fast, safe , an d generi c way. The othe r half of the problem come swith down-castingcasting a pointer to a generic base class safely down to a more spe-cialized subclass. I f we want to iterate a list of root class pointers, and check whethereach really points t o a specific type o f subclass, we nee d to mak e us e of the dynami ccasting operator, introduced by C++.

    The dynamic casting operator is used to convert among polymorphic types and isboth safe an d informative . I t even return s applicabl e feedback about the attempte dcast. 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 passa reference to a class, we must pass a modifiable l-value in the second parameter. Hereare two examples:

    // Given a root object (RootObj), on pointer (pRoot) we// can down-cast like thisCChildClass* pChild = dynamic_cast(pRoot);CChildClass& ChildObj = dynamic_cast(RootObj);

    To gain access to these extended casting operators, we need to enable embeddedruntime type information in the compiler settings (use the /GR switch for MicrosoftVisual C++) . I f the requested cas t cannot be made (fo r example, i f the roo t pointerdoes no t really point to anythin g more derived) , th e operator will simply fai l and theexpression wil l evaluat e t o NULL. Therefore , fro m th e precedin g cod e snippet ,

    (f : ,js*:*:*'% pChild woul d evaluat e t o NULL IfpRoot reall y di d onl y poin t t o a CRootClass object .ON me a> I f 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 u s determine what type i s reall y hidden behind a

    pointer. Imagine we want to iterate through every robot in a certain radius and deter-mine which ones are mechanica l models , an d thus immune to the effect s o f a certainweapon. Give n a list of generic CRobot pointers, we could iterate through these andperform dynamic casts on each, checking which ones are successful and which resolveto NULL, and thus exacting which ones were in fact mechanical. Finally , we can nowsafely down-cast too, which complete s ou r runtime type informatio n solution . Th e

  • 1.6 Dynamic Type information 43

    / c -., cod e o n th e companio n CD-RO M ha s a mor e extende d exampl e o f usin g th eon m CD dynami c casting operator.

    Implementing Persistent Type InformationNow 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, thu s extendin g ou r type-relate d capabilitie s an d allowin g u s t o handl e thing s

    ,- c " ) suc h as game saves or object repositories with ease. The first thing we need is a bare-mtmco bone s implementation of a binary store where we can keep our object data. An exam-

    ple implementation, CdtiBin can be found on the companion CD-ROM.There are a number of utility member functions, bu t the two importan t points

    are the Stream member function, and the friend operator s tha t allow us t o writeout or load die basic data types of the language. We'll need to add an operator for eachbasic type we want to persist. When Stream is called, the data will be either read fromthe 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 addthe Serialize function, shown here:

    virtual void Serialize( CdtiBin& ObjStore );

    Note tha t i t i s virtua l and needs t o be overridde n for all chil d classes tha t haveadditional 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 CdtiBinobjects. We could write object settings out to a file, and later load them back in andrepopulate fresh objects with die old data, thus ensuring a persistent object solutionfor use in a game save routine . All types would thus know how to save themselves,making our game save routine s much easie r to implement .

    However, chil d classe s nee d t o writ e ou t thei r dat a and that o f their parents .Instead of forcing the programmer to look up all data passed down from parents andadding 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) thei r inherited databefore their own data. We use the DECLAREJSUPER macro for this:

    #define DECLARE_SUPER(SuperClass) \public: \

    typedef Superclass Super;

  • 44 Section 1 General Programming

    class CChildClass

    DECLARE_SUPER(CRootClass);

    This farthe r extend s 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, andthus it s Serialize member only needs t o cop e with it s ow n 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 operato r for th e float data typ e was adde d t o suppor t th e above . Not e

    that th e orde r i n whic h attribute s ar e save d an d loade d i s alway s th e same . Cod eshowing 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 attributeswill remai n persistent between saves and loads . Adding the correc t friend operator s t othe CdtiBin class add s suppor t fo r basic data types . I f we want to ad d user-definedstructures to our class members, we just need to write an operator for coping with thatstruct. With this in place, al l objects and types in the engine will know precisely howto save themselves out to a binary store and read themselves back in.

    Applying Persistent Type Information to a GameSave Database

    As mentione d previously , object s nee d t o b e serialize d ou t an d loade d bac k i n th esame order . Th e quickes t an d easies t metho d i s t o onl y sav e ou t on e objec t t o th egame saves , an d the n jus t loa d tha t on e bac k in . I f we ca n defin e an y poin t i nthe game by constructing some kind of game state object that knows precisely how toserialize itself either way, then we can write all our game data out in one hit, and readit bac k i n a t an y point . Ou r gam e stat e objec t woul d n o doub t contai n array s o fobjects. As long as the custom array type knows how to serialize itself, and we have allthe correc t CdtiBin operators written fo r our types , everythin g will work. Savin g andloading a game will be a simple matter of managing the game from a high-level, all -encompassing containment class, callin g just the one Serialize routine when needed.

  • 1.6 Dynamic Type Information 45

    ConclusionThere is still more that could be done than just the solution described here. Support -ing multiple inheritance wouldn't be difficult . Instea d of storing just the one parentpointer 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 byextending 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 weonly ever wanted t o b e containe d i n othe r classes , an d neve r b y themselve s ("con -tained classes").

    References[Meyers98] Meyers , Scot t D., Effective C++ 2ndEdition, Addison-Wesley , 1998 .[Wilkie94] Wilkie, George, Object-Oriented Software Engineering, Addison-Wesley,

    1994.[EberlyOO] Eberly , Davi d H. , 3D Game Engine Design, Morga n Kauffman ,

    1999-2000.[WakelingOl] Wakeling , Scot t J. , "Coping with Class Trees," available onlin e a t

    www.chronicreality.com/articles, March 12 , 2001 .

  • 1.7

    A Property Class for GenericC++ Member AccessCharles [email protected]

    Practically every game has a unique set of game objects, and any code that has tomanipulate those object s has t o be written from scratch for each project . Take ,for example, a n in-gam e editor , which has a simple purpose: t o create , place , display ,and edit object properties. Objec t creation i s almos t always specifi c to the game, o rcan b e handled b y a class factory . Objec t placement i s specifi c t o th e visualizationengine, whic h make s reus e difficult , assumin g it i s eve n possible t o visually place anobject on the map. In some cases, a generic map editor that can be toggled on and off(or possibly superimposed as a heads-up display) ca n be reused from game to game.Therefore, in theory, it should be possible to develop a core editor module that can bereused without having to rewrit e the same cod e over and over again fo r each project .However, given that all games have unique objects, how does the editor know what todisplay for editing purposes without rewriting the editor code?

    What we need is a general object interface that allows access to the internals of aclass. Borland' s C++ Builde r provides an excellent C++ declaratio n type called ^prop-erty that does this very thing, but alas, it is a proprietary extension and unusable out-side o f Borlan d C++ . Interestingl y enough , C# , Microsoft' s ne w programmin glanguage developed by the creator of Borland C++ Builder , contain s the same feature.Microsoft's CO M interfac e allows runtim e queryin g of an objec t for its members , bu tit requires that we bind our objects to the COM interface, making them less portablethan straigh t C++. Thi s leaves a "roll-your-own" solution , which can b e more light -weight than COM, and more portable than proprietary extensions to the C++ lan-guage. Thi s wil l allo w code module s suc h a s th e in-gam e edito r t o b e writte n jus tonce, and used across many engines.

    The CodeThe interface is broken into two classes: a Property class and a PropertySet class. Prop-erty i s a container fo r one piec e o f data. I t contain s a unio n o f pointers t o differen tdata types, a n enumeration for the type of data, an d a string for the property name.The full source code can be found on the companion CD.

    46

  • 1.7 A Property Class for Generic C++ Member Access 47

    class Property{protected:

    union Data{

    int* m_int;float* m_float;std::string* m_string;bool* m_bool;

    enum Type{

    INT,FLOAT,STRING,BOOL,EMPTY

    Data m_data;Type m_type;std:: string m_name;

    protected:void EraseType() ;void Register(int* value);void Registerffloat* value);void Registerfstd: :string* new_string);void Registerfbool* value);

    public:Property () ;Property(std: :string const& name);Property(std: :string const& name, int* value);Property (std :: string const& name, float* value);Property (std :: string const& name, std::string* value);Property (std :: string const& name, bool* value);-Property () ;

    bool SetUnknownValue(std: :string const& value);bool Set (int value) ;bool Set(float value);bool Set(std: :string const& value);bool Set(bool value);void SetNamefstd: :string const& name);std:: string GetName() const;

    int Getlnt();float GetFloatf);std:: string GetString();bool GetBool() ;

  • 48 Section 1 General Programming

    The example code shows basic data types being used and stored, althoug h thesecould be easily expanded to handle any data type. Properties store only a pointer backto th e origina l data . Propertie s d o no t actuall y declare thei r ow n objects , o r allocat etheir ow n memory , s o manipulatin g a property' s dat a result s i n th e origina l data' smemory being handled. Setting a value via the Set function automatically defines thetype o f the property .

    Properties ar e constructe d an d manipulate d throug h a PropertySet class . Th ePropertySet class contain s th e lis t o f registered properties , th e registratio n methods ,and th e lookup method .

    class PropertySet{protected:

    HashTablem_properties;

    public:PropertySet();virtual -PropertySet();

    void Register(std::string const& name, int* value);void Register(std::string const& name, float* value);void Register(std::string const& name, std::string* value);void Register(std::string const& name, bool* value);

    // look up a propertyProperty* Lookup(std::string const& name);

    // get a list of available propertiesbool SetValue(std::string const& name, std::string* value);bool Set(std::string const& name, std::string const& value);bool Set(std::string const& name, int value);bool Set(std::string const& name, float value);bool Set(std::string const& name, bool value);bool Set(std::string const& name, char* value);

    };The PropertySet is organized around a HashTable object that organizes all of the

    stored properties using a standard hash table algorithm. The HashTable itself is a tem-plate tha t can be use d to has h into differen t objects, an d i s include d on th e compan-

    ONIHfCD f^r-~.ion UJ.We derive the game object from the PropertySet class:

    class GameObject : public PropertySet{

    int m_test;};

    Any properties or flags that need to be publicly exposed or used by other objectsshould b e registered , usuall y at constructio n time . Fo r example:

    Register("test_value",&m_test);

  • 1.7 A Property Class for Generic C++ Member Access 49

    Calling objects can use the Lookup method to access the registered data.

    void Update(PropertySet& property_set){

    Property* test_value_property=property_set.Lookup("test_value");

    int test_value = test_value_property->GetInt();// etc

    }

    As all of the game objects are now of type PropertySet, and as all objects are usu-ally stored in a master update list, it is a simple matter of handing the list pointer offto the in-game editor for processing. New derived object types simply have to registertheir additional properties to be handled by the editor. No additional coding is neces-sary because the editor is not concerned with the derived types. I t is sometimes help-ful t o specify the type in a property name (suc h a s "Type" ) t o assis t the use r whenvisually editing the object . It' s also useful to make the property required, so that theeditor could, for example, parse the property list into a "tree" style display.

    This process also provides the additional benefi t of decoupling the data from itsname. Fo r instance, internally , th e data may be referred to as m_colour, bu t can beexposed as "color."

    Additional UsesThese classes were designed around a concentric ring design theory. The PropertySetcannot be used without the Property class. However, the Property class can be used onits own , o r with anothe r se t typ e (fo r example , MultiMatrixedPropertySef) withou trewriting the Property class itself . Thi s i s true o f the HashTable inside th e PropertySetclass as well. Smaller classes with distinct and well-defined purposes and uses are muchmore reusable than large classes with many methods to handle every possible use.

    The Property class can also be used to publicly expose methods that can be calledfrom outside code via function pointers . Wit h a small amoun t of additional coding ,this can also be used as a save state for a save game feature as well. It could also be usedfor objec t messagin g via networks. With the addition o f a Send(std::string xml) andReceive(std::stringxml), the PropertySet could easily encode and decode XML messagesthat contai n th e propert y values , o r propert y value s tha t nee d t o b e changed . Th eProperty!PropertySet classes could also be rewritten as templates to support differentproperty types.

    Isolating the property data using "get " an d "set " method s wil l allo w for forma tconversion to and from the internal stored format. This will free the using code fromneeding to know anything about the data type of the property, making it more versa-tile at the cost of a small speed hit when the types differ.

  • 50 Section 1 General Programming

    Additional ReadingFowler, Martin, Kent Beck, John Brant, William Opdyke, Don Roberts, Refactoring,

    Addison-Wesley, ISBN : 0201485672 .Gamma, Erich, Richard Helm, Ralph Johnson, John Vlissides, Grady Booch, Design

    Patterns, Addison-Wesley, ISBN: 0201633612.Lakos, John, Large-Scale C++ Software Design, Addison-Wesley, ISBN: 0201633620.McConnell, Stev e C. , Code Complete: A Practical Handbook of Software Construction,

    Microsoft Press, ISBN: 1556154844 (anything by McConnell is good).Meyers, Scott , Effective C++: 50 Specific Ways to Improve Your Programs and Design

    (2ndEdition), Addison-Wesley, ISBN: 0201924889.Meyers, Scott , More Effective C++: 35 New Ways to Improve Your Programs and

    Designs, Addison-Wesley, ISBN: 020163371X.

  • 1.8

    A Game Entity FactoryFrangois Dominic [email protected]

    In recent years, scripting languages have proven invaluable to the game developmentcommunity. B y isolating the elaboratio n an d refinemen t o f game entit y behavio rfrom th e cor e o f the cod e base , the y have liberate d leve l designer s fro m th e code -compile-execute cycle , speedin g up game testin g and tweaking by orders o f magni-tude, and freed senior programmers' time for more intricate assignments.

    However, fo r th e data-drive n developmen t paradig m t o wor k well , th e game' sengine mus t provid e flexibl e entit y constructio n an d assembl y services , s o tha t th escripting language can provide individual entities with different operational strategies,reaction behaviors, and other parameters. This is the purpose of this gem: to describea hierarchy of C++ classe s an d a set of techniques that support data-driven develop-ment on the engine side of things.

    This simple framework was designed with the following goals in mind:

    A separation of logical behavior and audio-visual behavior. A single Door classcan support however many variations o f the concep t as required , withou t concernfor size, number of key frames in animation sequences, etc.

    Rapid development. Onc e a basic library of behaviors has been defined (whic htakes surprisingly little time), new game entity classes can be added to the frame-work with a minimum of new code, often in 15 minutes or less.

    Avoiding code duplication. B y assembling bits and pieces o f behavior into ne wentities at runtime, the framework avoids the "code bloat" associated with script-ing languages that compile to C/C++, for example.

    Several of the techniques in this gem are described in terms of patterns, detaile din the so-called "Gang of Four's" book Design Patterns [GoF94].

    ComponentsThe ge m i s buil t aroun d thre e majo r components : flyweigh t objects , behaviora lclasses and an object factory method. We will examine each in turn, and then look athow they work together to equip the engine with the services required by data-drivendevelopment. Finally , we will discuss advanced ideas to make the system even more

    51

  • 52 Section 1 General Programming

    flexible (at the cost of some code complexity) i f a full-fledged scripting language isrequired by the project.

    Flyweight, Behavior, and Exported ClassesBefore we go any further, we must make a distinction between the three types of"classes" t o which a game entity will belong in this framework: it s flyweight , behav-ioral, and exported classes.

    Th e flyweight class is th e look and fee l o f the entity . I n th e code , th e relationshi pbetween an entity and its flyweight class is implemented through object composi-tion: the entity owns a pointer to a flyweight that it uses to represent itself audio-visually.

    Th e behavioral class define s ho w th e objec t interact s wit h th e res t o f the gam eworld. Behavioral classes are implemented as a traditional inheritance hierarchy,with class Entity serving as abstract superclass for all others.

    Th e exported class is ho w the objec t represents itsel f to th e world. Mor e o f a con-venience than a requirement, the exported class is implemented as an enum con-stant an d allow s a n entit y to advertis e itsel f as severa l differen t objec t classe sduring its lifetime.

    Let us now look at each in turn.

    Flyweight Objects[GoF94] describe s flyweight s as objects deprived of their context so tha t they can beshared and used in a variety of situations simultaneously; in other words, as a templateor model for other objects. Fo r a game entity, the flyweight-friendly information con-sists of:

    Media content: Sound effects, 3D models, textures, animation files, etc. Control structure: Finit e state machine definition , scripts , an d the like.

    As you can see , thi s i s just about everything except information on the curren tstatus of the entity (position, health, FSM state). Therefore, in a gaming context, the\&iv\ fly weight is rather unfortunate, because the flyweight can consume megabytes ofmemory, while the context information would be small enough to fit with