Example Code: Code_050713.zip
In the previous post I talked about how you can use macro lists to solve the problem of wanting to generate a bunch of code based on the same set of data. This was useful for doing things like defining a list of resources a player could accumulate, and then being able to generate code to store and manipulate each resource type. You only had to update the resource list to add a new resource and the rest of the code would almost magically generate itself.
What if you wanted the reverse though? What if you had a fixed set of code that you want to apply to a bunch of different sets of data? This post is going to show you a way to do that.
In the example code, we are going to make a way to define several lists of items, and expand each list into an enum that also has a ToString and FromString function associated with it.
Another usage case for this technique might be to define lists of data fields, and expand each list into a data structure that contains serialization and deserialization functions. This would allow you to make data structures that could be saved and loaded to disk, or to sent and received over a network connection, just by defining what data fields they contained.
I haven’t yet seen this technique in the wild, and it kind of makes me wonder why since they are just two sides of the same coin.
GameEnums.h
In the last post, our data was always the same and we just applied it to different code. To do this, we had the code in one .h and the data in another .h that would get included multiple times. This allowed us to define different pieces of code in one .h, then include the other .h file to apply the fixed data to each piece of code.
In this post, it’s going to be the exact opposite. Our code will always stay the same and we will apply it to different data so our data will be in one .h and the code will be in another .h that gets included multiple times.
Here’s GameEnums.h:
////////////////////// // EDamageType ////////////////////// #define ENUMNAME DamageType #define ENUMLIST ENUMENTRY(Normal) ENUMENTRY(Electricity) ENUMENTRY(Fire) ENUMENTRY(BluntForce) #include "EnumBuilder.h" ////////////////////// // EDeathType ////////////////////// #define ENUMNAME DeathType #define ENUMLIST ENUMENTRY(Normal) ENUMENTRY(Electrocuted) ENUMENTRY(Incinerated) ENUMENTRY(Smashed) #include "EnumBuilder.h" ////////////////////// // EFruit ////////////////////// #define ENUMNAME Fruit #define ENUMLIST ENUMENTRY(Apple) ENUMENTRY(Banana) ENUMENTRY(Orange) ENUMENTRY(Kiwi) #include "EnumBuilder.h" ////////////////////// // EPlayers ////////////////////// #define ENUMNAME Player #define ENUMLIST ENUMENTRY(1) ENUMENTRY(2) ENUMENTRY(3) ENUMENTRY(4) #include "EnumBuilder.h"
EnumBuilder.h
This header file is where the real magic is; it’s responsible for taking the previously defined ENUMNAME and ENUMLIST macros as input, and turning them into an enum and the string functions. Here it is:
#include // for _stricmp, for the enum Fromstring function // this EB_COMBINETEXT macro works in visual studio 2010. No promises anywhere else. // Check out the boost preprocessor library if this doesn't work for you. // BOOST_PP_CAT provides the same functionality, but ought to work on all compilers! #define EB_COMBINETEXT(a, b) EB_COMBINETEXT_INTERNAL(a, b) #define EB_COMBINETEXT_INTERNAL(a, b) a ## b // make the enum E #define ENUMENTRY(EnumValue) EB_COMBINETEXT(e, EB_COMBINETEXT(ENUMNAME, EnumValue)), enum EB_COMBINETEXT(E,ENUMNAME) { EB_COMBINETEXT(EB_COMBINETEXT(e, ENUMNAME), Unknown) = -1, ENUMLIST EB_COMBINETEXT(EB_COMBINETEXT(e, ENUMNAME), Count), EB_COMBINETEXT(EB_COMBINETEXT(e, ENUMNAME), First) = 0, EB_COMBINETEXT(EB_COMBINETEXT(e, ENUMNAME), Last) = EB_COMBINETEXT(EB_COMBINETEXT(e, ENUMNAME), Count) - 1 }; #undef ENUMENTRY // make the EToString function const char *EB_COMBINETEXT(EB_COMBINETEXT(E,ENUMNAME), ToString)(EB_COMBINETEXT(E,ENUMNAME) value) { switch(value) { #define ENUMENTRY(EnumValue) case EB_COMBINETEXT(e, EB_COMBINETEXT(ENUMNAME, EnumValue)): return #EnumValue; ENUMLIST #undef ENUMENTRY } return "Unknown"; } // make the EFromString function EB_COMBINETEXT(E,ENUMNAME) EB_COMBINETEXT(EB_COMBINETEXT(E,ENUMNAME), FromString)(const char *value) { #define ENUMENTRY(EnumValue) if(!_stricmp(value,#EnumValue)) return EB_COMBINETEXT(e, EB_COMBINETEXT(ENUMNAME, EnumValue)); ENUMLIST #undef ENUMENTRY return EB_COMBINETEXT(EB_COMBINETEXT(e, ENUMNAME), Unknown); } // clean up #undef EB_COMBINETEXT #undef EB_COMBINETEXT_INTERNAL // these were defined by the caller but clean them up for convinience #undef ENUMNAME #undef ENUMLIST
Main.cpp
Now, here’s how you can actually use this stuff!
#include "GameEnums.h" int main(int argc, char* argv[]) { EDamageType damageType = eDamageTypeBluntForce; EDeathType deathType = EDeathTypeFromString("smashed"); EFruit fruit = eFruitLast; EPlayer player = EPlayerFromString(EPlayerToString(ePlayer1)); return 0; }
Combining the files
As a quick aside, in both this and the last post, I separated the code and data files. This is probably how you would normally want to do things because it’ll usually be cleaner, but it isn’t required. Here’s a cool technique I came across today…
Here’s Macro.cpp:
#ifdef MACROHEADER // Put your header stuff here #else // Put cpp type stuff here // Include the "header" #define MACROHEADER #include "Macro.cpp" #undef MACROHEADER #endif
If you ever really just want to combine all your code and data into a single file and not muddy up a directory or project with more files, this technique can help you do that. IMO you really ought to just use separate files, but I wanted to share this for when there are exceptions to that rule (as there always seem to be for every rule!)
Being Data Driven
After my last post, a fellow game developer friend of mine pointed out..
I hope that one day we hurl C++ into a raging sea of fire.
I do like this technique, in theory at least, but whenever I feel like it’s the right solution, a voice in the back of my head yells that I’m digging too greedily and too deeply and should step back for a second and consider what design choices have lead me to this point.
And I think that usually that introspection ends up at the intersection of “we’re not data-driven enough but want to be” and “we decided to use C++ for our engine.”
— jsola
He does have a point. For instance, in the case of our resource list from last post, it would be better if you had some “source data”, such as an xml file, listing all the resources a player could have. The game should load that data in on startup and make a dynamic array, etc to handle those resources. When you had game actions that added or subtracted specific resources from a player, the details of which resources got modified, and by how much, should also be specified in data.
When you save or pack your data, or at runtime (as your situation calls for), it can verify that your data is well formed and makes sure that if a data field is meant to specify a resource type, that it actually corresponds to an actual resource type listed in the list of resources.
That is closer to the ideal situation when making a game – especially when making larger games with a lot of people.
But there are still some good usage cases for this kind of macro magic (and template metaprogramming as well). For instance, maybe you use macros to define your data schemas so that your application can be data driven in the first place – I’ve done that on several projects myself and have seen other well respected people do it as well. So, add these things to your toolbox I say, because you never know when you might need them!
Next Post…
The next post will be what i promised at the end of the last one. I’m going to talk about a way to define a list of lists and then be able to expand that list of lists in a single go, instead of having to do a file include for each list individually.
Example Code: Code_050713.zip