Example Code: Code_051113
In the two previous posts I showed some ways to use macro lists that I’ve found useful in the past. A downside to those methods though was that you had to manually expand / use each macro list individually. Whenever you have to do extra steps that are always the same, you have a possible point of failure in your code / development process that doesn’t need to be there (when you forget to do the step that’s always the same).
Wouldn’t it be great if you could make a list of macro lists, such as a list of enums that each had their own enum values, and say “expand all of these into all the enums described, with to and from string conversion functions” and it would do it?
How about describing a bunch of events that can happen in your game, specifying what data fields the events should have, what their types and default values are, as well as whether they are optional and required – and it would build full on classes or structs based on your settings to facilitate strongly typed, compile time protected message and event passing that also supported inheritance and custom dynamic cast functionality?
The example code for this post shows you how to do both of those things.
Boost Preprocessor Library (BOOST_PP)
I had to pull out the “big guns” for making this example work, specifically the boost preprocessor library. Yes it’s true that anything the boost preprocessor library can do, you can also do yourself without the boost preprocessor library. However, the neat thing about using the boost preprocessor library is that there are variations in the behaviors of the preprocessors for different compilers, and boost abstracts that away from you so that you know if you use their functions, they ought to work and behave the same way on other compilers. That’s a bunch of compatibility headaches that you don’t have to worry about.
The boost preprocessor library has a very developer friendly license too. It’s very similar to the zlib / mit license which in effect says “don’t call us and we won’t call you”. You can use it commercially for free, without having to give them any credit for anything, and also you aren’t required under any circumstances to link dynamically to avoid having to make your code open sourced (a problem you commonly hit with some other software licenses). That being said, there are a couple small requirements about keeping the license file in tact when redistributing the source code of their library etc so have a look at the license just to make sure you dot all your i’s and cross all your t’s, but if you work somewhere that is hesitant to use open sourced software for legal reasons (I have worked at my share of those places), the license is literally the same as zlib and i’ll bet your project is already using zlib, or one of the libraries you are using is internally using zlib. So seriously… there ought not be any problems with using boost_pp in your code, unless your legal department is retarded (again, something i’ve experienced in times past LOL).
Best of all, the boost preprocessory library is SUPER EASY to download, integrate into your project and start using. I’ve included a version in the sample code. It’s just a folder with some headers in it. The sample code #include’s some of those header files and it works. No other setup needed. You didn’t even need to build boost or link to it or anything crazy like that. SUPER EASY, don’t be afraid of boost_pp! Boost.org
FWIW I had my doubts about boost_pp too, but a fellow game dev friend of mine turned me onto it and I’m really glad he did. As they say, T-Law is the law.
Main.cpp – What our program is able to do in the end
Here’s the main.cpp file to show the capabilities of our sample code. Note specifically the custom dynamic cast functions, enum to/from string functions, and the CGameEvent objects. The constructors to the CGameEvent objects have required parameters for all required fields, and defaulted, optional parameters for all of the optional fields of the defined message.
#include "GameEvents.h" int main(int argc, char **argv) { // make some test events CGameEventPlayerDamaged damaged(ePlayer1, 5.0f, eDamageTypeElectricity); CGameEventPlayerDeath death(ePlayer1, eDeathTypeElectrocution); // get base class pointers to them CGameEvent *gameEvent1 = &damaged; CGameEvent *gameEvent2 = &death; // do a dynamic cast on the 1st game event (the damaged event) // test1a will be non null, and test1b will be null CGameEventPlayerDamaged *test1a = gameEvent1->GetAs(); CGameEventPlayerDeath *test1b = gameEvent1->GetAs(); // do a dynamic cast on the 2nd game event (the death event) // test2a will be null, test2b will be non null CGameEventPlayerDamaged *test2a = gameEvent2->GetAs(); CGameEventPlayerDeath *test2b = gameEvent2->GetAs(); // set and get some values using the accessors damaged.SetIsFatal(!damaged.GetIsFatal()); damaged.SetDamageTaken(damaged.DefaultDamageTaken()); damaged.SetAttackerPlayerID(EPlayerFromString(EPlayerToString(ePlayer2))); death.SetDeathType(EDeathTypeFromString("drowned")); return 0; }
GameEvents.h
This is the file where the game events are defined. For each event, you specify the data type, the field name, the default value, and whether the field is required or optional. Note that you must specify all required field before all optional fields. It’s possible that there’s a way to make it so you don’t have to do that, but I couldn’t figure out a way. Note that if you mess that up, you’ll get a compile error.
Optional / required settings for fields are enforced by the constructor in each game event class.
//===================================================================================== // // GameEvents.h // // Define game events here. They are expanded into full classes for each event. // //===================================================================================== // include our enums so we can use them in our event definitions. #include "GameEnums.h" // Note that we could also use complex structures, even other events in our event // definitions. Also, this code could be expanded to allow you to define inheritance // for each event. So for instance, the PlayerDeath event could inherit the // PlayerDamaged event if you wanted it to, and it would contain all of the same fields // that the PlayerDamaged event had. It might get a bit tricky making the constructor // though. #define EventList /*===================================================================================*/ /* Settings */ /*===================================================================================*/ (GameEvent) /*Event Prefix*/ ( /*===================================================================================*/ /* PlayerDamaged */ /*===================================================================================*/ ( (PlayerDamaged) ( ((EPlayer) (VictimPlayerID) (ePlayerUnknown) (ELB_REQUIRED)) ((float) (DamageTaken) (0.0f) (ELB_REQUIRED)) ((EDamageType) (DamageType) (eDamageTypeNormal) (ELB_OPTIONAL)) ((EPlayer) (AttackerPlayerID) (ePlayerUnknown) (ELB_OPTIONAL)) ((bool) (IsFatal) (false) (ELB_OPTIONAL)) ) ) /*===================================================================================*/ /* PlayerDeath */ /*===================================================================================*/ ( (PlayerDeath) ( ((EPlayer) (VictimPlayerID) (ePlayerUnknown) (ELB_REQUIRED)) ((EDeathType) (DeathType) (eDeathTypeNormal) (ELB_OPTIONAL)) ) ) /*===================================================================================*/ ) /*===================================================================================*/ // build our event classes #include "EventListBuilder.h"
GameEnums.h
Here’s where game enums are defined. Note that it makes an enum per entry here, but it also makes ToString and FromString functions for each enum type.
//===================================================================================== // // GameEnums.h // // Game enums are defined here //===================================================================================== #define EnumList /*===================================================================================*/ /* EDeathType */ /*===================================================================================*/ ( (DeathType) ( (Normal) (Incineration) (Electrocution) (Drowned) (Crushed) (Telefrag) ) ) /*===================================================================================*/ /* EDamageType */ /*===================================================================================*/ ( (DamageType) ( (Normal) (Fire) (Electricity) (Ice) (Arcane) ) ) /*===================================================================================*/ /* EPlayer */ /*===================================================================================*/ ( (Player) ( (1) (2) (3) (4) ) ) /*===================================================================================*/ #include "EnumBuilder.h"
EnumBuilder.h
The simpler of the two files used to generate code from our data, here’s EnumBuilder.h.
Note that I use both BOOST_PP_SEQ_FOR_EACH as well as BOOST_PP_SEQ_FOR_EACH_I. The reason for that is because with BOOST_PP you are unable to do nested for loops. However, if you use two different looping methods, it works just fine. So… i use FOR_EACH for one level of looping, and FOR_EACH_I for another level of looping. I’m not sure if you could do 3 or more levels of looping by switching between which loop method you use or not.
//===================================================================================== // // EnumBuilder.h // // Define "EnumList" and then include this header file to expand it into an enum and // also provide ToString and FromString functions. // // Here's an example of what EnumList might look like // // #define EnumList // ( // (FruitType) // ( // (Apple) // (Pear) // (Banana) // (Orange) // ) // ) // ( // (VegieType) // ( // (Potato) // (Broccoli) // ) // ) // // The first entry is the name of the enum, and then after that are the enum values. // You can define as many enums as you would like I believe. Boost might have an // internal limit, but I'm not sure... // // The above would make two enums like below: // // enum EFruitType // { // eFruitTypeUnknown = -1, // // eFruitTypeApple, // eFruitTypePear, // eFruitTypeBanana, // eFruitTypeOrange, // // eFruitTypeCount, // eFruitTypeFirst = 0 // }; // // enum EVegieType // { // eVegieTypeUnknown = -1, // // eVegieTypePotato, // eVegieTypeBroccoli, // // eVegieTypeCount, // eVegieTypeFirst = 0 // }; // // It will also make these functions: // // const char *EFruitTypeToString( EFruitType value ); // EFruitType EFruitTypeFromString( const char *value ); // // const char *EVegieTypeToString( EVegieType value ); // EVegieType EVegieTypeFromString( const char *value ); // //===================================================================================== #include #include #include #include //===================================================================================== // define an enum value #define ENB_ENUMVALUE(depth, enumName, enumItem) BOOST_PP_CAT(e, BOOST_PP_CAT(enumName, enumItem)), // convert something to a string #define ENB_TOSTRING(x) #x // ToString() enum case #define ENB_TOSTRINGCASE(depth, enumName, enumItem) case BOOST_PP_CAT(e, BOOST_PP_CAT(enumName, enumItem)): return ENB_TOSTRING(enumItem); // FromString() string test #define ENB_FROMSTRINGTEST(depth, enumName, enumItem) if (!stricmp(ENB_TOSTRING(enumItem), value)) return BOOST_PP_CAT(e, BOOST_PP_CAT(enumName, enumItem)); // define the enum, including an "unknown" value at -1, and also "first" and "count" // values which are useful for iterating through enum values #define ENB_MAKEENUM(depth, data, index, enumList) enum BOOST_PP_CAT(E,BOOST_PP_SEQ_ELEM(0, enumList)) { BOOST_PP_CAT(BOOST_PP_CAT(e,BOOST_PP_SEQ_ELEM(0, enumList)),Unknown) = -1, BOOST_PP_SEQ_FOR_EACH(ENB_ENUMVALUE, BOOST_PP_SEQ_ELEM(0, enumList), BOOST_PP_SEQ_ELEM(1, enumList)) BOOST_PP_CAT(BOOST_PP_CAT(e,BOOST_PP_SEQ_ELEM(0, enumList)),Count), BOOST_PP_CAT(BOOST_PP_CAT(e,BOOST_PP_SEQ_ELEM(0, enumList)),First) = 0, }; // make the ToString function #define ENB_MAKETOSTRING(depth, data, index, enumList) const char *BOOST_PP_CAT(BOOST_PP_CAT(E,BOOST_PP_SEQ_ELEM(0, enumList)),ToString) (BOOST_PP_CAT(E,BOOST_PP_SEQ_ELEM(0, enumList)) value) { switch(value) { BOOST_PP_SEQ_FOR_EACH(ENB_TOSTRINGCASE, BOOST_PP_SEQ_ELEM(0, enumList), BOOST_PP_SEQ_ELEM(1, enumList)) } /* error case */ return "Unknown"; } // make the FromString function #define ENB_MAKEFROMSTRING(depth, data, index, enumList) BOOST_PP_CAT(E,BOOST_PP_SEQ_ELEM(0, enumList)) BOOST_PP_CAT(BOOST_PP_CAT(E,BOOST_PP_SEQ_ELEM(0, enumList)),FromString) (const char *value) { BOOST_PP_SEQ_FOR_EACH(ENB_FROMSTRINGTEST, BOOST_PP_SEQ_ELEM(0, enumList), BOOST_PP_SEQ_ELEM(1, enumList)) /* error case */ return BOOST_PP_CAT(BOOST_PP_CAT(e,BOOST_PP_SEQ_ELEM(0, enumList)),Unknown); } //===================================================================================== // make all of the enums BOOST_PP_SEQ_FOR_EACH_I(ENB_MAKEENUM, _, EnumList) // make the ToString functions BOOST_PP_SEQ_FOR_EACH_I(ENB_MAKETOSTRING, _, EnumList) // make the FromString functions BOOST_PP_SEQ_FOR_EACH_I(ENB_MAKEFROMSTRING, _, EnumList) //===================================================================================== // don't pollute other code with our macros #undef ENB_ENUMVALUE #undef ENB_TOSTRING #undef ENB_TOSTRINGCASE #undef ENB_FROMSTRINGTEST #undef ENB_MAKEENUM #undef ENB_MAKETOSTRING #undef ENB_MAKEFROMSTRING // this was defined by the caller but clean it up here for convinience #undef EnumList
EventListBuilder.h
Here’s the code that expands an event list into classes with constructors, accessors and dynamic casting functionality:
//===================================================================================== // // EventListBuilder.h // // Define "EventList" and then include this header file to expand it into classes // // The EventList schema is: // // (ExpansionPrefix) // (Events) // // ExpansionPrefix - a prefix added to every event name in the group // Events - the list of events, see the schema below // // The Events schema is: // // (EventName) // ( // ((type)(Name)(Default)(Required)) // ) // // EventName - The name of the event. A "C" and the expansion prefix are added // to the front when making it into a class. // // Type - The data type of the field. Make sure the data type is "viewable" // before including EventListBuilder.h // // Name - The name of the data field, used to name member vars and accessors // // Default - The default value of the field. // // Required - Specifies whether a value must be provided when creating a new event // of this type or not. If optional fiels are not provided, the default // value will be used. ELB_OPTIONAL / ELB_REQUIRED // // Note: All required parameters must come before all optional parameters // // Example EventList: // // #define EventList // (InputEvent) // ( // ( // (ButtonPress) // ( // ((EButtonId) (ButtonId) (eButtonInvalid) (ELB_REQUIRED)) // ((bool) (Pressed) (true) (ELB_REQUIRED)) // ) // ) // ( // (AnalogStick) // ( // ((float) (PosX) (0.0f) (ELB_REQUIRED)) // ((float) (PosY) (0.0f) (ELB_REQUIRED)) // ((EAnalogStickId) (AnalogStickId) (eAnalogStickMouse) (ELB_OPTIONAL)) // ) // ) // ) // // The above example will make two event classes with getters/setters for members // and a constructor on each class which has optional and required parameters as // specified in the EventList data. // // CInputEventButtonPress // EButtonId ButtonId - the button that generated the event // bool Pressed - whether the button was pressed or released // // CInputEventAnalogStick // float PosX - X position of the stick // float PosY - Y position of the stick // EAnalogStickId AnalogStickId - optional parameter to specify which stick. // defaults to "mouse" // //===================================================================================== #include #include #include #include //===================================================================================== // Define the base class // C // // It has a templated GetAs function that works like a dynamic cast and lets you ask // the event type too. //===================================================================================== #define ELB_DEFINEBASECLASS(expansionSettings) class BOOST_PP_CAT(C, expansionSettings) { public: /* Constructor */ BOOST_PP_CAT(C, expansionSettings) (BOOST_PP_CAT(E,expansionSettings) eventType) { m_eventType = eventType; } /* EventType() */ BOOST_PP_CAT(E,expansionSettings) EventType() const {return m_eventType;} /* GetAs() */ template EventClass *GetAs() { if (m_eventType == EventClass::StaticEventType()) return (EventClass *)this; else return 0; } private: BOOST_PP_CAT(E,expansionSettings) m_eventType; }; //===================================================================================== // Define the class // C // // It has getters and setters for each data member defined and the construct handles // initialization of members to defaults and enforces required data fields. //===================================================================================== #define ELB_DEFINECLASS(depth, expansionSettings, classData) class BOOST_PP_CAT(C, BOOST_PP_CAT(expansionSettings, BOOST_PP_SEQ_ELEM(0, classData))) : public BOOST_PP_CAT(C, expansionSettings) { public: /* Constructor */ BOOST_PP_CAT(C, BOOST_PP_CAT(expansionSettings, BOOST_PP_SEQ_ELEM(0, classData))) ( BOOST_PP_SEQ_FOR_EACH_I(ELB_CONSTRUCTPARAMS, _, BOOST_PP_SEQ_ELEM(1, classData)) void *dummyParamIgnore = 0 ) : BOOST_PP_CAT(C, expansionSettings) (BOOST_PP_CAT(e,BOOST_PP_CAT(expansionSettings, BOOST_PP_SEQ_ELEM(0, classData)))) { BOOST_PP_SEQ_FOR_EACH_I(ELB_INITIALIZEMEMBERVAR, _, BOOST_PP_SEQ_ELEM(1, classData)) } /* Accessors - Get, Set, static Default*/ BOOST_PP_SEQ_FOR_EACH_I(ELB_DEFINEMEMBERVARACCESSORS, _, BOOST_PP_SEQ_ELEM(1, classData)) /* StaticEventType() */ static BOOST_PP_CAT(E,expansionSettings) StaticEventType() { return BOOST_PP_CAT(e,BOOST_PP_CAT(expansionSettings, BOOST_PP_SEQ_ELEM(0, classData))); } private: /* Member Variables */ BOOST_PP_SEQ_FOR_EACH_I(ELB_DEFINEMEMBERVAR, _, BOOST_PP_SEQ_ELEM(1, classData)) }; //===================================================================================== // to make required / optional more apparent in the EventList. // NOTE: BOOST_PP_IF needs to take a 1 or 0 unfortunately! #define ELB_REQUIRED 1 #define ELB_OPTIONAL 0 // add member variables to the constructor, optional or required as specified in data #define ELB_CONSTRUCTPARAMS(depth, data, index, classData) BOOST_PP_SEQ_ELEM(0, classData) BOOST_PP_SEQ_ELEM(1, classData) BOOST_PP_IF(BOOST_PP_SEQ_ELEM(3, classData), , = BOOST_PP_SEQ_ELEM(2, classData)), // initialize a member variable to the parameter passed into the constructor #define ELB_INITIALIZEMEMBERVAR(depth, data, index, classData) BOOST_PP_CAT(m_,BOOST_PP_SEQ_ELEM(1, classData)) = BOOST_PP_SEQ_ELEM(1, classData); // define a single member variable #define ELB_DEFINEMEMBERVAR(depth, data, idex, classData) BOOST_PP_SEQ_ELEM(0, classData) BOOST_PP_CAT(m_,BOOST_PP_SEQ_ELEM(1, classData)); // define a single member variable accessors #define ELB_DEFINEMEMBERVARACCESSORS(depth, data, index, classData) BOOST_PP_SEQ_ELEM(0, classData) BOOST_PP_CAT(Get,BOOST_PP_SEQ_ELEM(1, classData))() const { return BOOST_PP_CAT(m_,BOOST_PP_SEQ_ELEM(1, classData)); } void BOOST_PP_CAT(Set,BOOST_PP_SEQ_ELEM(1, classData))(const BOOST_PP_SEQ_ELEM(0, classData) &value) { BOOST_PP_CAT(m_,BOOST_PP_SEQ_ELEM(1, classData)) = value; } static BOOST_PP_SEQ_ELEM(0, classData) BOOST_PP_CAT(Default,BOOST_PP_SEQ_ELEM(1, classData))() { return BOOST_PP_SEQ_ELEM(2, classData); } // define an enum value #define ELB_ENUMVALUE(depth, expansionSettings, classData) BOOST_PP_CAT(e,BOOST_PP_CAT(expansionSettings, BOOST_PP_SEQ_ELEM(0, classData))), //===================================================================================== // make an enum of the event types enum BOOST_PP_CAT(E,BOOST_PP_SEQ_ELEM(0, EventList)) { BOOST_PP_SEQ_FOR_EACH(ELB_ENUMVALUE, BOOST_PP_SEQ_ELEM(0, EventList), BOOST_PP_SEQ_ELEM(1, EventList)) }; // define the base class ELB_DEFINEBASECLASS(BOOST_PP_SEQ_ELEM(0, EventList)) // build EventList into classes BOOST_PP_SEQ_FOR_EACH(ELB_DEFINECLASS, BOOST_PP_SEQ_ELEM(0, EventList), BOOST_PP_SEQ_ELEM(1, EventList)) //===================================================================================== // don't pollute other code with our macros #undef ELB_DEFINEBASECLASS #undef ELB_DEFINECLASS #undef ELB_REQUIRED #undef ELB_OPTIONAL #undef ELB_CONSTRUCTPARAMS #undef ELB_INITIALIZEMEMBERVAR #undef ELB_DEFINEMEMBERVAR #undef ELB_DEFINEMEMBERVARACCESSORS #undef ELB_ENUMVALUE // this was defined by the caller but clean it up for convinience #undef EventList
Possible Improvements
- Imagine if the event classes had binary and/or text serialization for being able to save and load events from disk, or over a network connection. Something like this sample code could be the back bone of your editor, your save game systems, or your network communication protocol. It would be type safe and easily modified. When you changed structures, you’d likely get compile errors or warnings in most of the places you needed to update code to support the changes.
- The definitions of enums and events are kind of weird in that they are lists of text in parentheses because that’s what a “boost sequence” is (BOOST_PP_SEQ family of functions). Boost also has something called tuples which are comma seperated lists. I think tuples would be more natural for usage in some of the definition cases but i was unable to get that working for whatever reason. If you end up getting that working, post back and let us all know how you did it!
- There are a lot of “magic numbers” in the macros, such as saying “use sequence field 0 for this” or “use sequence field 2 for this”. It probably would be better (more readable at least) if I defined symbolic constants for various indices. That way you’d see ELB_ENUMNAME instead of “0”, which would make a lot more sense.
That’s All
This post was code heavy and explanation light but hopefully you can understand what’s going on here. If you have any questions about the code, or why I did something one way instead of doing it a different way, post a comment and I’ll get back to you!
Example Code: Code_051113