Macro Lists For The Win

Around 6 years ago I was introduced to a programming technique that really blew my mind. John, My boss at the time and the tech director at inXile, had written it as part of the code base for the commercial version of a game called Line Rider and I believe he said he first heard about the technique from a friend at Sony.

Since seeing it at inXile, I’ve seen the same technique or variations on it at several places including Monolith and Blizzard, and have had the opportunity to use it on quite a few occasions myself.

What is this technique? I’m not sure if it has an actual name but I refer to it as “Macro Lists” and it’s a good tool towards achieving the DRY principle (don’t repeat yourself – http://en.wikipedia.org/wiki/Don’t_repeat_yourself)

Macro lists are often useful when you find yourself copy / pasting code just to change a couple things, and have multiple places you need to update whenever you need to add a new entry into the mix.

For example, let’s say that you have a class to store how much of each resource that a player has and lets say you start out with two resources – gold and wood.

To implement this, you might write some code like this:

enum EResource
{
	eResourceGold,
	eResourceWood,
};

class CResourceHolder
{
public:
	CResourceHolder()
	{
		m_resources[eResourceGold] = 100.0f;
		m_resources[eResourceWood] = 0.0f;
	}

	float GetGold() const
		{ return m_resources[eResourceGold ]; }
	void SetGold(float amount)
		{ m_resources[eResourceGold ] = amount; }

	float GetWood() const
		{ return m_resources[eResourceWood]; }
	void SetWood(float amount)
		{ m_resources[eResourceWood] = amount; }
private:
	float m_resources[2];
};

That seems pretty reasonable right?

Now let’s say that you (or someone else who doesn’t know the code) wants to add another resource type. What do you need to do to add a new resource?

  1. Add a new enum value to the enum
  2. Initialize the new value in the array to zero
  3. Make a Get and Set function for the resource
  4. Increase the array size of m_resources to hold the new value

If #1 or #3 are forgotten, it will probably be really obvious and it’ll be fixed right away. If #2 or #4 are missed though, you are going to have some bugs that might potentially be very hard to track down because they won’t happen all the time, and they may only happen in release, or only when doing some very specific steps that don’t seem to have anything to do with the resource code.

Kind of a pain right? As the code gets more mature and more features are added, there will likely be other places that need to be updated too that will easily be forgotten. Also, when this sort of stuff comes up, people tend to copy/paste existing patterns and then change what needs to be changed – which can be really dangerous if people forget to change some of the values which need to be changed.

Luckily macro lists can help out here to ensure that it’s IMPOSSIBLE for you, or anyone else, to forget the steps of what to change. Macro lists make it impossible to forget because they do the work for you!

Check out this code to see what I mean. It took me a little bit to wrap my head around how this technique worked when I first saw it, so don’t get discouraged if you have trouble wrapping your head around it as well.

#define RESOURCE_LIST 
	RESOURCE_ENTRY(Gold, 100.0) 
	RESOURCE_ENTRY(Wood, 0)

// make the enum
#define RESOURCE_ENTRY(resource, startingValue) 
	eResource#resource,
enum EResource
{
	eResourceUnknown = -1,
	RESOURCE_LIST
	eResourceCount,
	eResourcefirst = 0
};
#undef RESOURCE_ENTRY

class CResourceHolder
{
public:
	CResourceHolder()
	{
		// initialize to starting values
		#define RESOURCE_ENTRY(resource, startingValue) 
			m_resources[eResource#resource] = startingValue;
		RESOURCE_LIST
		#undef RESOURCE_ENTRY
	}

// make a Get and Set for each resource
#define RESOURCE_ENTRY(resource, startingValue) 
	float Get#resource() const 
	{return m_resources[eResource#resource];} 
	void Set#resource(float amount) 
	{m_resources[eResource#resource] = amount;} 
RESOURCE_LIST
#undef RESOURCE_ENTRY

private:
	// ensure that our array is always the right size
	float m_resources[eResourceCount];
};

In the above code, the steps mentioned before happen automatically. When you want to add a resource, all you have to do is add an entry to the RESOURCE_LIST and it does the rest for you. You can’t forget any of the steps, and as people add new features, they can work with the macro list to make sure people in the future can add resources without having to worry about the details.

Include File Variation

If you used the above technique a lot in your code base, you could imagine that someone might name their macros the same things you named yours which could lead to a naming conflict.

Keeping the “global macro namespace” as clean as possible is a good practice to follow and there’s a variation of the macro list technique that doesn’t pollute the global macro namespace like the above.

Basically, you put your macro list in a header file, and then include that header file every place you would normally put a RESOURCE_LIST.

Here’s the same example broken up that way. First is ResourceList.h:

///////////////////////////////////
//	RESOURCE_ENTRY(ResourceName, StartingValue)
//
//	ResourceName - the name of the resource
//	StartingValue - what to start the resource at
//
RESOURCE_ENTRY(Gold, 100.0)
RESOURCE_ENTRY(Wood, 0)
///////////////////////////////////

And now here is CResourceHolder.h:

///////////////////////////////////
// make the enum
#define RESOURCE_ENTRY(resource, startingValue) 
	eResource#resource,
enum EResource
{
	eResourceUnknown = -1,
	#include "ResourceList.h"
	eResourceCount,
	eResourcefirst = 0
};
#undef RESOURCE_ENTRY

class CResourceHolder
{
public:
	CResourceHolder()
	{
		// initialize to starting values
		#define RESOURCE_ENTRY(resource, startingValue) 
			m_resources[eResource#resource] = startingValue;
		#include "ResourceList.h"
		#undef RESOURCE_ENTRY
	}

// make a Get and Set for each resource
#define RESOURCE_ENTRY(resource, startingValue) 
	float Get#resource() const 
	{return m_resources[eResource#resource];} 
	void Set#resource(float amount) 
	{m_resources[eResource#resource] = amount;}
#include "ResourceList.h"
#undef RESOURCE_ENTRY

private:
	// ensure that our array is always the right size
	float m_resources[eResourceCount];
};

The Downside of Macro Lists

So, while doing the above makes code a lot easier to maintain and less error prone, it comes at a cost.

Most notably is it can be really difficult to figure out what code the macros will expand to, and it can be difficult to alter the functionality of the macros. A way to lessen this problem is that you can tell most compilers to make a file that shows what your code looks like after the preprocessor is done with it. It can still be difficult even with this feature, but it does help a lot.

When you have compiler errors due to macros, because perhaps you forgot a parameter, or it’s the wrong type, the compiler errors can be pretty difficult to understand sometimes.

Another problem with macros is that I don’t know of any debuggers that will let you step through macro code, so in a lot of ways it’s a black box while you are debugging, which sucks if that code malfunctions. If you keep your functionality simple, straightfoward and format it cleanly, you ought not to hit many of these problems though.

Instead of using macro lists, some people prefer to put their data into something like an xml or json data file, and then as a custom build step, use XSLT or the like to convert that data into some code, just like the C++ preprocessor would. The benefit here is that you can see the resulting code and step through it while debugging, but of course the downside is it can be more difficult for someone else to get set up to be able to compile your code.

To Be Continued…

Macro lists are great, but what if you want your lists to have sublists? For instance, what if you wanted to define network messages for your game in a format like this, and have it automatically expand into full fledged classes to be able to ensure that message parsing and data serialization was always done in a consistent way to minimize bugs and maximize efficiency (less code to write and less testing to do)?

As you might have noticed, macro lists can take parameters to help them be flexible (like, the starting value of the resources… you could add more parameters if you wanted to), but, a macro list can’t contain another macro list. At least not how the above implementations work.

I’m going to show you how to tackle this problem in the next post, so stay tuned! (:


3 comments


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s