You are on page 1of 603
1.0 The Magic of Data-Driven Design Steve Rabin Games are made up of two things: logic and data, This isa powerful distinction. Sep- arate, they are useless, but together, they make your game come alive. The logic defines the core rules and algorithms of the game engine, while the data provides the details of content and behavior. The magic happens when logic and data are decou- pled from cach other and allowed to blossom independently, Obviously, game data should be loaded from files, not embedded inside the code base. The genius comes from knowing how far to run with this concept, This article gives seven ideas that will revolutionize the way you make your games, or at least con- firm your suspicions. Idea #1: The Basics Create a system that can parse text files on demand (not juse ar startup), This is essen- tial to putting data-driven design to work, Every game needs a clean way to read in general-purpose data, The game should evennually be read in binary files, bur the abil- ity to read in text files during development is crucial. Text files are dead simple for editing and making changes. Withour altering a single line of eade, your whale team, including testers and game designers, can try out new things and experiment with dif. ferent variations, Thus, something that is trivial to implement can quickly became an indispensable tool. Idea #2; The Bare Minimum Dorit hard-core constants. Put constants in text files so that they can be easily changed without recompiling code. For example, basic functionality such ax camera behavior should be exposed completely. If this is done properiy, the game designer, the producer, and the kid down the street will all be able to alter che behavior of the camera with nothing more than Notepad. Game designers and producers are often at the merey of programmers, By exposing algorithm constants, non-programmers can 3 4 Section 1 Programming Techniques rune and play with the values co ges the exact behavior they desire—vithout bother- ing a single programmer. Idea #3: Hard-Code Nothing Assume that anything can change, and probably will. If che game calls for a split sereen, don't hard-code it! White your game to support any number of viewports, each with its own camera logic. It isn't even any more work if irs designed right. Through the magic of rext files, you could define whether the game is single-screen, split- screen, of quad-screen. The files would also define all che starting camera values, such as position, direction, field of view, and tile, The best part is that your gume designers have direct access to all elements within the text files. When core design decisions are flexible, the game is allowed ro evolve to its full porential, In fcr, the process of abstracting a game to its core helps tremendously in the design. Instead of designing ro a single purpose, you can design each component to its general functionality. In effect, designing flexibly forces you to recognize what you should really be building instead of the limited behavior outlined in the design document. For example, ifthe game calls far only four types of weapons, you could progeam a perfectly good system that encompasses all of them. However, if' you abstract away the functionality ofeach weapon, using dara to define its behavior, you'll allow for the possibility of countless weapons that have very distince personalities. All it takes is a few changes in a text file in ander to experiment with new ideas and game-play dynamics, This mindset allows the game to evolve and ultimately become a much better game. Did You Believe Me When | Said “Nothing”? The truth is that games need to be tuned, and great games cvolve dramatically from the original vision. Your game should be able to deal with changing rules. characters, races, weapons, levels, control schemes, and objects. Without this flexibility, change is costly, and every change involves a programmer—which is simply a waste of resources. If change is difficult, it promotes far fewer improvements to the original design. The game will simply not live up to its full potential. Idea #4: Script Your Control Flow A script is simply a way to define behavior outside of the code. Scripts are great for defining sequential steps thar need to occur in a game or game events char need to be iggered. For example, an in-game cutscene should be scripted. Simple cause-and- effect logic should also be scripted, such as the completion conditions of a quest or environment triggers. These arc all great cxamples of the data-driven philosophy at work. 1.0 The Magic of Data-Driven Design 5 When designing a scripting language, branching instructions require some thought. There are two ways to branch. The first is to keep variables inside the seript- ing language and compare them using mathematical aperarors such as equals ( = ) or less than ( < ), The second is co directly call evaluation fanetions that compare vari- ables that exist solely inside the code, such as IsLifefelowPercentage(so). You could always use a mix of these techniques, but keeping your scripts simple will pay off. A game designer will have a much casier time dealing with evaluation functions than declaring variables, updating chem, and then comparing them. Iz also will be casice to debuy eee seripts require a scripting language, ‘This means chat you need co create an entirely new syntax for defining your behavior. A scripting language also involves creating 2 script parser and possibly a compiler to convert the script to a binary file for faster execution. The other choice is to use an existing language such as Java, bat thac requires a Large amount of peripheral support as well. In arder nor co sink too much time inte this, it pays off to design a simple system. Overall, the cen dency is wo make the scripting language too powerful. The next idea explains some pitfalls of a complicated scripting language idea #5: When Good Scripts Go Bad Using scripts to data-drive behavior is a natural consequence of the dara-dsiver methodology. However, you need to practice good common sense. The key is remem- bering the core philosophy: Separate logic and dats, Complicated logic goes in che code; dara stays outside. The problem arises when the desire to data-drive the game goes too far. Ar some point, you'll be tempted to put complicaced logic inside scripts. When a script starts holding state information and needs to branch, it becomes a finite state machine. When the number of states increases, the innocent scripewriter {some poor game designer) has the job of programming. If the scripting hecomes sufficiently complex, the job reverts to the programmer who must program in a fictional language that’s severely limiting. Scripes are supposed ta male people's jobs easier, not more difficult, Why is ic so important to keep complicated logic inside the code? Its simply a mater of functionality and debugging. Since scripts arc not directly in the ende, they need to duplicate many of the concepts that exist in programming languages. The aacural tendency is to expose more and more funcconalicy tntil it rivals a real Lan- guage. The more complicared scripts become, the more debugging information is needed to figure out why the scripts are failing, This additional informacion results in more and more effort devoted to monitoring every aspect of the script as ie runs, ‘As you probably gucssed, non-trivial logic in scripts can get very involved. Months of work can be wasted writing seript parsers, camnpilers, and debuggers. Its as though programmers didn't realize they had a perfectly good compiler already in front of them. 6 : Section 1 Programming Techniques The Fuzzy Line There is no doubt thar the line berween code and scripts is firrzy. Generally, irs a bad idea to puit artificial intelligence (AI) behavior in scripts, whereis its generally a good idea to have a scripted trigger system for making the world interactive. The rule should ber IF the logic is too complicated, it belongs in the code. Scripting languages need to be kept simple, so they don’t consume your game (and all of your program- ming resources), However, some games are designed to let players write their own AL. Mast com- monly, these games are first-person shooters that allow the creation of bots. When this is the goal, its inevitable that the scripting language will resemble a real programming Janguags. An example of this situation is Quake C. Since bat creation was a require. ment of the design, resources and energy had to be put into making the scripting lan- guage as usefull as C. A scripting language of chis magnitude is a huge commitment and shouldnt be taken lightly. Above all, remember thar you dont want your game designers or seriptwriters programming che game. Sometimes programmers are trying w shirk responsbiliry when they create scripting languages. Ir'sall too casy te lure game designers into pro- gramming the game, Ideally, programmers should be boiling down the problem and exposing the essential controls in order to manipulate the logic. That's why program- mers get paid the big bucks! dea #6: Avoiding Duplicate Data Syndrome Its standard progtamming pretice wo never duplicate code, If you need the same behavior (for example, a common function} in two different spots, ie needs to exist in only one place, This idea can be applied to data by using references to global chunks of data. Furthermore, by caking a reference to a chunk of data and modifying some of its values, you end up with a concept very close to inheritance. Tnberitance is a great idea that should be applicd to your data, Imagine that your game has goblins that live inside dungcons. In any particular dungeon, your data defines where cach goblin stands, along with its properties. The right way to encapsu- lane this data is to havea global definition ofa goblin. Each dungeons data simply has a reference to thar global definition for every instance of a goblin. In order to make cach goblin unique, the reference can be accompanied by a list of properties ta over- tide. This technique allows every goblin w be different while eliminating duplicate data. ‘This idea can be raken to multiple levels by allowing each chunk of data to have a reference. Using this techaique, you can have a global definition of a goblin along with another global definition of a fast goblin chat inherits from the basic goblin. ‘Then inside each dungeon definition, regular goblins ar fast goblins can be instanced trivially. Figure 1.0.1 shows this inheritance concept using referencing and overriding of values, 1.0 The Magic of Data-Driven Design 7 Fast Goblin Goblin Instance Reference = Fast Gobdia| Postion ~ (3.0, 0) Facing = (1. 0.0) FIGURE 1.0.1. Dara inheritance, Idea #7: Make the Tool That Makes the Data Wich any lange game, tex files eventually become unruly and hard wo work with. The real solution is to make a tool that writcs the text files. Call this tool a game editor, a level editor, oF a script editor, but you'll speed up the game development process by building the sight cools. Having a tool doesn't change the data-driven methodology: ie merely makes it more robust and efficient. The time you save always makes the extra tool development time worth it. Conclusion ee 15 easy to buy into the dara-driven methodology, but it's harder co visualize che dra matic results, When everything is data driven, amazing possibilities unfold, An ceample of this rule is the game Toral Annihilaion. The decgner, Chris Tay- loc, pushed data-driven design to che limit. Tozal Annihilation was an RTS thar fea- tured two distincr races, the Arm and the Core. Although the entire game was centered on these two factions, they were never hard-coded into the game. Theoreti- cally, data could have been added te the game to support chree races, even after the game shipped. Alchough this possibility was never exploited, Total Annihilation cook full advantage of its flexibility. Since all units were completely defined by data, new units were released on a weekly basis over the game's Web site. In fact, many people arated their own units with functionality thar shocked evem the game's developers, The data-driven desien helped Total Annihilation maintain a committed follaw- ing in a crowded genre, Since Toral Annihilation, other games, such as The Sims, have cmployed the same idea by providing new data cantent over their Web sites. Without developers’ serious commitment ro the data-driven philosophy, his unprecedented expandability wouldn't be possible. 1.1 Object-Oriented Programming and Design Techniques James Boer Iris easy to understand the popularity of C++ among game programming profession- als, While not straying wo far from the highly portable and efficicnt roots of C, it also offers the design benefics of an object-oriented language. Inherent in his power, though, is the requirement that C++ code be properly designed and implemented. Although the object-oriented programming (OOP) paradigm was created to enhance program design, portability, and mainrainabilicy, the brutal fact of the matter is that poorly designed C++ programs can be worse than poorly written C programs. Many books and articles give good adviceon general object-oriented design prac tices; very few teach those practices with game programmers specifically in mind. Game programmers are a slightly diffecent breed than the typical application pro- grammer, Because their work is always expected to be cutting edge, pushing both human and hardware constraints to the limit, game developers tend to he much more willing to bend or even break traditional programming design rules. Unforcunarely, this tendency often has the negative side effect of creating unmaintainable cade due to a poor understanding or implementation of basic OOP principles. ‘As games grow more and more complex, companies are looking to reuse more and more code to mitigate ever-increasing development costs. Engine licensing is becoming more prominent as companies focus on content and game play and will undoubtedly grow into a major and separate support industry in the near future. This sort of development work requires much more stability and long-term planning than was previously known in the game development world. No longer is it acceptable to completely scrap your previous code with each new game. “This article obviously can't even begin to coverall thar a game programmer needs to knew. Instead, it identifies key areas in which a game programmer, and a company, can take steps to improve the quality and consistency of production code, which will in cum ead to both more robust and mote reusable librarics and. game engines. We also point you toward resources that much more choroughly cover the topics dis cussed. 1.1 Object-Oriented Programming and Design Techniques 8 Goding Style ee Programming style can often degradc inco a seligious argument, I'm not going to enter the debacle of where curly braces should he placed, but it is important for a company to adopt style, and for everyone in that company to use it. A company, not to mention an individual, should strive for consistency in class, function, and variable naming conventions as well. Many companies have adopted a simplified Hungarian notation scheme. The Hungarian notation was invented by Dr. Charles Simonyi, chief software architect of Microsoft, years. ago in order w help stan- dardize variable naming conventions. Some argue that such a naming convention is unnecessary in a type-safe language such as C++ and creates more work when chang- ing data types (since it requires changing the variable prefix), but others appreciate the ease and speed with which data types are visually identified. The basic premise of Hungarian notation is to preface the variable name with an identifier describing the type of data the variable represents. For instance, an integer variable named SomeVariable would instead be named iSoneVariable. In addition to variable types, pointers can be represented. A pointer to some class Fee might he called pfooobj. Prefixes can also be combined to provide more information than a single prefix can provide. For instance, a pointer to an integer would be represented by che prefix pf, ora pointer to a pointer would be represented 2s pp. Other types af scope information are often used in front of the type prefix, Memt- ber variables are labeled with n_, so an integer member variable might be labeled 8_i8onavar. Global variables (tsk, you shouldnt even really be using these) are repre~ sented as g_, and some variations represent static variables as s_, although this isn't seen as often. Although the formal Hungarian notation can be somewhat complex, many companics have adopted a simplified version of it. Table 1.1.1 presents an example of a common variation on formal Hungarian notation. You can find other descriptions in books such as [Peteold96], or you can find Simonyi’s ariginal paper on the World Wide Web in various locations. The most commonly used notational types are listed in the table, Objects are gen- erally nor given any prefix, with the exception of a few common classes such as those representing 3D vectors and points, Your company might adopr conventions for rep- resenting other commonly used utility classes as well. Note that most of the desceip- tive tags are quite logical and would not require you to look them up in a table, The cxact syntax you adope is mot as important as the relative consistency of everyone who conforms to it. IF all company code looks similar, it will be easier for programmers to work on code that they might not have written. One ward of caution: Dont over-engincer a coding specificatian. A page or wo should really be all that’s required to describe the company style. IF programmers have to look up how a variable should be named, they'll be far less likely to use the stane dard. 1 hesitate to recommend strictly adhering to Simonyi’s original system. lk's far too complex for day-to-day operations, and since readability is novr more important Section 1 Programming Techniques 4 An Example of Hungarian Notation Descriation Type 1 Inteper F Flat D Double (Hoar) z Long (integer) c Character B Boolean Dw Double word Word eT | 4] 3 Common Extensions Description Ste Cr+ aring object Ht Handle (user-defined ype) v Vector (user-defined class) Pr Point (user-defined class) Reb RGB wiplec (user-defined struct or type) Modifier Description P Pointer to R Reference 10 u Unsigned aurary Auray of Scope Description mn ‘Member variable 5 ‘Global variable 5 Seatie variable than type safery; there's no reason co create hard-to-read code when a simplified ver- sion will work just as well Class names should also be designed for ease of maintenance and readability. A convention thar has gained some popularity among Windows programmers is the usc of class prefixes to indicate general design intent. Classes beginning with the letter C are designated as Concrere clases, or classes with a specific use and implementation. Classes beginning with the letter fare Jrtenfiare classes, or classes intended to be used as design templates. These classes arc not used directly by applications; instead, they allave other classes ta be derived from chem. 1.1 Object-Oriented Programming and Design Techniques é 4 In addition tw or instead of these class prefixes, ix can also be helpful to prefix lasses by functionality. For instance, all classes dealing with a user interface (LIT) sys- tem can be prefaced with U1. This is especially helpful in programming environments and tools thar sort classes in a project alphabetically. Class Design ‘C++ classes offer an unlimited amount of design Aexbility, which can be both a good and a bad thing, There are no naming requirements, other than for your constructor and your destructor. However, you might want to self-impose a standandized class naming convention. Here is a simple example: class Sample { public: ‘Sanple() { Glear(}; + —Sanple() { Destroy(}; } veid Glear(); bool Greate(); yoid Update (): void Destroy); H The first thing you'll notice about this class is the trivial constructor, Implement ing classes this way is a goad idea for a number of reasons. Ta start with, the Cr+ con- structor has no return value. Therefore, it’s simply nota good idea to do anything that might fail. So instead, we simply call Cleart), a function chat clears our alll the imter- nal member variables. The benefit of clearing variables in a separate function is that it allows you to clear rhe class variables at any time. You'll see why this is especially important later. Actimes, you won't want to “activate” a class che moment itis created. This often happens for wrapper classes that are themselves members of another class. Lastly, there is an efficiency issue, Divorcing the object's acrual creation point from che con- satuctor allows you to dynamically create an object once but repeatedly call the Gre- ate() and Destroy() members to reuse the same object’s memory. Dynamically allocating memory is expensive, so when possible, ir’s best co avoid doing so. As men- tioned, the Create() and Oestroy() members do the work of actually creating and destroying whatever ic is the object represents. The Greate() function has a simple bool value for indicating success or failure. This value is both intuitive and easy to implement. Another popular choice of reurn type is standardized error code types (usually signed integers). Bools are easy ro use but require additional error-querying mechanisms if return codes are not provided. Exception handling, although theareti- cally superior te simple return values, tends to be hoch expensive in run-time perfar- 12 Section 1 Programming Techniques mance and casy for programmers to overlook. In addition, exception handling is nos self-documenting, as error codes of return values are in, header files. There is alse an importanc caveat forthe Destroy) function, Since we want bath the convenience of automatic cleanup and the flexibility of “destroy and recreate on demand,” we need to make sure that the Destrey() Function can be called multiple times safely or withour the create() function having been called. Be suse co ealll the tear() function at the end of your destroy function in order to reser all the object variebles back to their initial states. Game programming often means programming a real-time system instead of the more common event-based programming model found in most commercial applica- ons. We might want to recognize this difference in our class designs. The last por- tion of the class is the Update(} function. This is the “step” function, or the function that gets called once every frame, Its a big help to agree on a common name for this function. Depending on the elass, you might or might not want to implement the Update() function with a bool return value ro allow for checking of run-time errors in the step function. Class Hierarchy Design Knowing how to make the most of class reuse chrough inheritance is a key factor in object-oriented programming. Although a complete discussion of relationships between objects and how to implement them is beyond the scope of this article, there is a single design rule chat is of such importance thar it bears brief mention. ‘There are two primary methods of extending classes to work with each other: inheritance and layering. fuberitance, of course, is deriving one class from another Layering is when one object is contained as a member of another object. Layering is also known by such terns as composition, comszinment, and embedding The simple mule is this: Ifan object has an iva relationship to another object, use public inheritance. Ifa hte relationship describes the objects best, then use layering. What exactly do the terms ita and Aat-a mean? Pretsy much cxactly what chey sound like, Ifwe use chem in a sentence, the meaning becomes clearer: Class Corvere ira type of class Cas Class Corvere hard type class Radio. When deciding how to relate classes to each other, it’s oftca helpful w actually spealc the two relationships out loud. More often than not, the correct answer simply sounds correct. Design Patterns ‘When creating a solution to a common programming problem, most developers unconsciously refer to a similar problem char they have solved previously and then 1.1 Object-Oriented Programming and Design Techniques 13 cxtrapolare the new solution ftom the old. Design parsers are about formalizing these general software solutions to give a common frame of reference when discussing everyday engineering casks. A number of design patterns are described more thor- oughly in other books, bur here we diseuss parterns most commonly used by and rel- vane to game develapers, ‘Tho Singleton Pattern The singleton pattern is used when a single global object must be accessed across a wide number of classes and/or modules. Simply creating a non-local static objece works, but there are many problems inherent with thar practice, not the least of which is determining whea ehe object will actually be created, compared with other objects with the same global scope requirements. The singlevon pattern solves this problem by forcing access through a class, which stores static object internally. Here's what a basic implementation might look like: class Singletant { public: Singletoni4 Instance(} { Static Singleton obj; return Obj; It private: Singletont (1; This simple code solves the problem quite elegantly, However, if you want to derive new classes from this one, you'll be hard pressed ta come up with as elegant an cxtension. By changing the design and requiring more specific intervention during abject creation and deletion, though, we can expand on the basic singleton concept and allow extensibility to our original class: class SingletonBase public: Singletonbase() { gout << "Singletonase creatad!- << endl; } virtual =SingletonBasa() { Gout ss 'SingletosBase destroyed!" << endl; } wirtual void Aecoss() { Gout <= 'SinglatonBase accesseg!* << endl; } static SingletenBase* GetObj() { return mpobj; } Static vOLd Sét0bj(SingletonBasa* pObi) { mpobj - pooj; } protected: static SingletonBase* m_pobj; i; 14 Section 1 Programming Techniques SingletonBase* SingletonBase::m pobj; inline Singletenfase* Base(} assert (SingletonBase: :GetObjt)) i return SingletonBase: :GetObj(}; } H/ Greate 4 derived singleton-type class class SingletonOerived : public SingletonBase { public: Singlotanberived() { cout << "SingletonDerived created!" << endl; } virtual ~SinglotonDerived() { cout << ‘SingletonDerived destroyed" << endl; } virtual void Access() { cout <¢ ‘SingletonDerived accessed!" << endl; } protected: iF inline SingletonDerived* Derived() { assert (SinglstonDerived: :Get0bj()); return (SingletonDerived* )SingletonDorived: -Get0bj (}; } Hy} Using the code... // The complex singleton requires mora work to use, but is Honore flexible. It also allows mora control over sbject J] creation, which 1s sonetimes desirable. SinglstonDerived: :Set0bj (naw SingletonDerived) ; HW Notice that the functionality has been overridden by the new J class, even though accessing it through the original method. Base()->Access(); Darived()-2Access( #{ This variation on a singleton untortunately requires both HJ explicit crestion and deletion. selete Singlatonlerived: :Gatop] (}; ‘This modified form of the singleton class is not quite as simple in the construc- tien and destruction phases, bur the global access, which is the primary peint of the singleton, remains as accessible as ever. Furthermore, with the addition of inline accessor Functions, the cade becomes quite easy to read from the user's perspective, Singleton patrerns arc often used in situations in which you traditionally might think of using 2 global object or pointer to reference a single instance of a dass, An example might be a manager-rype class, where only a single instance (thus, the name of rhe partemn) is required. Classes that manage an application's sound, a user inter- face, graphics, or even the game ot application itself arc all likely candidates to Recometinelecmarpe cies 1.4 Object-Oriented Programming and Design Techniques i 15 So, why go tall chis bother inscead of simply creating a global object or pointer? There are a few great reasons. First, if you were planning on creating glabal objects, accessing an abject through a single function is easier than having to extern a glabal object in all your files. In addition. you gain the benefit af controlling exactly when your object is initialized. Second, if youre using a pointer instead of an object, you gain C++ control over every access of the abject, meaning that you gain benefits of access control, in turn meaning that you can do things such as monitor every time the class is accessed. Finally, if you create pour singleton with derived classes in mind using the techniques described, you can extend your base class while maintaining compatibility with the existing base class. Let's examine how this might work. In order to make the most of this sort of extensibility, you can imagine a scenario such as the following: Library A utilizes « singleton class, as described previously. Library B must usc Library A in order to function, so it is dependent on thase classes and includes their header files. Application C makes use of both Libraries A and B but requires changes to be made oo Library A for some game-specific items. Instead of having to ereate a new version of the library (and lose any improvements made to the original library by, say, a concurrent project), Application C can simply derive a new class (Class D) from Library A. If, as part of our singleton convention, we require char the application is responsible for allocating the abject, we can substirure derived Class D for Class A. By creating a new acecssor function with a new name thar returns a pointer to Class D instead of Class A, we can access all of D's new functions. How- ever, Class B will continue to use the old accessor function that returns a pointer to A and so will expect the old functions to function similarly to. the way they did before Nore chat virtual functions’ behavior can be overridden, but you must ensure that the new functionality is compatible with the old to preserve backward compatibility. In this way, the singleton pattern allows you to create a primitive library version- ing scheme. You can see how a simple technique can evolve into a powerful mecha- nism for code organization and reuse. You can find in this book another variation of the singleton pattern in the article "An Automatic Singleton Utility.” by Scat Bilas. Tn this article he describes an elegant method of using vemplates and public inheri« tance to automate the creation of singleton classes. Facade Pattern ‘The singleton segues nicely into the next pattern welll investigate: the fagacde patterm. This pattem is penerally used as what is often referred wo 24 a meumnager clase. This is 2 class that provides a single interface no a lange collection of related classes, usually same sort of subsystem. These classes are often designed as singletons because it usti- ally males sense to have only onc manager object per type of subsystem, For example, you need only a single ebject co manage access 19 your audio or graphical user inver- Face subsystems, “A fagade or manager is necessary in order to keep interdependencies berween classes, otherwise known as cowpéing, to a minimum. One can imagine in a theoretical Section 1 Programming Techniques SubyysierL FIGURE 1.1.1 An example of bad coupling. worst-case scenario thar every class in a project “knows” about and requires explicit access to every other class, as illustrated in Figure 1.1.1. The maximum number of interdependencies berween clases can be described as (n-1}', where » is che number of classes ina project. The problem with this sort of interdependency come: when an entire subsystem needs to be heavily modified or even replaced. Object-oricnted programming protects against implementation changes within single lasses, but a new paradigm is needed for protection against more sweeping changes. The fagade pattern solves the same sort of problem that object-oriented programming protects against, but on a much larger scale, The general rule of thumb when implementing fagade classes is this: Whenever possible, avoid exposing intemal subsystem classes to outside systems, This is not always possible to do entirely, but with some clever coding and function wrapping, you can reduce the exposure of these classes a great deal, as illustrated in Figure 1.1.2. Every dass you hide means that there is less worle to be dane the next time that sub- system has ro be reimplemented. Stato Pattern Almost every game programmer has had to deal with the problem of keeping track of constantly shifting game states in real time. States usually start out as simple enumer- ations, and behavior is implemented based on switching berween states in a switch +++ case structure. Problems cin develop, however, when the number of states starts growing larger and functionality must be shared in a greater number of these states. A cut-and-paste nightmare can quickly ensue, wherein the programmer tries to find all the srares thar share code and make sure that any changes to onc state occur in all of them. 1.1 Object-Oriented Programming and Design Techniques 47 Subsysteen] FIGURE 1.1.2. Using fagade classes to seduce interdependencies, AA more elegant object-oriented solution is to simply use objects to represent logi- cal states. The advantages of using objects are that states are better encapsulated, states can logically share code in their base classes, and new seates can easily be derivedl from ing ones using inhesitance. These advantages reduce the typical problem of hav- ing ta cut and paste code between discrete states, a shown in Figure 1 Alchough the pattern does not specify how state mansitions arc to be made, itcan often make sense to leave the transitions of lasses to 2 central manager. In this man- ner, Inter-object dependencies can be avoided, leaving only the manager to worry about having to know all the different stare objects, Better yet, che states can simply be enumerated and created through the use of a factory object, which is explained in the next section, The state pattem does not necessarily have co be used only co represent discrete Bam states, It can also be used in AI systems or even to represent different types of game medes within a single game. By tepresenting each game mode as a different object, for example, you gain the flexibility of allowing new behavior to be added after r =a OnEnter() | StateMenager BaseState Update() ; sa —-~ On Exit) | Dynamicallyswitches | betweenstate 1 objectsinreal-time t Aes gees ‘ Stet | | statez || Stated FIGURE 1.1.3. Using che stare pattern, Section 1_ Programming Techniques the initial release through the use of dynamic link libraries (DLLs) ar other means of adding an objec to existing code dynamically. Factory Pattern ‘The jaciory parcern deals with organization in the creatlon of objects. A form of che pattern is defined as a method for allowing abscract interface classes to specify when to “rate concrete, derived implementation classes. This method is offen required in application [rameworks and ather similar class hierarchies. However, game program- mers often deal with a specific subset af the factory pattern—namely, the use of face tory abjects with enumerated object creation located in a central class, usually via a single-member function. In English, this means that a single object is responsible for creating a wide vari- sty of other objects, usually related by a common base clase, This class ofven tales the form of a class with 2 single method chat accepts some sort of class ID and renurne an allocated object. The advantages of dustering abject allocation inta a single location are especially noteworthy for game developers: * Dynamic memory allocation is expensive, so you want ro carefully monitor allo- cations. Allocating all objects in a central arca makes it easier to monitor these allocations. Often, common initialization or creation methods must be called for all objects within a class hicarchy. If all object allocation is put into a central ates, ir becomes easy to perform any commen operations (such as inserting chem into a fesource managerjan all objects, * A Goory allows extensibility by allowing new objects to be detived from the existing factory, By pasting ina new class ID (which can casily be obtained from data instead of code), you can provide run-time extensibility of new classes with- ‘out changing the existing base codc, ‘The final poine stresses extensibility as a benefic of using the factory pattern. For this reason, creating simple functians or static classes should be avoided, since you cannot derive new classes from them: Baseclass* ClassFactory::Greataobject (int id) { SaseClass* pClass = switch(idy { case t: PGlass = new Classi; break; ease 2: pClass = new Class2; case 3: BGlass - new Class3; brea 14 Object-Oriented Programming and Design Techniques 19 default: assert(l*Error! Invalid class 10 passed to factery!* Jf perhaps perfors some comtan initialization is needed pClass-=Tnite); return pOlass; } You can see thar chere is technically nothing sophisticated abut a factory creation method. However, centralizing these object allacations provides a powerful organiza- tion and extensibility mechanism. Factory patterns ase used whenever large numbers of different objects within an object hierarchy must be dynamically created at un time. This can include Al objects, resources such as textures or sounds, or even more abstract objects such as game scates (as in the previous discussion), Summary Developing good object-oriented techniques is not an end in itself, It should pervade very aspect of your code, which will save you time and trouble in the long run, Well- written object code is more flexible, morc maintainable, and more extensible than preceduural code. Game progrimuners have not adopted a complex new language and ceding paradigm for their personal entertainment; there is a method to their madness. ‘There are several references listed at the end of this article. Do yourself a fvor and immediately pick them up if you dont yee own them. They are indispensable tools in learning the finer points of both object-oriented programming and C44 lan- guage usage in general. Referen ee [Gamma] Gamma, et. al., Derign Petters, Addison-Wesley Longman, Inc., 1994. [Meyers98] Meyers, Score, Effective C+, second edicion, Addison-Wesley Longman, Inc, 1998. [Meyers96] Meyers, Scor, More Effective C++, Addison-Wesley Longman, Inc., 1996. [Pecold96] Perzold, Charles, Programnsing Windows 95, Mictosoft Press, Ine. 1996, 1.2 Fast Math Using Template Metaprogramming Pete Isensee When programmers think of C++ templates, they usually think of things like the STL, generic containers, and type-safe macros. Most programmers are unaware that templates can act as virtual compilers, creating tremendously optimized code in terms of both speed and size. This unfozeseen quality of cemplates was first noticed hy Erwin Unruh in 1994, He presented the C++ Standards committee wich a template program that didnt compile but instead coerced the compiler into generating a lst of prime numbers in its error messages [Veldhuizen9]. ‘This discovery led a number of language experts to focus on the use of templates as precompilers. Todd Veldhuizen and David Vandevoorde greatly expanded oa this capability, showing that virtually aay algorithm could be templatized, provided che input parameters to the algorithm were known ar compile time [Veldhuizend5]. Given a good compiler, intermediate code can be completely optimized away, result- ing in extranedinary efficiency. The best way to see this in action is to consider a simple example. Fibonacci Numbers The Fibonacci sequence looks like this 0, 1, 1,2, 3, 5, 8, 13, ... The general equation for the sequence is Fié(a) = Fib(n-1) + Fib(-2). A typical function to recursively gen- erate Fibonacei numbers is as follows: unsigned RecursiveFib( unsigned n ) a if(n<=4) return nj return RecursiveFib( n-1 } + RecursiveFipg n-2 ); Believe it or nor, this simple function runs in exponential time, Irs highly incfli- cient and should never be used in production eode, The function is simply 2 step- ping-off point for generating a templatized version: 1.2 Fast Math Using Template Metaprogramming 24 templates unsigned N > struct Fib t shun { J] Recursive definition Val = Fibe N.1 >::Val © Fipe N-2 >::val hi hi f/f Specialization for base cases 7 (termination conditions} template <> struct Fib< 0 > { enum { Val = 0 }; }; template <> struct Fibs 1> { enum { Val = 1}; }; // Make the template appear like a funetion Sdefine FibT( m ) Fib< m >::Val ‘An example “call” co the template via the define: stdircout << FibT( 4); // Fibs 4 >::Val Some important things to nore abour the templatized version are as follows: ‘The template function is not really a function ar all—ir's an enumerated integer called val, recursively determined at compile rime. The notation Val ~ Fibsh- 1>2:¥Val + Fib«N-2>:;¥al is uncommon bur valid C++ syntax. Fid is defined as a struct co simplify the notation. Struct dara is public by default, which is exactly whar we want. Similar notation is used for all the fallowing code listings, ‘The template parameter Nis used to specify the function input. This is an uneom- mon but perfectly acceptable use of template parameters. For example, std: :bit« setetiz uses the numeric valuc Was its template parameter to define the aumber of bits represented. This numeric parameter must be lmown at compile time. Call- ing FibT(i) when d is a non-const variable will generate a compiler crror. ‘To terminate the recursion, you must handle the base case properly. For Fibonacel numbers, the base case is when Wis zero or one. With templates, che way to han- dle base cases is with template specialization. The noration tenplate=> indicates a specialization. When NW is zero or one, Val = H. Now consider haw 2 compiler might evaluate FibT(4): FibeO>::Wal + 1+ 1+ 0 +0 22 i Section 1 Programming Techniques Since all inputs are known at compile time, a compiler can reduce FApT(N) toa constant. In other wards, the compiler can produce exactly the same code as though you had written: std::cout << 3; 1) Fib[4) ‘This is an amazing tool co have in your C++ tool kit, It’s not every day you can go from exponensial run time to constant rm time. With template metaprogramming, the price you pay is additiexal compile time instead of additional execurion time. For games, execution time is usually more critical than compile time, so chis technique is very appealing. Factorial Here's another cumple of tuming a standard function into a cemplatized version. First, the standard C++ version for reference: unsigned AecursiveFact( unsigned n } retura ((n <= 1) 7 1 : (n * Recursiveract(n - 1)))} ‘And the template metaprogram version: ff Templatized factorial(a) templates unsigned N > struct Fact { enum { Val =" * Facts N- 1 =::¥el }; hi ff Specialization for base case Template < struct Facts 1 > enum { Val = 1}; ff Make the tenplate appear Like a function Sdefine FactT( mn) Facte n >::¥al. Asin the Fibonacci example, the compiler can reduce a “call” such as FactT(4) w the constant 24. We've gone from linear run time to constant ran time. Thar's a pow- erful argument for using metaprograrnming, There ate two drawbacks: a compile- time penalty, which is typically insignificant, and a code rradability penalty, which can usually be hidden by a well-defined macro such as FactT(n). Ler’s step back for 2 moment. Template metaprageamming is compelling and undeniably efficient, bur nor many games require a Fibonacel sequence or factorial number generation. Even if they did, it’s not likely hat the code will know the 1.2 Fast Math Using Template Metaprogramming 23 required inpur parameters at compile time. Is this justa near C++ party trick, or is this something thar's actually useful? Trigonometry ‘Time for a more complicated example. Lets take om the sine function, Many games use sine tables or 2 similar method for fase trig calculations. What if we could make the compiler read something like x = sine(/.234) and generate 2 single move instruc- tion? Template metaprogramming to the rescue! Generating standard trig functions involves using a seties expansion. For sines, the expansion looks like this: sine(x) =x— (2/31) + (8/5) —(7 FT) 4 GES —., where x is in radians, 0 < = x <2n. To compute the terms efficiently, we can rewrite the expansion: sine(x) =x * cermi{0) where rerm(m) is computed recursively as: termf{in) = 1-2 / (2m 42) f (2m + 3) * cerm(el) You cin write this expansion without templates as follows double Sine( double fad ) const int iMaxTerms = 10; roturn Thad * SineSeries( thad, 0, iMaxTerms ); double SineSeries( dewble fhad, int 1, int iMaxTeres } if{ i > iMaxTerns ) return 1.0; roturn 1.0 fad * thad / (2.0 * i+ 2.0) j (2.0 * 4 + 3.0) * SinaSeries( ffad, i +1, illaxTerns ) }; } Increasing iutexTerms improves accuracy at the expense of run-time speed. It's not difficult to convert this to a templatized version. The solurion is presented in Listing 2.1, The solution uses two templace objects: SinecR> computes & * term(() and Serios computes terry) recursively out to the number of terms specified by WaxTores. Wich the template metaprogramming version: double x = SineT( 1.234 pp the compiler can theoretically generate the same code as though we had written: double x = 0.94381820937463368; 24 Section 1 Programming Techniques ‘The actual value of sin(1.234) is 0.94381820937463370.... so che template ver- sion, which evaluates 10 terms, is accurate ta approximately 15 decimal places! With- out much effort, we have a solution in which we can get sines for free (i.e., constant time) by using the compiler as the workhorse. We den't have to compute a table at fun time or embed a table in our executable, because the compiler can generare the table entries we need (and only the ones we need) at compile time. Compilers in the Real World ‘There's potentially a hig problem with template metaprogramming. Many compilers available today (circa 2000) can't reduce the recursion and mathematics involved with complex template-based algorithms. In che sine example, evaluating che series expan- sion to 10 terms requires the compiler so reduce approximately 20 floating-point multiplications, 50 integer multiplications, 20 FP divides, 10 FP subwactions, and 10 recursive calls down to one or two move instructions, Coulda compiler do tha? OF course. Shondd a compiler do chav? Probably, given ample resources (RAM and time). Willa compiler do chat? Ie depends. Ttested the preceding examples using Microsoft Visual C++ 6.0. C6 did a splen- did job with the Fib and factorial templates, producing single move instructions for cach, It had more difficulty on the sine template, gencrating code thar is inferior even to the C run-time sin() function! By default, VC6 was able to uneoll the recursion to only eight levels, and it hardly optimized the arithmeric at all. Using the VOS-specific Wpragnas inline depth(255) and inline_recuraion(on) allowed WC6 to unroll the recursion completely and optimize away all the marh, so fortunately, good results are still possible. ‘The moral of the story is that all optimizations are guilry until proven innocent, Examine the code produced by the compiler, and evaluate the performance before and after eemplares are introduced as an optimization. You might need to wweak some compiler flags co get the resules you wanr, In the future, expect compiler writers 10 focus more heavily on template optimization and templare metaprogramming itself, In the meantime, progeam softly and carry a big profiler, Trigonometry Revisited Given chat C++ compiler technology is still immacure when it comes to dealing wich templates, is there anything we can do to improve our chances of the compiler doing the right ching? Listing 12.2 shows another amempe at the sine function. The recur sion has been removed and the series expansion is inline out to 10 terms, We're down to 12 multiplications, 21 divides, and 10 subtractions. We've also eliminated the tem- plate specializarion since ir’s no longer needed. 1.2 Fast Math Using Template Metaprogramming 2 ‘The resulting function is a bit casier to handle, from both a readability seandpoint as well as a compiler standpoint. Indeed, VC6 has an easier time with this version. ‘The special sprageas are no longer required for che compiler to generate a constant. Atthis point can we clearly sce the benefits and drawbacks of the rechnique. Tem- plate metaprogramming can generate massive speed improvements—sometimes, but not bays: Templates and Standard C++ Not many compilers are complecely compliant with the C++ standard, especially when it comes to templates. Templates are so flexible and powerful, compiler writers have a tough job getting them right. In no place is this more apparent than Visual C+, which was slow to adopt templates in che first place and slawer still ra conform to the standard. For example, VC6 docs not support partial specialization, making the specialized versions for many of the template functions more camplicated and less generic than they need to be. Much more important, however, is the support or non-supporr of floating-point template parameters. The sine cemplare functions in Listing 1.2.1 and Listing 1.2.2 use a floating-point template parameter for the incoming radian value. However, according to the C++ standard, “a non-type templare-parametes shall not be declared to have floating point ... type.” In-other words, ona conforming compiler, templates double R > struct Sine // compiler errer gives an etroc message, The solution is wo use a reference parameter instead: Template< doubles R > struct Sine // OK Tnverestingly and unforrunately, Visual C++ 6.0 supports floating-point types as template parameters but does maf support references as parameters. It has things cam- pletely backward! To use the sine template code on conforming compilers, change double A to doubled R. Matrices a Where template metaprogramming really comes into its own is the handling of maxtix operations. Three-dimensional games heavily use matrices, Templatizing key functions can generate noticeable speed improvements. In the following section, we use templates to improve initializing, transposing, and multiplying matrices. Section 1 Programming Techniques Identity Matrices ‘The identity matrix contains elements whose values are zero, with the exception of the diagonal, which contains ones. We begin with a typical implementation, Note the embedded for loops: matrix33& matrixndd; :identity() for (unsigned ¢ = 0; c < 9; crt} for (unsigned r = 0; r <3; ree) coll cj[r] “(errr ) 71.0: 0.0; return “this; ‘The template metaprogram version is shown in Listing 1.2.3. The parameters for the template version include the matrix utx, the size of the matrix W (a square matrix isassumed), the current row A and column G. At every iteration, we evaluate the next element of the matrix, ‘The key thing to notice is the method by which we loop over the columns and rows, At each step in the algorithm, we knew 1, which simply goes from 0 to W squated. Given I, we can compute the current row by taking 1 modulus, For aam- ple, if N is 3, the row scquence would be: 0, 1, 2,0, 1.2.0, 1, 2, 0. The current col- umn is f divided by N mod NW. IFW is 3, che column sequence would be 0, 0,0, 1, 1, 1, 2, 2, 2, 0, The template specialization terminates the algorithm when 1 reaches w squared. Now we can replace the original version with: Matrix3& matrixdd: :identity{) IdentitywtxT( matrixsa, tthis, 2}; return “this; The compiler can expand the new version to: matrix33& matrin3d:sidentity() col[ 0 ][ 0] = eol[ 0 ][ 1] = Phcwas eol[ 2 J[ 1] = solf 2 1[ 2] = return "this; 1 In other words, the compiler can complrtely enroll the lagp. OF course, we could have unrolled the loop ourselves, but the template version is a general solution. It will work for square marrices of any size (provided you include the specializarian). For eample, the code for 44 matrices would look lilse: matrix44& matrixe4:sidentity() 1.2 Fast Math Using Template Metaprogramming 27 IdentitywexT( matrixa4, ‘this, 4 }; return “this; Matrix Initialization ‘We can create templatized initialization code by using dhe same technique we used in generating the identity matrix. In fact, the only line chat needs to change in Listing 1.2.3 is the line thar determines each mamrix element value: mx( CJT AR] = ¢ G == A) 7 1.0: 0.0; sf identity matrix which is replaced by: mtx[ CJ[ BR] - 0.0; J/ zero matrix or more generally by: mtx{ C J[ R] = statie_cnst< F >( Init }; // init matrix where F is the type of value stored in each element and Init is a numesic template parameter that defaules to zero. The genezal solution allows you to easily initialize matrix elements to any constant value, Matrix Transposition ‘Transposing 2 marrix flips the matrix using the diagonal as the axis: matrix33& matrix33::transpose(} tor (unsigned c = 0; ¢ < 3; ce) for (unsigned r= + 1; r <8; r+4) std::swap( col[ ¢ ][ fr], coll r If ec] 3 return "this; + ‘This algorithm: cries our for optimization because it dees su little actual work. For 2.3 3 matrix, there are only three swaps. Fora 4 x4 matrix, there are only six. List- ing 1.2.4 shows che templace implementation, We can aw replace the original with: matrix33A matrixa3::tranapase() TransittxT( matraxss, *tmis, 3); return *this; which the compiler will expand co: hatring3& matrix: ranspose(} Section 1 Programming Techniques std::awap( col[O]{1], col[1][0] ); std::swap( col[1][2], col[2][t] }; return *this; } The embedded for loops are optimized away, leaving only the swaps themselves. Sap itself is an inline function, so we're down to only nine move instructions. Docsnit get much better than chat. Matrix Multiplication For our final look at metaprogramming, we templatize matrix multiplication. struct Sine { enum { MaxTaras - 10 }; // increase for accuracy Static inline double sin() return Ai* Series< A, 0, WaxTerms >::val(); i template< double R, int I, int maxTerns > Struct Series i enum ff Continue 4s true until we've evaluated M terns Continue = 1 +1 != waxterns, Nxtl = ( 1 #1) * Continue, lixtNaxTerns = Maxterns * Continue ff Recursive definition called once per term Static inline double val() return 1 - A* Rs (2.07 1 + 2.0) 7 {2.0 * 1+ 9.0) * Series< & * Gontinwe, Nxtz, NxtiaxTerms >::val(}; } i #/ Specialization to terminate the loop a2 Section 1 Programming Techniques template <> struct Series< 0.0, 0, 0> 4 static inline double val() { return 1.0; } } JJ Make the template appear like a function doting SineT{ r ) Sine< r =::sin() Listing 1.2.2 ee i] Series expansion for sin{ R }. JJ For conferming compilers, change double R to doubles A template < double A > struct Sing t HALL values known at compile time. Ff A decent conpiler should be able to reduce ff-to @ single constant. static inline double sin() t double Asqr = R © A; return A * - Rear - Regr - Rsqr - Regr Rsqr - Reqr = Rsgr - Rage - Regr - Rage reeeseccece Sie ese 68 oe } re (/ Make the tomplate appear like a function fdefine SineT( r ) Sine< r »::8in(} Listing 1.2.3 ——— eee {4 Tomplatized identity matrix; NM is matrix size templates class Mtx, unsigned N > struct TdMtx { static inline void eval( tx mtx ) { IdMtxImpl< Wtx, N, 0, 0, © >::eval( mtx pj } ve 1 Assigns each elenent of the matrix tomplate< class MEX, ungigned N, unsigned C, unsigned A, unsigned I > struct [dutxlmpl t 1.2 Fast Math Using Template Metaprogramming nom { Natl =r +1, (f Counter Nxth = NxtI % N, if Row (inner loop) Niet = NxtT SN AN // Column (outer leap) = static inline void eval( Wtsk ate ) { atx C IPR P= (C=R) 21.0: 0. idMteInpl< itt, M, NxtG, Meth, MxtT eval( mtx ); } LE ff Specialize for 9x3 and 4x4 natrix templates struct IdwtxImpl< natrix33, 3, 0, 0, 9*3 > { Static inline voig aval( matrixgs& ) {} i tenplate<> struct [aMtxInple matrixgs, 4, 0, 0, 4*4 > { Static inline void eval( matrixass ) {} hi (f Moke the template appear like a tunction define IdentitywexT( wtxType, Mtx, N} 4 Idltx< WexType, NM >::eval( Wtx ) Listing 1.2.4 TT ‘/ Tenplatized transpose; N is matrix size templates class Mrx, unsigned N > struct Transitx 1 static inline void eval( Mtx& mtx 7 { r TraneittxInple Mix, N, 0, 1, 0 >z:eval( mtx 4; templatec class Mtx, unsigned N, unsigned ¢, unsigned A, unsigned I > atruct Transittximpl t enum { Natl =141, NetG = Net | N &N, Neth = ( Nxt & N ) + Nxt + 1 i Static inline void eval( Mtx& atx ) { it( R< mw) stdsrawap( mtx[ 6 1[ RJ, mtx AIT} ); Trancutxinpl< Mtx, N, NxtC, Nxtfi, NxtI >:zoval( mtx ): } hi Section 1 Programming Techniques (] Specialize tor 3x3 and 4x4 matrix Template<> Struct TransMtxINpl< matrix33, 3, 0, 1, 3°3 > { Static inline void evall metrixa3& } {} h Template<> struct TransMtximpl< matrixa4, 4, 0, 1, 4"4 > { static inline vais eval matrixase ) {} hs JJ Make the template appear Like define TrangutxT( MtxType, Mex Transiitx< MtxType, Mf > Listing 1.2.5 & function wou eval( itx } if Templatized multiplication; N is matrix size template< Claas UTX, unsigned N > struct multutx { Static inline void eval( Mtx& 1 MuLtiexImpl< Utx, Ny 0, 0, } Ki template< class tx, unsigned N, unsigned K, unsigned T struct MultitxlepL P, Const Mtxk a, const Mtxa b ) Q, O >sseval( ry a, b )5 unsigned G, unsigned R, > 1 onun 1 Nxt = 1441, Jf Counter Nixtk = Nett %& N, J} Internal loop Nxt = Netl NSN, // Colunn Moth = Mtl FNS NAN SS Row aa static inline void eval( Wtxk r, const Mtx& a, const Mtx& b } { FEC ILA] = al KIER] * bE CIE K IF MultletxInpl< Mtx,N,MXtC,Ncth MetK,Netl >trevald rya,b J; " Hf Specialize for 3x3 and 4x4 matrix tenplate<> struct MultMtxImpl< matrixa3, 3, 0, 0, 0, 3*3*3 > { gtatic inline void eval( matrix334, const matrix3i4, Const matrix33& } {} He template<> struct MultWtxImpl< natrixd4, 4, 0, 0, 0, 4*4*4 = { static inline void eval( matrix4é&, const matrix44s. 1.2 Fast Math Using Template Metaprogramming 35 const matrixdéd ) {} Hh ff Make the template appear like a function Adefine tultutxT( MtxTypa, r, a, b, NY} \ Multitx< MitsType, N2iceval( r, a, b) References a [Veldhuizen99] Veldhuizen, Todd, “Techniques for Scientific C++." Available wonw.extreme.indiana.edu/—tveldhui/papers/techniques!, August 1999. [Veldhuizen98] Veldhuizen, Todd, ccal., “Blitze+ Numerical Class Library.” Available vwe.oonumerics.org/blin/,August 1998, [Veldhuizen96) Veldhuizen, Tedd, and Kumaraswamy Ponnambalam, "Lincar Alge- bra with C++ Templace Metaprograms,” Dr. Dobib Journal, August 1996, [Weldhuizen95] Veldhuizen, Tedd, “Using Cee Template Metaprograms,” C++ Report May 1995_ [Pescio97] Prscio, Carlo, “Binary Constants Using Template Metaprogramming,” CiC++ Users Journal, February 1997. [Karmesin99] Karmesin, Steve, et al. “PETE, Porrable Extension Template Engine,” Available waww.acl.lanl.gow/pete!, February 1999. 1.3 An Automatic Singleton Utility Scott Bilas This article presents an easy and safc method to provide access to a C++ class single- ton while retaining control over when ic is instantiated and destroyed. Definition Asingletan is an object that has only one instance in a system at any time. Some com- moa examples of singletons in games are managers for tcxmure maps, files, or che user interface. Each is a subsystem thar’s generally assumed to be available ance the game has started and will stay in existence until the game shuts down. Some of these subsystems can be implemented using global functions and statle variables. An example would be a memory manager's nallec() and free() functions. These types of subsystems are not singlerons in char chey don't have their functional- icy encapsulated into a class and can't be represented using a single instance of that class, There's no reason a memory manager such as this couldnit be converted into a class and used as a singleton, buc this practice isnt common. An example of a singleton is a sextire map manager. It could be called Textureligr and have methods such as Gettexture() and useTexture(). Itt purpeie would be to find texture maps in the file stare, convert them to system graphics objects, make them available to the rasterizer(s), and own chem until they are no longer needed, at which point it deletes them. Only one instance of Texturetigr will be needed in the system, so this class would namurally be used as a singleton. intag) What's the poine of singlecons? First, chey provide conceprual clarity because labels are very important. Calling a class a singleton and following a naming convention (such as -Mgr, Api, GLobal-, etc.) relates importance details abour how we intend that class to be used. Singletons also provide notational convenience. Every object in a Cee system must be owned by something. The ownership pattern of these objects depends on the 1.3 An Automatic Singleton Utility a7 game, bur ir often resembles 2 multilevel hicrarchy, in which cach higher level owns a set of child objects, cach of which in turn can own child objects, Each object pub- lishes a set of functions to access its children. For example, to get at che Texturatgr instance, you might need co call a sequence of functions such a5 GetAgp()-2Gerser- vices ()->G=tGui()->GetTexturelge(}, where each function remrns 3 pointer to the requested child object. This system is inconvenient and not exactly efficient, consid- ering the multiple dereferences. Singlerons can solve this problem because they are treated as global objects. The Problem Well, chen why not just use global objects? They are certainly convenient; the Tex- tureligr abject could be accessed through a g_ToxtureNige abject reference that has been declared with external linkage at global scope (or withia a namespace) or per- haps through function that returns a reference to chat object instead. However, the construction and destruction order of global objects is implementation dependent and generally impossible to predict in a portable manner: There are workarounds to all these problems, but what we really want is a way to have the convenience advantage of treating a singleton like a global object, without the inconvenience of losing control over when and where it ets constrected and destroyed. Traditional Solution ‘The textbook solution to managing a singleton usually looks something like this: Textureigré GetTextureligr( woid ) i static T s Singleton; return ( © Singleton ); There are many variations that use templares and macros for netational conve- nience, bur the effect is still che same, This solution allows singleton to only be instantiated on demand—the first time this function is called. It's convenient to use, bbut it leaves ins destruction up to the compiler and requires that it be done anky at application shutdown time. We need more control than thar. Order of destruction is very important in 4 game in that some subsystems must be shut down and destroyed before others. Furthermore, what if we want to shut down only part of the game while it's still running? Doing so is impossible with this solution. 38 Section 1 Programming Techniques A Better Way All we're really afier is the ability to track a singleton, and for thar what we need is a pointer to it. Whar if we were ta de something like this: class TexturaNgr { Static Taxtureiige™ ms_Singleton; public: Textureligr( void ) { ms_Singleton = this; /*...*/ -Textureligr( void }) { ms_Singleten = 0; *.. 8h} if... Textureugré Getsingleton( void) { return { "ms Singleton }; } i Add a few assertions for safety purposes, and this solurion would work! We can now construct and destroy a Texturewar wherever we like, and accessing the singleton is as simple as calling Texturetige: :GetSingleton(). However, this solution is still a liule inconvenient, given that the same code (to track the singleton pointer) needs to be added to every singleton class, An Even Better Way A more generic solution is xo usc ccmplates co ausomatically define the singleton pointer and do the work of setting it, querying it, and clearing it, It can also check (through ascert()) co make sure that we aren accidentally instantiating more than one, Best of all, we can get all chis functionality for free just by deriving from this sim- ple litdle class: #include ‘tenplate class Singleton { static T- ns_Singleton; public: Singleton( void ) { assert( ims Singleton J; int offset = (int}(T*)1 - (int) (Singleton T* Singleton , 2. Make sure that you're constructing an instance of MyClass somewhere in the syse tem before using it. How you instantiate it doesn’t matter. You can ler the com- piler worry about it by making i a global or local static, or you can worry about it yourself via new and detete through an owner class, Regardless of how and when you construct the instance, it will gec tacked and could be used as a single- ton through a common interface by the rest of the system. 3. Call MyClass; :GetSingleton() co use the object from anywhere in the system. If youlre lazy lilee me, you cam #define g_MyClage te be MyClass: :G2t8ingleton(} and treat ir exactly like a global object for nararional convenience. Here is a sample usage of the class: class Texturemgr : public Singleton purely to inherit this convenient func- tionaliry. This doesnt affect the size of the class in any way; it only adds some auto- matic function calls. Se how does this work? All the important work is done in the Singleton con- structor, where it figures our the relative address of the derived instance and stores the result in the singleton pointer (n¢_singleton). Note that the derived class could be deriving from more than just the Singleton, in which case “this” from tyClass might be different from the Singleton “this.” The solution is to take a monexistent object sit- ting at address x1 in memory, cast it to both rypes, and see the difference. This dit- a0 Section 1 Programming Techniques ference will effectively be che distance berwecn Singletea and its derived type MyGlass, which ie can use to calculate che singleton painter. References Meyers, Scott, More Effective C++, Addison-Wesley Publishing Co., 1995. Using the STL in Game Programming James Boer In 1997, C+ was officially standardized, ending a nine-year process thar not only defined the official language specifications but also gave C++ programmers a massive new sec of tools in the form of the standard C++ library. A large portion of this library isthe Stindend Template Library or STL. The STL is a collection of container feollec- tions of data) classes, ranging from vectors to balanced binary trees, In addition to the basic containers, the STL provides a massive amortment of algorithms that can oper- are on those basic containers. A-common concern is~whether using STL will slow down your code. The truth of the matter is thar the STL was designed with speed asa foremost priority. For isstanee, vectors do no bounds checking, and iterators are never validated before attempting to access a container. The net result is char, for example, STL vecears can produce code with performance equivalent to that of a simple dynamically allocated array, Other containers fate just as well when put under the performance microscope. ‘The STL was designed far high-efficiency C+ applications, Dont lose deep about using them extensively in your code. STL Types and Terminology ‘The STL is a large and somewhat complex portion of the Standard C++ library, Using the STL effectively requires the understanding of the basic components and how they work together, Containers STL containers represent the dassic data abstractions and organization schemes such as vectors, lists, queues, and maps. However, we should make a few distinctions between certain types of containers anc how they are implemented. The STL containets’ vector, list, and deque (pronounced “deck”) are implicit data types thar both describe an absteact data type and imply a specific method of imple- at 42 2 Section 1 Programming Techniques mentation, A vector is, of course, a dynamically resizable amay. ‘The ice is imple- mented as a double-linked list. A degne, or double-ended queue, is implemented in a manner that allows amortized constant time insertion or deletion of elements ar either end of a randomly accessible array-type structure, Deques are also known as sequence contafners because they store ordered sets of data, meaning that the ander in which you insert che data affects the order in which they are stored. Conrainers such as stack, queue, and priority_queue are slightly higher-level abstractions. They describe a conrainer's behavior but allow for different rypes of underlying implementations. For example, a queue might be implemented using a vector, list, er deque internally. These are known as conrainer adapter. Container adapters, because they rely om sequence containers as their underlying data, als fall into chat category as well. Other containers, such as a map, set, multimap, or multiser, are all implemented internally as red-black mees (balanced binary crees) bur offer different container behaviors. These are also known as atrocketing containers because the data inserted into them is ordered based on a certain sorting criteria, Herators: Jreraeors can be choughe of as pointers to elements in the containers, and indeed the STL even uses pointer notation for traversal and access co container data, Bor instance, the ++ operator moves the iterator to the next element in a container, much the way a pointer to an clement in an array can be incremented. In addition, like pointers, the actual data can be accessed by derefetencing the iterator using the * operator. Algorithms Unlike what you might expect, #lgerizhms designed to operate on STL classes do not come in the form of member functions of the container classes. Instead, they exist in the form of stand-alone functions that operate on iterators. Why did che designers of STL choose this seemingly anti-OOP design paradigm? By separating the dara from the algorithms, the designers dramatically reduced the number of combinations of specialized algorithms. Since each container has simi- lar types of iterators, cach algorithm had to be written only once instead of ance for cach container. The downside is char chere are sometimes less than obvious side effects or suboptimal solutions. In most cases, however, specialized member functions are all yeu heed to perform most basic operations on your containers, STL Concepts A few basic concepes are important to working with the STL. First, icis important w understand the methods used ra determine ranges when working with a container. Two methods common to alll containers bagin() and end(), securn the full range of 1.4 Using the STL in Game Programming 43 the comtalner. As you can sec in Figure 14.1, pegin() retums the first element in the container, but ond() returns the position beyond the Last slid! element. ‘There are several advantages to organizing the ranges in this manner. First, spe- cial-case coding for empry lists is eliminated, Second, iterating through containers has a simple ending critetia: continue as long as era(} is not zeachedl, The disadvantages of chis system are that it is somewhat less intuitive, and reverse iteration requires spe- cial members and iterators. It obviously becomes important to remember not 1 dereference an iterator that is pointing at end(). Such behavior is undefined. When you use functions specifying a range, functions in STL usually take as Parameters two irerators, ote specifying the beginning element and one specifying the end element. To pair effectively with begin() and eng(), these functions assume an inclusion of the first clement specified and exclusion of the last element specified. Mathematically, che following notation usually designates this sort of range: range [begin...end) ‘There is another aspect of the STL design of which you should be aware, STL containers pass information by value, nor by reference. This means that when dealing with small data types, it is acceptable co allow the container to make a copy of the data, With larger dara structures or classes, it becomes aclrantageous to pass in point- ers to these objects or structs. Chtherwise, every insertion or access results in a capy constructor being called. Vectors STL vectors are essentially resizable arrays. Note that although the formal Cus stan- dard docs noc specify what underlying dara structures are to be used far containers, the performance and interface requirements leave little ambiguity as to how they will be implemented in practice, Thus, all versions of STL will likely be very similar, with only minor vatiations in implementation details. Container fe ee l x L begin() end() begin() points to end() points to position first element beyond Inst element FIGURE 1.4.1, ond() points after che lasr valid clemeat. Section 1 Programming Techniques Vectors behave almost identically to standard C arrays, with one major exception: they are dynamically resizable. However, it is important vo understand the nature of this resizing. Vectors are implemented as arrays that periodically need to reallocare memory and tansfer data to a new array. ‘This means two things for developers. First, a vector can allocate more memory than it currently needs, due to the requirement char it might be expected ca grow at any time, Second, adding an element to the end of a vector is described as comsvene time—it is important to remember that this means amarticed constant time. In other wards, some grow functions can require a substantial amount of resources as they allocate new memory, copy the existing array inte thie new block, and delete the old memory, but they do not require these extra resources every time, Depending on implementation, a vector can allocare ewice its current alla- cated memory when it runs out of buffer space. It is alsa critical that you understand when a vector reallocates memory, since doing so invalidates any ieerarore currently pointing to elements in the vector, Lets examine the functions ta help more precisely manage a vector’s internal memory, after viewing the code: finglude include using nanospace sta; i) Typedef tha containar and iterator names for netter if readability typadet vector Intvector; typedet IntVector::iterator IntVectItar; void main() { /} Create a vector object of integars IntVector ¢; {/ Rosorva room for 10 integers c.reserwe( 10); {/ Fill the vector with 3 different elements c.push_back(3) ; c.push_back(S9) ; ‘9. push back{42}; {7 Now loop and print out sll the element values for(IntWectItor itor = c.begin(); itor [= c.end( cout << "elament value = " << (*iter) << endl; ++itor) {f Since the elements have been created, we can access or tf replaca them just like a normal array. a[0] = 12; elil = 32: 1.4 Using the STL in Game Programming 45 cl2] = 999; Tor(int 2 = 0; 4 < c.sized); i++) cout << “element value = * << ¢[i] << endl; } This example shows most of the basic principles you need to know to start using STL containers. Notice at the top of the listing the inclusion of the appropriate header files for this program, In addition, note the usage of namespace sta. Like all portions of the Cr» library, the ST is part of the ete namespace and so requires you to declare such in your program. Next, we see typedefs for the type of container and iterator we want to use in the program. This is a very comman practice; it noe only makes the code easier to read, bur it becomes casier to change the underlying data structure, if desited, (We'll sec how casy that is next.) The next section of code creates the vector container object v and proceeds to call a vector-only function that reserves 10 integers’ worth of memory. The code proceeds to push_back() 3 integers onto the back of the vector. Since we have preallocated well over this amount of memory, no additional memory allocation is required, There are a few routines that you might find helpful when you want to clasely monitor and control the allocation of your vector’s memory. As shown, you can reserve a buffer in the vectar by calling reserve() and passing a size parameter. This value can be revrieved by calling capacaty(). If eapacity()--reserve() and another clement is inserred inca the array, a memary allocation will rake place and all current inerarors will be invalidated, In order to determine the maximum amount of memory that can be allocated for a single vector, use the #ax_size() function, The push_front(), push_back(), pop_fract(), and pop back() Functions are common to all basic ordered containers (veczor, list, and deque)}. These functions obviously add and remove elements from the front and back of the container. Due to the implementation of a vector, you want to avoid push_trent() or pep_frant() on these types of containers, if possible, due vo the On) performance, but they are avail- able far use if you absolutely need chem. ‘The final portion of sample code demonstrates one of the most commonly used components of STL usage: the iteration loop. We use a for loop with an iterator determining the current position. The initial position is set to pegin(), and the itera- tor increments with the prefix ++ operator until the iterator equals ena). at which point che exit condition of the loop is satisfied. Every container with an accesible iterator can be looped through in this manner, Since the iterator is the only item keeping track of the current position in the vee- tor, we must use it to extract any information we want. We can sec that in keeping with the norion of a pointer, we simply dereference the iterators to access the data. After che standard iteration loop, we see an example of a vector in use like a typi- cal array, It is important to note chat array subscripring cannat be used to insert ele- ments into a list—only to access existing elements. 46 : Section 1 Programming Techniques ‘The STL fist is perhaps che most widely used of the basic STL structures, Ir is imple- menred as a doubly linked list, so any insertion and deletion of elements is done in true constant time. The tradeoff for this capability is the loss of random access that the vector and deque allow. One beauty of using STL containers is the consistent naming conventions and methods used throughout the brary. Once you lear the basics of manipulating one type of container, you csentially know how to use them all. Using a list is even simpler than using a vector. The push front() and push_back() functions work exactly as you would expect. Iterating through the list also works exactly as we saw in the vector example. In this code, we see many of the same techniques used in the vecror class: include #include using namespace std; class Fao i pubLic: Foo(int i) { miata - i; } void SetMata(int 4) { m_ivata = i; } int GetData() { return m abate; } private: int m_iData; Hy Ji Typedet the container and iterator names for better ‘1 roadability typedef List Foolist; typedef Foolist::iterator FoolistItor; void main{) t if Greate a list container of integers Foolist ¢; 7 Fill the List with 3 different elements e.push back (new Fool)); .push_back(new Foo(2)); t.push back(new Foo(d)); (/ Tteratar through the List tor(FoolistItor iter = c.begin(); iter 6: eeond(};) it((*itor)->GetData() == 2) 17 Geronstrates proper method of removing an Jf elenent trom the middle of the list. delete (*itor); 1.4 Using the STL in Game Programming a7 ater = c.erage(itor); } else ++itor; + ff Make gure to delete all the objects, since the list ff destructor will not do this automatically for yau Tor(Foolistitor itor2'- o.begin(}; itor2 I= c.end(); ++itor2) delete (*itor2); ‘We see in this example the same basic type of container manipulation, but we have added the wrinkle of using user-defined objects instead of built-in data types, This is 2 much more common usige soenario, so we cxamine haw it differs in practice from inserting data by value. STL containers do not operate on the data you pass into them. Rather, they make copies of the data they receive and distribute, In order to negate the cost of copying large data structures in memory, you'll want to pass pointers to larger, dynamically allocated objects, Narurally, our ebjects are ridiculously small for example purposes, bur they could conceivably be large enough 10 scriously affect performance if we copied a large number of thers. ‘There area few chings eo remember when working with pointers to dynamically allocated structs or objects. First, and perhaps most obviously, is thar you are respen- sible for freeing any allocated memory when you are finished with the objects. Since the container has no ides what type of data might be used, there is no way for the con- tainer to automatically deallocate memory for you. Second, and perhaps less obvious, is that many openitions appear to fail because they are operating direcrly on the object or struct pointers instead of am the objects or structs themselves, Take the list’s sort () function, for example. Te operares by usin the < operator co determine value and sort accordingly, Even if a proper eperator i designed for class Fea, the list still sorts on the actual value of the pointer, not hy the value of the data in the object. It therefore becomes necessary to design your own compare operator that derefer- ences the poincers before comparing them, See the sample cede in the article “Resource and Memory Management,” by James Boer, to sec how this comparison can easily be done. ‘The third “gotcha” is appropriate to all pointer manipulation routines but also bears mentioning in the context of STL. When copying containers, remember that only the pointers ate being copied, not che objects, If you create duplicate pointers, it could become extremely difficult to know which objects to delete. There are only cwo solutions to this problem: Use smart pointers with your objects or avoid STL routines and algorichms that enpy elements from container to container. Section 1 Programming Techniques You should also be wary of removing an element from a list while ierating through the list. Since removing an element to which you are currently pointing invalidates the iterator, you must be sure to make proper use of the erasa() function's return walue, which retrieves the next valid position in the container, By assigning this return value to the old iterator, we essentially skip ahead of the invalidated position. However, chis leaves us with another problem. Since we've already incremented the iterator to the next position, we run into rouble when the for loop tries to increment in again at che end of the loop. To solve this psoblem, we remove the increment oper« ator from the body of the tor loop and plice it conditionally inside the loop itself, incrementing only when an element is nor erased It is often preferable to use algorithms ta cras¢ elements from a containes instead of iterating through them manually. Algorithms such as remova_it() perform the same operation safely and efficiendly. Unfortunately, a complete listing and deserip- tion of che provided algorithms (and hew to czeate your own) could fill up an entire book, so | recommend the resources listed at the end of this article for further study. Deques, or double-ended queues, are designed for situations in which insezting and removing elements from either end of the container must be performed, but insexting and removing elements from the middle of the container is noc required (or doesn't hawe to occur often). Like vectors, deques can perform insertions and removals at the front and back of the container in amortized constant time, and inserting or deleting elements from the middle is somewhat slaw, Deques also allow random access, but because of the slightly more complex nature of the internal data ofa deque, which is arranged in a linked scrics of memory blocks, random access is not quite as efficient as with vectors, Unlike vectors, though, there is ne mechanism in place for determining exactly when additional memory allocations will take place. include #include using namecpace std; ff Typedef the container and iterator names for better JF readability typedef dequexint> IntDeque; typadat IntDequa::reverse iterator IntOequeRZtor; void main(} { Ji Greate a deque container of integers IntOaque cj Ji FA11 the deque c.push_front (3); c.push_front(2)3 ith 3 different olements 1.4 Using the STL in Game Programming 4g c.push_front(1) ©. push_back (3); @.push_back (2); G.push_Dack (1); / Cycle BACKNARD through the list - special iterators and i} notation is necessary to do this. for(intDequeRitor riter = c.rhegin(); rator I= e.rend(); ++ritar) cout << ‘Value = * << (*ritor) << engl; ff remove the first and last elessnts ¢.pop_frant(); G.pop_back(}; FF Accessing elements directly - if needed renenbor to Hf chack to seo the dequa d¢ not enpty. Accessing non- f/f existent elenents will lead to undefined behavior; J! probably en access violatian if(te.onpty()) t cout <¢ "Front = * << ¢.front() << endl; cout << "Back = * << ¢.back(} << endLp } ‘We see im the preceding listing che familiar code of STL usage, but with a few new twists this time. First, let's introduce the reverse iterator. You might notice that all ‘aur iterations up w this point have been in the positive direction, Although bidirec- tional iterators do exist, it often is much simpler to create a dedicated revere ftenstor and utilize it as you use the standard iteraros. ‘The reason we need a reverse ireracor is that because of the bounding conditions of a container (illustrated in Figure 1.4.1), we cant simply iterate backward and expect to be able to check for the same exit conditions (iter != begin()). This would leave the first element in a container out of che ineration loop. Instead, we uti- lize a reverse iterator combined with the rbeqin() and rena() functions. These fanc- Gons work exactly like their forward-looking cousins, bur rbegin() actually accesses the last clement, whereas rend() points to a position in front of the first valid entry. This exactly mirrors the forward versions of these functions. Because the reverse iter- ator travels backward when the increment operator is applied, you can use the exact same syntax for looping through all elements in a container. In this example, we also introduce the opposites of push_front() and push_back(), Pop_tront() and pop back(}. These functions simply remove an clement from the front or back af 3 container, respectively. Note thar the value of the element is not returned. You must use two more functions we introduce in this example to aceess the front or back elements: tront() and back(). These functions return the value of the front of back element in the container. In the example, we check co ensure that the container is not empry using the enpty{) function before uying tw access these 50 Section 1 Programming Techniques elements. Accessing elements in an empry list results in “undefined behavior," which you ean expect to probably result in some sort of acess violation; pop_rroat() and bep_back()simply are “no-ops” when performed on an empty container. Maps STIL maps are perhaps the most complex (relatively speaking) of the basic containers to use and perhaps che most versatile. Here we examine maps instead of the other tree- based scructures: sets, multisets, and multimaps. Learning the fandamentals af maps allows you to easily use the other container types, so we leave chat rescarch up to you. ‘The map is essentially a value-pairing container. ‘Two arbitrary types of data are paired as a key/walue scruccure and inserved into the container, Looking up the value via the key then can occur in o(aog n) time, Although not quite as efficicne as a hash table, the difference is often negligible and has che advantage of sorting the dara dur- ing insertion. This process allows iteration of completely sarted data, which is a ben- ficial consequence of the method of storage (a balanced binary tree, otherwise known asa red-biack rrec). #pragma warning (dieable:4786] #include #includs #include #include using namespace std; i This function abject allows us to compare map containers tenplata ale { return elen.sacond == gecand; } J] Typedef the container and iterater names tor better J) readability typedef mapcint, string> swap; typedef isMap::value_type isValType; typedef isMap::iterator iswapltor; void nain() isMlap cz 1.4 Using the STL in Game Programming 51 J) Insert key / value pairs c.insert(isvaltype(100, “One Hundred™)); c.insert(isValType(3, “Three")); c.insert(isvalType(150, "One Hundred Fitty*)); G.insert(isValTypo(89, “Ninety Nine") (display all the keys and values for(isMepItar iter = c.begin(); tar I= G.end(); +4itor) cout << ‘Key = * << (*itor).first << “, Value = * <© (vitor). second << endl; {/ You can also access thé map lake an associative array gout << ‘Key 3 displays value * << c[3].c_str() << endl; {/ Or insert like this as well c[123] = “One Hundred Twenty Three"; #} Find and remove a specific element based on the key fellaplter pos = c.find(123); if(pas i= c.end(}) /J erasing an elenent invalidates any iterators J} pointing to it. Calling pos++ now would result in J} undefined behavior. ©.0rase(pos}; iJ Find and renove an element based on the value pas = find_if(c.begin(), c.end(}, value equals ( "Ninety Nine")); if(pos I= c.end[)) c.orase(pos); ‘If you must remove elements whale iterating thraugh ff the list... for(isMapitor itr = c.begin(); itr != c.andi}; } { if(itr->second == "Three") c.erase(itre+ else eatery } } Were introduced ro a new intermediate dara type in this example, the nalue_sype, which represents the key/data pair representing every element in the container. For convenience, we've typedet’ed this type along with the other usual types. Inserting combined key/dara values uses the insert() function like any other container, with the only difference being that you must insert type map: :value_type. ‘The map sorts every entry as it is inserted, so at any given time the container is always sorted by keys. We can see this as we iterate through the map and display all the keys and their associated values. Accessing keys and data through iterators means an additional structure to navi- gate through. Dereferencing the iterator returns the value_type structure, which has Section 1 Programming Techniques cwo data members: first and second. Accessing first gives you the key value; accessing second gives you the dara value. Tn addition to access through iterators, maps also provide random aceess via their key values. The map acts like an associative (or sparse) array. Elements can be accessed of inserted using the index(} operator, Caution must be used when using this opera- tor, however. IF you artempr to access an element with an index that does not yet exist, the clement is created with a default constructor and is inserted into che map. This might not be the intended behavior and so is something to watch out for, Moving, on, we see a simple method of finding an element based on the key wsing the Tind() function. Since the keys are sorted, this function performs in O(log a} time. IF we want to find aa clement based on the value, we must doa bit more work. At best, this work will be performed in Tinear time, since the dara is sorted on the key rather than the value, The solution to this problem gives us our first look ar generic STL algorithms, We use the find_if() algorithm for this particular problem. The function requires three parameters: an iterator telling where to begin, an ireracor telling when to stop, and a function telling when the algorithm should recurn a true value. The iterators are self-explanatory, but the function object, or fiumctor, requires some further elaboration. In STL, classes with overloaded function operators (did you even know you could do thar?) are used in place af functions. This replacement enables both cncapsulated and type-safe solutions to generic programming problems. The function object pra- vided in this example simply compares the second value in a value_pair and reuums the result, Tnitializing the object with the result we want to search against provides 2 dean and completely encapsulated solution. Note that for most solutions, STL pro- vides ready-to-use function objects that you can simply plug into your code. See a comprehensive STL book for a listing of different algorithms and function objects available to use. ‘The previous paragraph describes the preferred method of searching for values in a container, but if you must iterate through and remove elements manually ina map, we also show you the proper way co do that, Removing elements while iterating through a map poses a special problem because for speed reasons, the designers of STL neglected to have the erase() function retum the value of the next valid pasi- tion, a5 other containers do. Unfurtunarcly, because of his failuse, we cannot use the simple method of removing elements, as shown in our secand code snippet. Instead, we have co resort to a bit of wickery to make sure we don't invalidate our iterator. In this example, instead of incrementing the iterator inside the for loop, we do it inside the body in a conditional manner, Notice that when an element must be eased, we post-increment the iterator when passing it 34 the parameter to erase(), bur ifan clement doesn't need to be erased, we perform the standard pre-increment epetator instead. Because of the order in which the operations occur, this method. allows safe iteration without having co resort to using temporary itcrators. Unfortu- 14 Using the STL in Game Programming z 53 nately, the necessity of this sort of coding creates far more possibility of buggy code than if the designers had just sacrificed a bit of speed in the erase{) function, With any luck, che standards committee will consider revising this function in che future to avoid these types of Kudgy workarounds. ‘This might be a good time to answer a question you might want to ask, namely, “Why do you always use the pre-increment operator in yout iteration loop?” ‘The answer is efficiency, The post-increment operatar must recurn a copy of its old value, so ix might require the use of a temporary object. The wo solutions worl the same way, but unless there's a specific reasom co use the post-increment (or post- decrement) operator, as in the previous example, you should prefer the pre-increment and pre-decrement operators. Stacks, Queues, and Priority Queues. We lump together stacks, queues, and priority queues because using them is simple enough thar they require lite additional explanation. These containers are really examples of contwiner adapters because they arc implemented as recwicted interfaces om top of existing containers Stacks The STL stack class provides three primary members—pusn(}, poet}, and top¢}—for adding and removing elements from the container, These member Functions respec tively push an element on the stack, pop it off the stack, or retricve the top clement. To check the current state of the stack, 5ize() and enpty() are provided, The stack is implemented as a deque by default, bus ic allows you to change the implementation in the constructor, ‘/ Inplenents a stack with deque ag tne underlying ff ¢ontainer type. etack > cy Note that using a vector might nor be as poor a choice ag it seems, because push(}, pep(). and tap() actually map to push_back(), pop_back(), and back(). Any container that supports these functions can be used as the underlying implemenration for the stack class, Notice that in the second linc of cade in the preceding example, we make sure to put a space between the owo greater-than operators, Otherwise, they would be incorrectly parsed as a single stream operator, >>. Ic is imporeant to also know that the stack class, like many STL containers, prefers speed to safery. Thus, the class assumes that when you call pop() oF top|), valid element actually exists. It is therefore important ro always remember to use 4 Section 1 Programming Techniques size() of expty{) to verily char a stack is not empry before performing these opera- tions on ic. Queues and priority queues worl in the same manner, so the same warn- ings apply to these containers as well, Queues ‘The quewe class works much like the stack class except thar elements are pushed onto the back and popped off the ffone. ‘The following members are defined for the queue class for element manipulation: pusht), pop(). front(), back(). back() refers to the location in which elements are inserted, and trant() refers to the locarian from which elements are removed. Like the stack class, the queue also defines size() and empty () to manage the size. As with the stack class, you can specify a conrainer other than the default deque to be used as the underlying implementation, Unlike stack, a vector used with a deque makes a poor choice due ro the bad performance when inserting elements at the frant ofa vector, However, a list might male sense in some situations. Priority Queuc The priority quewe works identically to a queue but differs in one important respect: all inserted elements are immediately sored in descending order based on a compati- son using the < (less-than) operator, Because af the sorting functionality, an additional third parameter is offered in the constructor, allowing you to ovetride the defaule « operator with your own function. This ability could come in especially handy if yau are inserting pointers to objects instcad of passing in the objects by value. Avoiding sorting the queuc based on the value of the pointers requires writing a functor elass that calls the < operator after first dereferencing the pointers. There is an example of this fanctor in the article “Resource and Memory Management,” with code sample provided. Summary a ee ‘The STL is a powerful new rool available for C++ programmers. By understanding both its strengths and its limitations, you can make the most of the features naw avail- able without compromising the speed or integrity of your code, Entire books have been written explaining hew to use the STL. It is therefore obviously impossible to think thar this article could do Justice to the broad function- ality chat exists in this library. Ifyou wait to fully utilize che power of STL, thete is no substitute fora good reference hook. Several excellent tutorials and references arc listed in the following References sectian. 14 Using the STL in ‘Game Programming 55 References [Nicolai99] Josuttis, Nicolai M., Tie Cis Skandand Library: A Tisorial and Reference; Addison Wesley Longman, Inc, 1999. [Scrouscrup97] Stroustrup, Bjarne, The Ce. Programming Language, third edition, Addison Wesley Longman, Inc., 1997. [Breymana98) Breymann, Ulrich, Designing Gampanents with the Ce+ STE, Addison Wesley Longman, Inc., 1998, A Generic Function-Binding Interface Scott Bilas Scripting engines and network messaging have an important requirement in core non: They must be able to interface with the game's functionality in a type-sale, efli- cient, and convenient way, This article provides a method far exporting functions and then binding to them dynamically ar run time. It does so without saceificing run dime speed or convenience. Requirements The hasic requirement for our scripting engine is chat we can call a function and pos sibly pass it parameters. For this task, we need to know the fumetion’s name, its loca- tion in memory, and the parameters i¢ tales, The rypes for these parameters must be types that we support directly in the scsipting engine as part of the language. Let's assuine we support bool, float, int, string, and vous. ‘The basic requirement for our network remote procedure calls (RPC) is that we can call a function on a remote machine and possibly pass ir parameters. Given chat eur machines will probably be running che code at different memory addresses, we fant pass function pointers over the network and must instead convert them into a token that both sides recognize. For this token, we use a serial [ID that can be con- verted back and forth to.an actual fanction pointer very quickly, In addition, we need to know how to recognize strings and memory pointers in the parameters so thar the data they point ro can be packed at che end of che RPC chunte for handoff to the ner- work transport. For convenience, we should be able to simply call an RPC-capable function wich- out having to do any explicit parameter packing from the caller's code. If the call is meant for another machine, the called function should auromatically send its para- meters and serial ID to che neework transport, then recur immediately, Ifmeane for local execution, ic would just directly execute the code, The dispatcher on the remate machine would look up functions based on the serial [D and then call them directly after resolving to a function pointer. 4.5 AGeneric Funetien-Binding Interface 2 ST Platform Concerns ———$—$————— ——ssssssssssssseses This isa good place co point our thar the sample code provided with this article is very specific to a particular platform: Visual C++ 6.0 running on an x86 version of Win32, In particular: 1. There's a little bit of assembly cade in here that is obviously x86 specific. 2. The name mangling and unmangling and how calling conventions work is spe- cific to Visual C++ 6.0. 3. Tse the specific way that Win32 image (DLL/EXE) exports work. Acthe very least, che concepts ifnot the implementation are still portable to other platforms. All the x86 assembly code can be converted to any other instruction set, although you need knowledge of the calling conventions of that platform for i to work, Dynamic link libraries (DLLs) are hardly unique to Win32; all chis article needs is a table that maps exporred function names to memory addresses, Finally, you should be able to figure out how other compilers (especially open source compilers such as GCC) mangle and unmangle names, Attempt #4 Ler's get back to the task at hand, We are trying to find a way to export game Func- tionality in a gencric way so that it can be called ftom scripts or passed over the net- work as RPCs. Here is a really simple solution: void Foo( void yi void Bar( void }; AT ave enum eFunction FUNSTEON_Fao, struct Funetion t typedat void (*Proc}{ void ); const chart m Nana; Proc Proc; eFunction = Funetian; ey Function g_Functions[] = { “Foo", Foo, FUNCTION_FOD, (F00, > { ‘Bar", Bar, FUNCTIONeAR, }, 58 Section 1 Programming Techniques (Powe h ‘The eFunction enumeration provides a scrialized list of unique IDs for all avail able functions. The Function structure maps a text name onto a function pointer and unique ID. Finally, che g_ Functions array is the set of all published functions in the system. Our example function exports are, of course, Foo and Bar. Our imaginary scripting engine can search through the g Functions array when it’s compiling a script to resolve function calls by name and then call the procedure directly once it is found. Hopefully, this lookup would be done through an index for speed. Our imaginary network-messaging system could conyerr function calls into their eFunetion IDs and use thosc [Ds to resolve the RPC on the other machine. Its easy and simple, ‘This solution would work fairly well but suffers from a critical drawback: all functions must be che same—they must all take no parameters and return void. We could change the Function: Proc type so thar the functions could at least feturn a value and take some parameters. However, this is not an acceptable solution, because it’s highly unlikely thar all published functions will have identical signausres. Besides that, ira very inconvenient limicarion, considering the large and vatied function sets required of modern games. One way ro work around this problem is to cast parameters back and forth from their real types to the common types required by Funetion::Prec. We could, for example, have each function pass two or three unsigned integers and pack our real parameters into them, This is a common and efficient technique used by application programming interfaces (APIs) for callbacks such as window procedures. However, it's unsafe and can't be supported very well by a general-purpose scripting language. Ir would also be impossible to figure out which of the generic parameters are pointers, a flaw char makes passing the parameters over the network for RPCs very difficult. Hicks are on the horizon. Ler’ try something else. Attempt #2 A common partial fix to the problems of Attempt #1 is to provide a package class thar sores the parameters in an internal buffer and provides add and extract methods to serialize data in and our of the object: struct Parameters t std:ivector m_Data; bool ExtractBool ( void aint Extractint ( wold Me Ve float ExtractFloat ( wold ); Const char® ExtractString( void }: 1.5 A Generic Function-Binding Intorface 59 void AcgBGOL ( beol J; void AddInt ( int }; void AdGFLogt ( float }; void AddString( const char* as void Foo( Paramaters& params } { int paramt = parans.xtractint 4); float param? - parans-ExtractFloat(}; fuse paramt, parang... } void Bar( Parameters& params }; faa enum eFunction FUNGTION_FOO, FUNCTION_EAR, Mi struct Function typedef void (*Proc)( Parametarsa }; Std:istring Mame; Proc Proc; aFunctien m_Function; hi Funetion Funetiens[] = { "Foo", Foo, FUNCTION FOO, }, { "Bar", Bar, FUNCTION_BAA, }, Tee hi Now we can pais gencric parameters ta any function—a big improvement! This method, however, has its own set of drawbacks, some of which it shares with the first attempt. First, chic solution is inkerently nontypesafe and dangerous because of its add/extract functions, The C++ compiler cannot check the types at compile time because it docsn't know whar's supposed to yo into 2 Paraneters object; by its very definition, it can hold anything. The best we can do is provide some basic run-time checking by staring a type each time an Add method is called and chen checking those types from the called function cach time an Extract method is called. This isn’t very efficient and can be error-prone. Furthermore, any time the funeon parameters change, every call co that function must be searched for and updated to match. The compiler cane detect changes like this, and the manual scarch-and-replace function is 60 Section 1 Programming Techniques: another error-prone process. Missing one changed call by accident could introduce Jatent and difficult-to-find bugs. ‘Calling functions in this way is also tedious and imefficient. The add/exeract process adds a lot of memory copying and verification averhead. Ir also has serious engineering time overhead, A simple function can no longer be added to an export list; it must now change its function signature and have a prologue dat converts a Parameters object into local variables. Likewise, callers must construct the Parana- ters object co begin with, although this requirement can be made a litle easier through some clever template work. Still, there must be a better way. Half of the Solution Let's start at the end and work back ro the beginning for the solution, What we're really looking for here is a function specification table that gives us everything we need to know about how to call a particular function in a campletely generic way. We need to be able to set up the stack with a chunk of memory (Le,, push che parame- ters), jump directly to the function for the call, and then retrieve the return value to pass back to che original caller. For this task, we need to know the function's name, location in memory, return type, parameter types, and calling convention: f7 function specification struct Function { Fy simple variable spec enum eVarType VAR_VOID, VAR_BOOL, VAR_INT, VAR_FLOAT, VAR_STRING, i if possible calling conventions enum eCallType CALL_CDECL, CALL_FASTCALL, GALL_STOGALL, GALL_THISGALL, is typedef stds:vector Paranvecy etditeteing m_Name; void* m_Prac; unsigned int m SerialID; evarType n_ReturnType; Paraniec n ParanTypes; eCalltype —n_Call Type; i typedef std::vector FunctionVed; JJ the global set of specifications for exported functions Functianvec g Functions; 61 Assume for the moment that we havea way ¢o fill g Functions with specifications forall our exported functions (I'll explain how to do thar a little later). Now, how can We use this information co actually call functions? First we must know how our place form's various calling conventions work, Calling Conventions You can check your compiler’: documestation to see how its calling conventions work. On Visual C++ for x86 Win32, all function calls have certain chings in com- mon: The stack grows downward, and all parameters are pushed from right co left. In effect, parameters go from left to right on the stack for increasing memory addresses. . The stack pointer (esp) always points to the lowest memory address of the stack, which unfortunarely has the name of “top.” It must be dword (4-byte) aligned, so cach parameter pushed must be likewise aligned ta adword, The push instruction decrements esp first, chen stares the data. The pop instruction loads data first. then increments esp. Parameters passed by value are pushed on the stack in their entirety: Doubles (&- byte) and user-defined types are just copied onto the stack. The memory addresses contained by references and pointers are directly pushed onto the stack. Simple non-float reuurn values such as integers and pointers are stored in the eax register. Eight-byte scructures are rerurned in od and eax as a pait, Floats and doubles are returned chrough the FPU in sto. Return values for user-defined types have their addresses pushed onto the stack last, but they will also be returned in eax, Here are the two calling conventions that we'll be supporting: —eoee2. The caller cleans wip the stack, meaning char ic is responsible for pop- Ping its own arguments off the stack after the call completes. This convention is required for variable argument functions because che called function doesn't nec- essatily have the information it needs to pop the cotrect number of arguments This is the defaulr calling convention for static and global functions in C and Cre. —Stdeall. The callled function deans up the stack. This is the standard conven- tion used for Win32 AFI calls, probably because it is more efficient in terms of client code size. Support for the other three calling conventions (_tasteall and the nwo this. cali variants) is beyond the scope of this article, bur it could be worth looking into and supporting, depending on the application. 62 Section 1 Programming Techniques Now we have enough information to do generic function calls with these owe conventions. We also need a function to retrieve a floating-point value from the FPUs S70 register (as is convention) ro be stored in a generic return valuc. Here are some functions that do the dirry work: DWORD Call_cdecl( const void* args, size_t sz, OWORD tunc ) { OWOAD re; ff bere’s our return value... _sam { mov ecx, sz ff gat size of buffer mov esi, args H get buffer sub esp, ecx f/ allocate stack space mov edi, cap ‘/ start of destination stack frane shr ecx, 2 Ff make 1t dworae rep moved ‘) copy params to real stack call [func] ff call the tunetion mov oft, eax ff save the return value add esp, sz {/ restere the stack pointer } return ( ro )+ 1 QWORD Call_stdeall( const vold* args, size_t sz, DWOAD func ) t DWOAD re; / here's aur return value... ase { mov ecx, sz {/ get size of buffer Rav 51, args if get buffer sub psp, cx ff allocate stack space Rov odi, esp f/ start of destination stack frane she ocx, 2 7 make it dwords rep moved yf copy it call [func] fy} call the function mov Pc, @ax {/ save the return value } return ( re Jj } Geclspec ( naked } IWORD GetSTO( void } WORD 1; ff temp var 38m t TStp Gword ptr [T] if pop STO into t mov eax, dword ptr [f] / copy into eax ret if dane 1 Now, given a function's address and some parameters stored in a memory buffer, or cea Sst fincrsnniimien akon completa onesie ae 1.5 A Generic Function-Binding Interface é 63 Calling the Function Before making the actual call, our client subsystem (scripting engine, network RPCs, ete) needs to do a little preliminary work. First it looks up the instance of the Func- ‘tion structure within g Funetions that corresponds to the function it will be calling. For the scripting engine, we want to verify that the function's specification matches up with what we're expecting: Check and convert any parameters i necessary, or give ain error if its a mismatch. This procedure could be expensive and should be done during the script compilation phase, and not in real time. Looking up the Function instance for nctwork RPCs is. litle more complicated, A good way to-set this up is to intercept the call from within the function thar is ddes- sined to be called over the network. Look in g Functions for the Function instance with the highest s_proo value thar is less than the current instruction pointer (eip) to Figure out which function is currendy being called. Hece is an example: —teclsper ( naked } OWOAD GetEIP( void } t a t mov eax, deord ptr [oop] ret I ‘! sample APS‘able function void NetFao{ bool sand, ant i } { ff FindFunction() snawld look in g Functions for highest ‘m_proct (f Jess than ‘ip* and return it static const Funetion* sFunction ~ FindFunctian( GetEIP() ); df ( send ) { J RowteFunction() shauld pack up the parameters and send the J] request aver the netwerk, RouteFunction{ sFunction, (BYTE*)asand + 4 ); return; 1 4/ ++. normal execution of Neteoo printt( “d= Sdin", i); } The next step is to construct the parameter buffer to pass to che function. Fora scripting engine based on a-virtual machine, chis is easy; all our parameters are already on a dword-aligned virrual stack. We can simply take the address of the start of the parameters and piss it along. For network RECs, it will be a little more difficult. We cantt pass pointers generically over che network, bur we can make a special case fot strings, so analyze the m ParariTypes for VAR_STRING types and append the contents of the string to the ond of the buffer thar gets sent to the network transport. On the 64 _Seetion 1 Programming Techniques receiving end, resolve the pointers to point ¢o che appended data, and then use the start of the chunk as the beginning of the parameter buffer. Now thar we have che Function instance and our parameter buffer, we call either tall_edecl() or Call_stdeall{), depending on n_calltype, passing in the paramecer buffer and m_Proc. Then we can cither use the recur value or call Getsto() te get ic if m_Returntype is a float or double. That's all there is co calling a function generically! Completing the Solution Until now we've been assuming that the g Functions array has already been set up. Let's go back and fill in this hole now. There are several ways to fill out the g_Fune- tione array. Perhaps the easiest ro implement but least safe to use is to apply macros or a finction to set it up: float Foo( int, eenst chart ); int Bar woid 5 void SatupFunctionExports{ veid } t { Function funstien; function .m_Nane = "Foo"; function .m_Prac > Foa; function.m Serialid = g Functions.siza(); function.a_ReturnType = Function .eVerType:iVAR_FLOM function.m_Parastypes . push_beck( Function.eVarType::VAa_INT ); function. ParasTypes . push _beck{ Function, VarType::VAa_STRING }; Tunction. m_CallType Function. eGall Type: :GALL_CDECL; g_Functions.push_back{ function ); } fi Function Tunetion; funetion.m Mane = "Bar"; funetion.m Proc = Bar; function.m SaralID = g Functions.size(); funetion.a ReturnType = Function.evarType: :VAR_INT; function. CallType = Function.eGalLType::CALL_GOEOL} Functions. push_back{ function }; } ‘This example is illustrative but not exactly optimal. It could be improved with some helper functions and macros co make it easier w add new functions to the table. However, i¢ will always be unsaf= and inconvenient. Adding anew function to the table 1.4 A Generic Function-Binding Interface means thar someone has to write some code that specifies its types, name, and calling convention, Changing a function fadding 2 parameter, for example) without updating the table could introduce some nasty and hard-ro-debug problems. It isa lot of work to keep the function specifications in syne with the actual function prototypes. ‘We need away to build chis table automatically and safely eliminate these prob- lems. Fortunately, the C++ compiler already has alll the information we need. While parsing the function's protocype, the compiler builds an internal representation of the function—its return type, parameters, calling convention, and so on—exacrly what is required to construct a function specification! ‘Unfortunately, we don't have access to this information from within the code, and besides, all char information pete ehrowrn away when the linker constructs the final EXE, We could probably find a way to use the PDB (debug symbols darabase) to query for what we need, bur we can't ship debug symbols with the game. Besides, we wouldn't have an easy way to tell which functions are for export and which aren't. Combining the export ble functionaliry of a Win32. image file with the C++ language’: name-mangling facility gives us the information we require. [we tag a function for export using the _daclspee( @llexport ) keywords, thar function’: name and addzess will appear in the EXE (or DLL) exporr rable. In addition, because this is a C++ application, those names will be mangled to support type safety and overloaded tame resolution. Mangled names are encoded with all che information we require, s0 all we need is to decode che names into a form we can understand and then use that to build the Funetion entry to add te g_ Functions. The rame-mangling format is completely implementation specific and undocu- mented, and it even changes from release w release of Visual Css, so attempting to reversc-engineer it is probably nat a good idea, It’s also unnecessary: Microsoft exported a name-unmangling fiction called UnDecarateymboiane() from both TmageHlp.dll and DbgHelp.dll that docs exactly this. So if we were co take our Foo() function from the last sample and DLL-cxport it, che entry 7oo@@VAMMPEDEZ would appear in the EXE's expore table. If we unmangle the name, here's whar we gct back: float _cedecl Foofint,ehar const *). Now chis is something we can easily parse and convert to a Function entry for addition to our g_Funetions rable. So now our procedure for building g Functions is: 1, Tkerate over all entries in the EXE’s export table, and retrieve each function's address and mangled name. 2, Unmangle each name ro get 2 function prototype in text form. 4. Parse the fisnetion procorype to retrieve name, type. and calling convention infor: mation. 4. Store the results in a new entry within g Functions. Repeat for cach ‘export. Ierating over the exports to get the function addresses and Mangled names requires knowledge of the binary format of Win32 Portable Executable (PE) format files. A specification for this formas is available from the Microsoft Developer Net- 66 Section 1 Programming Techniques: work Library (huip://msdn.microsoft.com), Search for the “data” section within the library entey for the Microsoft Portable Executable and Common Object File Format Specification to find the structure of a Win32 export table. ‘There's one final little derail. The entries in the export table point ro a jump rable, which in tums points to the actual functions, This detail isn't imporsant if all you're interested in is binding to functions and calling them generically. However, if you need to be able to do a reverse lookup and convert eip from within che called function to find irs Function instance (required for RPCs, as described extlier), you need to get the actual address of the function for comparison, not the address of the entry in the jump table. This is casy cnough: Dereference the address given by she DLL cxport entry to find the jump table entry. The first byte will be 0x9 (jmp), followed by a 4- byte offer to the actual entry point of your function. ‘Take che address given by the DLL export entry, add 5 for che full jmp instruction, add the 4-byte offset, and this will be the address of the entry point of your function. This address can then be used for reverse lookup to find the Function instance from within g_Functians. Conclusion We now have everything we need to call functions in a completely generic way. In order ro publish a function in the system and allow other subsystems such as scripe- ing and network RPCs ro bind ca it, we simply tag it with _declspec( dliexport } (this verbose rag is best wrapped in a macra to reduce clutter). At run time, the function-binding publisher iterates aver the Win32 export table and extracts name, type, and calling convention information from cach entry, Other subsystems can look up functions by memory address, name, or serial 1D and call them generically using Call_edecl() or Cal1_stdeall(). This scems like quite a bic moze work a0 implement than necessary, and for smaller projects with small export sets, it probably is. Larger projects, on the other hand, will probably be changing constantly, The good news is that, once the basic work is done, adding new functions co the system is as simple as tagging them for export, and they'll immediately be available. This process more than pays for itself and is a powerful ability to give any engineer on your team. When combined with a general-purpose scripting engine, the process can be turned into a useful debugging tool as well as serving the concent-specific needs for which it was originally written. In the interests of space and simplicity, we have left out many of chis arricle’s fea- tures, The generic function-binding concept can he taken much further ina variety of ways. It can easily be enhanced to include support for pointers and references, variable argument functions, and passing more than just strings over a network. User-defined types could be supported for RPC packaging through a serialization interface that can be detected and called directly when post-processing RPC parameter buffers for aut- bound network buffers. In addition, support for calling class member functions is 2 very useful tool and can be easily added, Finally, one fearure chat might or might not 1.5 A Generic Function-Binding Interface = 67 be necessiry isa tool chat will post-process an EXE, stripping off the exports table and converting it into a native data format for direct impor into 9_Functiana. This tool could be necessary either for sccuriry reasons (co prevent cheating, perhaps) or to make it unnecessary to ship DbgHe1p.d12 with the game, References ——————————— Microsoft Developer Network Library, htep:/'msdn.microsoft.com. 1.6 A Generic Handle-Based Resource Manager Scott Bilas All computer applications are databases, They spend most of their time juggling data resources—creating, destroying, caching, modifying, querying, saving, and restoring objects of various types. Games typically contain multiple types of databases, each of which is generally hard-coded for cach different case, to keep things speedy. Some cxamples of game databases are file systems, texture managers, font managers, and game actor managers, On top of those, there is a wide variety of domain-specific dara- bases thar completely depend on the game’s genre and content, A resource database that's built inco all C++ games is the basic abject memory man- ager. A programmes calls new co construct a new abject and passes its pointer around so that other objects can pass it messages. When the object is no longer needed, some- body deletes it, and its resources are retumed to the system, This method works very well in general, bur it breaks down when we have to worry about shared resources. This is where we aced 2 more specialized database. Lets use a font abject for our example. At minimum, the font consist of a bit- map and a set of specifications, such as che X, ¥'(or U,V) locations of its character cells, so the graphics system can render it to the screen. Such an object is fairly heavy duty in terms of memory usage and creation time. Different systems in the game, such as the development console and a text cantrol within the GUI, want to use font objects, but we can't have each system creating its awn local copy of the font object, Obvi- ously, that would be slow and consume a lot of memory: To selve this problem, we need te eome up with a way co share font objects, Our solution is called che Footage and features methods that get painters to fonts, loading them on the fly and caching them until they are no longer needed. ‘The Fontttgr is made available from a global location (possibly as a singleton; see the article “An Automatic Singleton Utility”) and is responsible for all che font objects in the system. What we'e really talking abour here is a specialized database. The Fontugr is responsible for juggling font resources and, now that it’s considered an API, suddenly takes on additional responsibilities as the central dearinghouse for fonts, What if someone tells the FontMgr co delete a font to free up resources, bur some systems in 1.6 A Generic Handle-Based Resource Manager 69 the game still have pointers to i? How do we guarantee safety of the system without sacrificing performance? Will we be copy-pasting this code again (with slight eweaks) when it comes time to build the MousePointertigr? This article presents simple, safe, feneric, and efficient way to manage controlled resource objects. The Method $e The job of a resource manager is to create resources on demand, hand them out to anyone who asks, and then evenurally delete them. Handing our these resources as simple pointers is certainly easy and convenient, bur it’s not a very safe way to do it. Pointers can “dangle”; one part of the system can tell the resource manager to delete a resource, which then immediately invalidares all other outstanding pointers. Thete’s no good way to prevent the dangling pointer problem from happening, and che only way we would find our that someone was atrempeing to dereference a deleted abject is when the game crashes. The problem is that, with pointers, there's no way to know how many references are outstanding, given that clients can copy the pointers as many times as they like without telling the manager about it. Another problem is that che underlying data organization can't change with pointers. Any reallocation of buffers immediately invalidarcs all outstanding pointers, This hecomes especially important when you are saving the game to disk. Pointers can't be saved to disk, because the next time che game is loaded, system memory will probably be configured differently or you could even be on a completely different machine. The pointers must be converted into a form that can be restored, which will probably be an offset or a unique identifier of some sore, Working around this prob- lem isn't exacdy trivial and can require a loc of work ro support in elient code. So it plainly not a good idea for a safe and flexible resource manager to be hand- ing our pointers. Rather than using pointers or attempting to write some kind of super-intelligent, overly complicated “smart pointer,” we can add one layer of abstrace tion and use handles instead, putting the burden of the manager class. Handles are an ancient programming concept that APIs have been using with great success for decades, An example of a handle is the HANDLE type returned by the GreateFile¢ } call in Win325 file system. A file handle, representing an open file system object, is cre ated through the GreateF£ie() call, passed ca other functions such as ReadFade() and SetFilePointer{) for manipulation, and chen finally closed off with Cleseliandie(), Attempting co call those functions with am invalid or closed handle docs not cause a crash; instead, ir returns an ezzor code, This method is efficient, safe, and easy to understand. Handles almost always fit iato a single CPU register for efficient storage in col- lections and passing as paramctess to functions, They can be easily checked for valid- ity and provide a level of indirection that allows the underlying data organization to change without invalidating any outstanding handles. This has significant advantages over passing around pointers. Handles can also be easily saved to disk, because rhe Section 1 Programming Techniques data structures they refer to can be reconstructed in the same onder an a game restore, This facility allows che handles to be stored direculy, with no conversions necessary, because they are already natively in unique identifies form. The Handle Class A fast and safe way to sepresent handles is to use an unsigned integer composed of two bitfield components (this class appears in Listing 1.6.1), The first component (¢ Index) is a unique identifier for fase dereferencing into che handle manager's data- hase, The handle manager can use this number however it likes, burt perhaps the most efficient use is as a simple index into an std::wector. The second component (= Magic) is a “magic number" thar can be used to validate the handle. Upon derefer- encing, the handle manager can check to make sure that the magic number compo- nent of the handle matches up with its corresponding entry in the database. ‘The Handie class is very simple and really doesn't do much except manage the magic number. Upon calling Init(), the handle is given the next magic number, which automatically increments and wraps around, if necessary. Note chat the magic sumber is not intended to be a GUID. les purpose is to serve as a very simple and fast validity check, and it relics om the high improbability of a condition arising where one object happens to have the same index and magic number (via wrapping) as another. ‘The magic number of zero is reserved for the “null handle" where che handle’s dara is zero, The default Handle constructor secs itselfto null, a stare that recurns true on an Istull() query. This is convenienr to use for an error condition; a function that cre- ates an object and returns a handle ro it can simply recurn a null handle to indicare shat an error occurred. In most ways, the Handie class acts as a read-only unsigned inceger Irs not intended to be modified after being created, although it can safely be assigned back to null to reset it. Notice that Handie is a parameterized class, taking a TAG type w fully define iz. The template parameter Ta doesnt do anything excepe differentiate among types of handles; an object of type TAG is never wed anywhere in the system. The motivation here is type salery. With Handle not parameterized, a handle meant for ‘one type of resource could be passed ca a function expecting a handle to a different type of resource, wichouc 3, complaint from the compiler. So to keep things safe, we qeate a new handle cype, taking any unique symbol and using it for the parameter. The TAS typecan really be anything so long.as itis unique across Handle types, but it’s convenient to define an empry struct and use that in the typedef for a handle, like this texture handle example: struct tagTexture { 3; typedef Handle HTexture; Now we need a handle manager that is responsible for acquiring, dereferencing, and releasing objects (via handles} for a higher-level owner. 1.6 A Generic Handie-Based Ressurce Manager a mW The Han Mgr Class ‘The Handlewge eles is a paramecerized type composed of three main elements: a data Store, 2 magic number store, and a free list (chis class appears in Listing 1.6.2). The dara store is simply a vector (or any other randomly accessible collection) of objects of type BATA. The DATA type, the first type paramerer for Handlewgr, should be a very simple class that contains context information about the resource that it controls. For cxample, in a Hanglewgr thar manages files, ehe OATA type would probably have only the file handle and che name of the file: struct Filegntry { std::string m FileNane; HANDLE nFileHandle; // O file handle I struct tagFile { }3 typedef Handle File; typedef Handlemgr that dereferences itself into a TAG by directly accessing the singleton that manages it, Save-game functionality should be fairly casy ws add, but it is necessarily specific to your game's architecture. The handles can be sived out disecrly; just make sure thar the Handiengr stores the indexes for its objects along with the object data, and on restore, all handles will remain valid. Listing 1.6.1 i nt a ON BRAT —- we PPR Sincluda template :: Init( unsigned int index ) 1 assert( TsNull{) }; Hf don't allow reassignment ageert( index <= MAX_INDEX ); J) verify range Static unsigned int ¢_AutoWagic ~ if ( ++5_AutoMagic > MAX_MASIC } 1 s_AutoWagic = 1; // 0 is used for “null handle" } m_Index = index; mMapic = s_Autollagic; } template 1.6 A Generic Handio-Based Resource Manager 76 inline bool oparator t= ( Handle 1, Handle r ] { return ( L.GetHandie() != r,GetHandle() ); 3 template inline bool operator == ( Handle 1, Handle r } { return ( L.GetHandle() == r.GetHandie() ); 3 Listing 1.6.2 a #include #include template class Handlougr i private: i private types typedef sto: :vector Uservecs typedet stdi:vector z: Acquire( HANDLES handle ) i J] if free list 1 empty, add a new one otherwise use first one found 76 Section 1 Programming Techniquas unsigned int index; if ( m_FreeSlots.empty() } t index = a Magichumbers.size(); handle. Inst( index }; n_UserData.push_back{ DATA() ); A _MagicNunbors.push_beck( handle.Getiegic() }; t else t index = =_FreeSlots.backi); handle. Init{ index m_FreoSlets.pop _back(); m_Magictumbers[ index ] = handle .Getmagic(); } return ( m_UserOata.beging) + index J; 1} Tenplate inline OATA* HandleMgr :: Dereference( HANDLE handle } { if ( handle.TsWull() ) return (03; Hf check handle validity - $ this cheek can be removed Tor speed Hf 4f you cen assume all handle references aro always valid. unsigned int index = handle.GetIndax(); if { ( index >= = _Userfata.aize() ) || ( m_Magichombers[ index ] I= nandle.Getitagic() } } ff mo good! invalid handla == client programning error assert( 0 ); return (0 ); ie roturn ( m_UserDeta.begin() + index }; } template ctypename DATA, typename HANDLE> 1.8 A Generic Handle-Based Resource Manager 77 inline const DATA* Handleligr rt Dereterence{ HANDLE handle ) const { (f this lazy cast is ok - non-const version does not modify anything typedet HandleWigr ThisType; return ( const_east ( this )--Dereference{ handle ) }; } Listing 1.6.3 ae cn en dese eI #include #include #include Hf... [ platform-specitic surface handle type nere | typedef LPDISECTORANSUAFACE? Ostandle; Struct taglexture { }; typedef Handle HTexture; class Textureigr t {f Texture object data and db. struct Texture { typecet std::veetor HendleVec; sta: ring m_Mame; f! for recenstruction unsigned int mwidth; J} mip 0 width unsigned int m Height; i) mip 1 width HandleVec m_Handles; Jf handles to mip surfaces OsHandle GetOstandle( unsigned int mip ) const { assert( mip HTexturemgr; 1/ Index by name inte do. #/ Gase-insensitive string comparison predicate struct istring less { bool operator () ( const stdz:atringk 1, const stdi:string& r ) const { return ( iistricmp( L.c_str(), r.c_str(} } <0); } Section 1 Programming Techniques hi typedef std:cnap NaneIndax; iypedef std::pair NanoIndexInserthie; ff Private data. HTexturetigr m_Textures; ManeIndex n_NameIndex; public: if Lifetdne. TextureWigr( void } { J* —Textureugr( void }; J/ Texture management. Hlexture GetTexture [ const char* namp ); void = DeleteTexture[ HTexture htex }; 4] Texture query. const stdiistrings Getlane( HTexture ntex ) const { return { mTextures.Oereferenca( ntex )->m_Nane }; } ant GetWidth( HTexture htex } const { return { mTextures.Oereference( ntex )->m Width }; } Ant GetHeight( HTexture htex } const {return { mTextures.Oereference{ ntex )->m Height }; OsHandle GetTexture( MTexture htex, unsigned int nig = 0 ) const { return { mTextures.Ocreterenca{ ntex }->GetOsHandle( mip ) }; } A Textureligr i: =Textureligr( void } i J/ release all our remaining textures before we go Namalndex::iterater i, begin = mNamelndex.begin(}, end = m_Nanelndex.endé); Tor ( i= begin j i I= end j 444) t n_Textures.Dereference( i->second )->unload(}; r } Hiaxture Texturelige :: GetTexture( const char* name } 1 ff ingert/ting NameIndexInserthe ro = M_Namalngex.ansort( std:imake_pair( mane, HTexture() ) ); af ( Pe.second ) ff this is a new dnsartion Texture* tox = n_Textures.Acquire( re.firat--second ); 1.6 A Generic Handie-Based Resource Manager 79 if ( Itex->Load( rc.first->first } ] { OeleteTexture( rco.tirst-=secend ); Fo.firat-seecead = HTexture(); I + return ( re.tirst->second ); + void TextureMgr :: DeleteTexture( HTexture htex } Texture* tex = m_Texturas.Dereference( htex ); if ( tex 1= 0) { ff delete from index m_MameIndex.erase{ m_Natelndex.tind{ tex->n_Name } }; {F delete from db tex->Unload (}s n_Textures-Release ntex ); } bool Texturevigr::Texture =: Losd( const star:string& name } { n_Name - nane; if... [ load texture fram filo system, return false on failure | return ( truc #* or false cn error */ ); void Texturelgr::Textura :: Unload( void } { m_Name.erase(); W ( free up nip surfaces | n_Handles.clear(); References ee [Bilas00] Bilas, Scott, GDC 2000 Talk, Jes Stil Loading? Designing am Efficiens File Setem, available online at www.aa.net! -scomb/gdc/. Meyers, Scott, Mare Effeerive C++, Addison-Wesley Longman, Ine, . 1995. 1.7 Resource and Memory Management James Boer Computer and video games, more than any other type of software, often require han- dling vast amounts of media resources such as graphics, sound effects, music, video models, animation, and other types of memory-hogeing data. Dealing with this large amount of data while maintaining a relatively reasonable memory footprint is not a trivial cask, In this article we examine the workings of a simple resource manager and discuss how it might be both used and extended in real-world applications. First, ler's clearly define our problem and how we expect to solve it. Within a fiven time in which it is not acceptable ro display a loading screen or break the action, we expect to use more data in our game than we can hold in memory at one time. It isalso assumed thac we havea medium from which we can dynamically load our dara while the game is playing, On console systems, this would most likely be a CD or DVD type of device, whereas on the PC iris probably the hard drive. ‘Our solution entails creating resource objects that are able ca automatically load, discard, and reload their data based on usage patterns. We will also create a manager to coordinate the available resources and control access to the resource objects. This will be accomplished through the use of handles, which are essentially just unique identification numbers. The Resource Class To begin with, let's examine the base resource class: class BaseResource { public: enun PriorityType { RES LOW FRIORITY = 0, RES MEQ PRIORITY, RES _HIGH PRIORITY i 1.7. Resource and Memory Managoment at Baseflesource() { Clear(p; } virtual -HageResource(} { Destroy(); + #/ Clears the class gata wirtual void Clear(); JJ Greate and destroy functions. Note that the Create() 4/ Tunetion of the derived class does not have to exactly Ji mateh the base class. No assumptions ara nade regarding / paranaters. virtual bao] Createt) { return false; } virtual void Destroy() i} {/ Dispese ang recreate must be able to discard and then {} completely racreate the data contained in the class with {} no additional parameters virtual bool Aecreate() = 0; wirtual void Dispose() = i GatSize() must return the size of the data inside the 7 class, and Ispisposed(} lets the manager know if the ff data exists. virtual size_t Getsize() - virtual bag] IsOisposed(} I These functions set the parametara By which tha sorting Ff operator determines in what order resources are M discarded inline void SetPriority(PriorityType priarity) { @ Priority = priority; } indine PriorityType GetPriorityd) { return mFriority; } inline void SetReferancecount(UINT ncount) {. @ARefCount = nCount; } inline UINT GetReferencecount() { feturn mnRefCount; } inline bool IsLecked(} { feturn (mnaefCount > 0) 7 true : false; } inline void SetLastaccess(time_t LastAccess) { s_LastAccess = LastAccess; } inling time_t GatLastAccess() { return mlasthccess; } i] The less-than operator defines haw resources get J] sorted tor discarding. virtual bool operator < (Basefesources container) ; protected: PriorityType m_ Priority; INT n_nReféount ; time t n_Lasthecess; Section 1 Programming Techniques ‘The GaseResource class acts as a template from which other resource container elasses must be derived. Several member functions must be overridden by any base class and are critical vo how the system works, Ie is expected thar che initial creata{) function will load some amount of resource data from disk or even from another location in memory, It is critical for the class to retain the necessary data in order to repeat this operation as many times as accessary in the Recreate() function. This may mean, for example, storing the path and file information of a bitmap to be loaded. The application must override the Dis- pese() and Recreate() functions in order to allow the resource manager co swap the resource in and out of memory as it sees fit. Keep in mind that only the mose signifi- cant portion of the resource (e.g., the bitmap data, the sound buffer, and the like} , not adfthe class dara, must be swapped out. Getsize() and IsDispesed() arc two more functions that must be overridden properly for the system to work, GetSize() is fairly intuitive. The function should recucn the size of the dara that can currently be swapped out, If the data has already been swapped out, the function should return a size of zeta. Technically, you could calculate the actual size of the abject by including all the other data members, but in all practicality, this mcthad is really not worth the effort. Is0isposeq() must return true if the dara has been discarded and false if it ha not, The class makes no aomp- tions about how you can determine this stare. It is up eo the derived class to provide any necessary dara members to keep crack of this state, if needed. Often simply check- ing to see if pointer is null works instead of adding a dara membes. ‘A oumber of other data access functions provide access to the data members n_rriority, m_nfefCount, and m_LastAccess. The first, = Priority, is an enumera- tion defining the general priority of a resource (high, medium, low). High-priority items tend to stay in memory longer, and low-priority items should be swapped out first. The function m_nfettount indicates the sumber of times the resource has been locked. We examine this fianction a bit later. The m_LastAccess function is the time at which the resource was last accessed. The less-than operator ( < ) is what determines the priority of sorting resources for discarding. The default function looks like this: beol BaseResource::operator < (BaseResourceh container) { if(GotPriority[] < container.GetPriority(}) return true; else if(GotPriority() > container.GetPriority(}) return false; else if(m_LastAccess < container.GetLastAccess()) Foturn true; else it(m_Lastaccess > containgr.GetLasthecess(}) return false; else { 1.7 Resource and Memory Management a3 if (GetSize() < container.cetSize(}) return true; alse return false; } } return false; You can sce from this function that resources are sorred first by priority: then by access time, and List, by size, Although a rather primitive algorithm, it works surprise ingly well for many sitwations. If you require a different or more sophisticated algo- tithm, you can cither modify the base code or supply a new sorting operator in the derived class. The Resource Manager ‘The other half of the managed resource problem is supplying a manager thar can organize all the stored resources, provide access on demand, and handle the dynamic disposal and reallocation of resources to stay within a memory budger. Let's examine the Restanager class to sce how ir works: clase ResWanager { public: FResWanager |) { Clear(); } virtual —Resilanager(} { Destroy(); 3 void Clear(); bool Create(UINT nilaxSize); void Destroy(); fps ‘/ Resource map iteration (Access functions for cycling through each ites. Giving ff direct access to the map or iterater causes a atack fi pointer fault if you access the map across a dll ‘/ boundary, but it's safe through the wrappers. inline void GotoBegin() { nm currantResource Resourcellap.begin(); } inline Basehesource* GetCurrenthesource() { return (*mCurrenthosource).seeond; } inline bool Gotolext() { "_CurrentResource++; roturn IsValid(}; } inline bool Isvalid() { return (m_CurrentRssource != m_Aesourcelap.end{)) ? true : false; Section 1 Pregramming Techniques ies // General resource atcess // Allows the resource manager to pre-reserve an amount of {/ memory so an inserted resource does not exceed the / naxinum allowed nenery bool Reservelflemory(size t nilem); 4 Tf you pass in the address of a resource handle, the {J Resource Manager will provide a unique handle far you. bool Inserthesource(RHANDLE* rhuniqueld, aseResource™ phesource); bool Inserthesource(RHANOLE rhUniquelo, BasaResources pAesource) ; ‘/ Removes gn abject econpletoly tron the manager. bool AesoveRtesaurce(BaseResource’ phesaurce) ; bool AemoveResaurce(RHANDLE rhUniquelD); i Destroys an object and deallocates it's mamary bool DestroyResource(BaseRescurce* phesource}; bool DestroyResouree(PHANDLE rhUniquerD) ; ff Using GetReeource tells tha manager that you are about (f to access the object. If the resource has been ff disposed, it will be recreates before it nas boon if returaed. BaseResource* GetResource(RHAMDLE rhuniquei); ff Locking the resource angures that the resource does not Ff get managed by the Resource Manager. You can use this ff to ensure that a surface does not get swapped gut, for 'f instance. The resource contains a reference count Hf te ensure that numerous locks can be safely mado. BaseResource* Lock(RHANOLE rhUniqualD) ; H Unlocking the object lets the resource manager know HM that you no longer need exclusive accese. When all J} lacks Nave been released (the reference count is @), the M object is considered safe for management again and can J} Da Bwapped gut at the manager's discretion, The abject J! can be referenced either by handle ar by the object's Jf pointer int Undock (RHANDLE rhUniquelD) ; int Undeck(BaseResource* pResource| {} Retrieve the stored handle based on a pointer to the tf resource. Note that (f resource, Note that it's assumed that there are no (f duplicate pointers, as it will return the first match ff Tound. AHANDLE FindResourceHandla(BaseResaurce pResourca): 1.7_ Resource and Memory Management as protected: {/ Internal functions inline void AidMemary(UENT nllem) { m_nCurrentUsedilenory 4= ndtom; andino void Removellanory(UINT nlten) { mncurrentUsedwenory «= rMem; } UINT GotNextResHandle() 4 raturn -m_rhNextResHandle; } i) This sust be called when you wish tne manager to chook ‘/ for digcardable resources. Resources will enly be i} swapped out if the maxinum allowable limit has been i) paaehed, and it will discard then fram lowest i} to highest priority, determined by the resource class‘s if < operater. Function will fail if requested nenory i# cannot be freed. bool CheekFordverallocation(); protected: RHANDLE = m_chNextResHandle; vINT m_nturrentUsedMenory; INT n_nilaxinueMemory ; Recwapiter m_CurrentRasource; Aestisp m_Aesourceltsp; H The heart of the resource manager is the dara member m_Resourcewap. This ic an STL map, which means thar every unique tcsource handle (which is simply an unsigned int) is paired with a pointer co a resource object, Handles can either be pre-assigned (perhaps hard-coded or read from script files} or dynamically assigned by the resource manager itself. Keep in mind that the current implementation is very primitive. It simply stares at the maximum value for handles and works down, Mixing thes: po methods works well if your user-defined ID val- ues start relatively low, This method gives you several billion handle values befare you run out of room, If you plan w use chat many resources, you'll want ro implement a more sophisticated handle distribution scheme. ‘Once the resource manager object has called the reate() function and passed in the target memory limit, the manager is ready to use. Simply call the Inserthe- source() fisnction to insert resources into the manager. Ifyou pass in che address of a handle instead of passing it by value, the function fills in the value for you, In the example program, we created a factory class that automatically allocares, creates, and then inserts the resource object into the manager. Ic is important to understand one ching about che resource manager. When you specify the memory target, the Inserthesource() fnction allows the memory target to be briefly exceeded by the amount of the current resource. The manager then Swaps out resources until the currendy used memory is lower than che threshold spec- ified, Although this methed may be acceptable if your resources are allocating out of a common memory pool or you are working in an environment with true virtual 56 Section 1 Programming Techniques memory, it could create problems if you are working with fixed amounes of special« ized memory, such as audio or texture memory. Requesting the manager to reserve an amount of memory for the resource you are about to load can solve the problem. This function, called &eservetlenory(), takes a standard size_type parameter, The function rerums true if it can free up the requested amount of memory. After this function successfully returns, you can then call Insertaesource(}. Most likely, che Aeservemenory() function would be ealled in dhe resource class's Create() function after loading some sort of resource header informa- tion, which would probably inform how much memory needs to be allocaced to hold the entire resource. Once the memory is reverved by the resource manager, the ¢ro- ate() function can finish che dara loading and insert che resource into the manager. In order to optimize this process, you might want to pre-load this information and store it in a glohally accessible table. How Handles Work This system uses handles in order to prevent clients from directly manipulating objects, which allows the manager the freedam to swap out resources as it sees fic. In order to gain access to a resource, the client must call a member function and pass in the handle in order to get back a pointer to the resource. Here's how it looks: SonpResource* pkes - (Senehesource*)}resngr.Gethesourco(hiesHandle) ; df(t phes) return Error; ff the resource can now be safely used before any other calls are i/ mode to the manager It is important to remember that the resource pointer must be considered valid only until another call to the resource manager is mace, Accessing another resource could cause the resource manager to swap out the resource you were previously access- ing. You will mostlikely wane to put asserts in your resource clasts code to ensure that their member functions are not called if the resource has been disposed. If for any reason you do want to get and hold onto a pointer roa resource, there isa mechanism in place to do so: the Lock() function, Locking a resouree increments the reference count on the object, which prevents the resource manager from dispos- ing of the object until the resource has been unlocked with, of course, the function Valock(). [cis important to remember to eventually unlock objects you've locked, or the resource manager assumes thar it is nor allowed to dispose of the resource when the program clases, and memory leaks could ensue. Since the resource manager has every resource indexed, it properly disposes of all resources automarically when its destructor is called. 17 Resource and Memory Management a7 Possible Extensions and Modifications The use of a resource manager ix extremely beneficial in managing large amounts of resources effectively. Alchough there is a slight increase in difficulty when access resources, this difficulty is offset by the simplicity of automatic memory management. If your application's entire data set is already indexed in che resource manager, a pre-caching system could be implemented by using existing functionality. To load a resource that has been determined a candidate for pre-caching, you should access the resource using the GetResource() function and raise the priority level. This method forces any swapped data in those resources co be reloaded and made ready for direct acess as well as discourages further swapping because of the heightened priority. Far a resource thar is no longer needed, simply lower che priority level in the resource, and it ts automatically discarded when more memory |s needed for other data. In addition to these enhancements, clients might want to build in more compre- hensive reporting functions, A feedback loop could be created to report on resources that are being discarded more than average, and the priority could be adjusted to min imize these sorts of problems. By effectively setting priority levels, pechaps even dynamically, clients can dramatically improve the performance of the manager, Other techniques you might want to try ase featured in a related article im this book, “A Generic Handle-Based Resource Manager," by Scott Bilas. Rather chan using the manager as a virtual memory system, this resource manager instead focuses on techniques such as using templates and more intelligent, type-safe handles. Conclusion ‘As the amount of dara content thar madem games must manipulate grows, the tech- niques for dealing with such vast quantities of data tmust also evolve, Creating an effective and efficient resource manager can help streamline the develapment process by allowing programmers to worry less about memory constraints and memory leaks, at the same rime providing a pawerfull tool for monitoring resource usage. 1.8 Fast Data Load Trick John Olsen One of the constant challenges with game programming is co make chings fist. Whenever you leave someone staring at a screen waiting, you break the flow of infor- mation and risk losing that player. One critical clement is the time it takes to load dara files into memory. With larger and larger game levels, you cnd up with longer and longer load rimes. Here is a trick chat can be used to reduce your load times. Preprocess Your Data One of the mast important things you can do to-your level dara is ta preprocess as much as you possibly can, This can be done either with a stand-alone utiliry program, such as a separate level editor used to edit your ingame dara, or within the game itself during development by enabling custom data-packing code for development builds. I've ued both methods, even on different portions of the same game, with good results, For the ultimate in fast data load times, you need to preprocess your data inwo the final formar it will take within the game. With a bit of planning, you can lay out your C++ classes of C structures in.a way char makes them good candidates for high-speed loading. Any data co be saved must be a non-satic member variable, and no poincers should be saved in the dara file. Ifyou need pointers in your data, be sure to never use them before seating them up properly after loading, since the data saved out in the pointer member is almost certain to contain bad dara when it is reloaded. Another possible option is to replace pointers with a handle of index number of some sort. See the article “A Generic Han- dle-Based Resource Manager,” by Soatt Bilas, for derails. Since Cry uscs viral function eables, you should make sure to not use any vir~ tual functions in your class, or it will end up calling into seemingly random memory locations when you overwrite your table with stale data, IF you want to play ie really safe, you can experiment with making all your accessor functions static, guaranteeing they woat show up in your dara. 1.8 Fast Data Load Trick a ao Save Your Data ‘Once your dara is all filled into structures, cither in-game or in a stand-alone picpro- cesiing tool, you can write that data out to disk. For C++, you can use your this pointer and stzeot() for the class. For C, just use the structure pointer and sizeof () for the structure, Be sure sot to use sizeot (this) or you will get the size of the pointer rather than the size. af the structure. This size is the size of the mon-static member data for your class, alang with any padding buile into che class by the compiler. Ideally, you have nested all your necessary data into one parent block holding all the athers, so you can load everything in one large nead. You have to break things up into multiple saves and loads if you are using anything bus one continuous section of inemory. The following example code shows how this might be done in Crs, with the seme dara class having member functions to perform che loading and saving, Please forgive the odd mixture of C++ classes with C file handling, If you're enough of a purist to be bothered by it, Fim sure its easy for you to change to your preferred method: #include class GameData a public: bool Save(char *fileName) ; bool Load(char *fileNams) ; bool ButferedLoad(char =fileviame); J/ Add accessora to get to your game data. private: /i Only open one file at a tina. static FILE *fileDeseriptor; #j Gane data goes here. int dataj1000]; // Replace this with yaur data fornat. Fi bool GameData: :Sava(char “fileName) TileDescriptor = fopen(filetiana, “wb"); Af(fileDescriptor) t Twrite(this, sizeof(GameDataj, 1, fileDescriptor); folose(fileDescriptor] ; Ji Report suctess writing the file. return TRUE; + else J} Report an error writing the file. return FALSE: f 0 Section 1 Programming Techniques: Load Your Data the Simple Way Saving the data as described previously makes it really casy to ger the dara back into your application later when you load the desired level. Just read the data back into the game, into the same structure or class you wrote it from: bool GaneData::Load{char *fileNene) { // Open tha Tile fer reading. falaDescriptor = fopen(filetane, “rb*); if(filepescriptor) { ‘fread(this, sizeof (GameData}, 1, fileDeseraptor); telose(fileDescriptor); Jf fisport success reading the file. return TRUE; alse ‘{ Report an error reading the file. return FALSE; } I Load Your Data More Safely There is atleast one teally important ching to watch our for on certain consele gaming hardware, Some systems always read our to the end of the current sector on a disk. For cxample, the Sony PlayStation loads data from CD-ROM in multiples of 2,048 bytes. ‘This means that if you read data directly into your structure, you stomp on whutever is in memory after thar structure if it isn't some mulkeiple af 2,048 bytes in length. ‘To avoid this memory stamp, you need to have 2 temporary buffer large enough ta hold the data file padded out to 22K boundary. Should you be reading several files, don't allocare and free a buffer each time. Instead, get the largest buffer size, allocate the buffer once, and reuse it for all reads. Free it after all the reads are compleved. Only che simpler single-read method is shown here, Ifyou are using a system with very tight memory; you might have already mapped our your entire memory usage and avoided dynamic memory all together. In that case, you need to find 3 buffer somewhere in memory thar is not in use at the time you need to read data files, Use that as your temporary buffer instead of using the dynamic memory allocation shown below: if Gheck your hardware to see what gize of blocks it reads. f] Put that value inte this define. ‘SdeTine AEAD_GRAMJLARITY 2048 bool GameData: :GutTerecLons(char *fileNane) 1.8 Fast Data Load Trick cay {f Wake sure there is roam in the read buffer. if This could be made smaller to match the if known read aize by making it a multiple of the id] READ GRANULARITY, but this way is 2 bit faster. char *tempBuTfar = new char[sizeot(GameData) + READ GAANULARITY]; if (!tampBuffer) { H Gould not allocate the buffer. ‘i? Return an error code. return FALSE; 1 TileDescriptor - fopenitiletiame, *rb*); if(filedeseriptor) { fread(tempSuffer, sizaot(GaneData), ileDescriptor); felose(fileDescriptor) mamcpy(this, tenpBuffer, sizeor (GaneData)); dalets tenpButfer; ff Report success reading the file. return TRUE; } alse { dalete tenpBuffor; ff Report an error reading the file. return FALSE; + } Now you are well on your way co highly optimized level loads. By preprocessing your data, you save che CPU time used ro convert data into a usable formar, and you compress che amount of data to be read. The best optimizations are those win/win sit- MEAEAS LE ira dusty ioe: pecan i eed eae d e 1.9 Frame-Based Memory Allocation Steven Ranck This article presents a simple and extremely fast memory allocacion system that pre- vents memory from becoming fragmented between game levels. It can be used for a wide range of game modules during level-loading time. In addition, the system is extremely fast at both allocating and de-allocating memory and can be used on any tyne of platform, from console to PC to arcade. The Challenges of Conventional Memory Allocation One problem with standard memory allocation systems that include mallact) and new is that memory can become fragmented and result in deteriorated game perfor- mance and the possibility of insufficiently Large memory blocks available, When an application requests a block of memory, sophisticared operating: systems, such as UNIX and Microsoft Windows, employ advanced memory management systems that can logically rearrange physical chunks of memory to create the requested contiguous memory block, But thisrearrangement comes at the cost of CPU cycles that the game could ordinarily have used. With game consoles, where the operating system is little more than a tiny set of slimmed-down library functions, there is no such sophisticaced memory manager. Introduction to Frame-Based Memory Asolution to these challenges of conventional memory allocation is franre-based meni ory. Frame-based memory eliminates memory fragmentation and is very fast. How- ever, it is not useful as a general-purpose memory allocation system like nalec() and new, Frame-based memory is best suited for game and level initialization modules. ‘As shown in Figure 1.9.1, frame-based memory works like a stack, At initializa- tion time, the game allocates a single memory block from the operating system, which

You might also like