The Game Developer's Guidepost

The Game Developer's Guidepost

Your guide to learning game development

Programming Simple 2D Games in D

Chapter 6: Introduction to Metaprogramming

In the last chapter we wrote functions for loading and unloading textures from .png files. If you look at those functions again you may notice the code for loading the hero and background textures are quite similar. In fact, the only differences are in the names of the files we load from and the members of the GameState in which we store the loaded SDL_Texture pointer. This is an example of code duplication, though in reality it's such a minor case that it's not really worth addressing. However, I'm eager to introduce a large part of what makes programming in D so powerful, and this seemed like as good of an excuse as any to introduce the topic.

Let's just pretend the code duplication in the loadTextures and unloadTextures functions is critical enough to find a solution. Say we had to manage hundreds of textures and we decided to change how the loadTextures function reported failure. In that case, we'd have to edit a line of code for every one of those hundreds of calls to newTextureFromFile. This is both time-consuming and error prone. What if we forgot to edit one of those lines? It would create a bug that could take a long time to manifest, assuming we caught it at all. Wouldn't it be nice if we could write our texture loading/unloading policy once and apply it automatically to every texture used in the game? We can actually do so easily enough by using a bit of metaprogramming.

But just what is metaprogramming anyway? It's the ability for a program to examine code (including its own) and generate additional code based off what it finds. Support for this is typically rather limited in compiled languages but D is an exception. The language offers powerful introspection capabilities paired with the ability to manipulate a program as it's being compiled. This chapter will only scratch the surface of what can be done using these built-in language facilities. Even so, we have some ground to cover before we jump to the solution. Let's start by introducing the features we'll take advantage of one at a time.

There are several statements in D that can be used for iteration. One of these is the "foreach loop" which is a succinct way to iterate over an array or range:

int[4] array = [2, 4, 6, 8];
foreach(element; array)
{
    printf("%d ", element);
}

/*
The code above will print the following:
2 4 6 8
*/

There's an additional form of the foreach loop which provides an index value that's incremented each iteration of the loop:

int[4] array = [2, 4, 6, 8];
foreach(index, element; array)
{
    printf("%lu ", index);
}

/*
The code above will print the following:
0 1 2 3
*/

It's important to note the foreach loop provides a copy of each element as it iterates. If we tried to assign a value to this copy, it wouldn't have any affect on the source array:

int[4] array = [2, 4, 6, 8];
foreach(index, num; array)
{
    num = 0;
    printf("%d ", array[index]);
}

/*
The code above will print the following:
2 4 6 8
*/

If we do wish assignment to affect the source array, we can use the "ref" keyword, like so:

int[4] array = [2, 4, 6, 8];
foreach(index, ref num; array)
{
    num = 0;
    printf("%d ", array[index]);
}

/*
The code above will print the following:
0 0 0 0
*/

The foreach loop can also iterate over a range of numbers. A number range is expressed by placing two consecutive periods between two numbers representing the lower and upper bounds of the range:

foreach(num; 4 .. 8)
{
    printf("%d ", num);
}

/*
The code above will print the following:
4 5 6 7
*/

Another important feature of D we need to introduce before we go on is the slice operator which is used to take a "slice" of an array. But what exactly is a slice? A slice can be thought of as a window into the memory of an existing array. It's not a copy of an array, nor does the act of slicing an array result in removing elements as the name may imply. Instead, slicing allows a programmer to easily take a sub-region of an array and perform logic only on that specific sub-region.

When declaring an array in D you write the data type followed by an opening square bracket, a number indicating the size of the array, and then a closing square bracket. A slice is declared in much the same way except no number is written between the square brackets. This should make intuitive sense because the slicing operation is done at runtime; the size of the slice can't be known until program execution. The slice operator is a pair of square brackets as well, which may be a little confusing at first. To slice a portion of an array, all you have to do is write a number range between the brackets you typically use to access the elements of the array. You can also slice the entire array by simply using a pair of square brackets with nothing between them. When taking a slice of an array the "$" symbol can be used as shorthand for the array's length. Let's put all this together with a simple example:

int[4] array = [2, 4, 6, 8];
int[] slice = array[2 .. $];
foreach(num; slice)
{
    printf("%d ", num);
}

/*
The code above will print the following:
6 8
*/

We can demonstrate that a slice isn't a copy by modifying its elements and observing how it affects the source array:

int[4] array = [2, 4, 6, 8];
int[] slice = array[];
foreach(index, ref num; slice)
{
    num = 10 - num;
    printf("%d ", array[index]);
}

/*
The code above will print the following:
8 6 4 2
*/

All of this is quite useful, but didn't we say this chapter was about metaprogramming? Don't worry, we're ready to show off a very simple case. All classes and structs in D have a special property called .tupleof which returns a list of their members. This property can be used with a foreach loop to iterate over all the members of a struct or class:

GameState s;
foreach(memberIndex, ref member; s.tupleof)
{
    enum memberName = __traits(identifier, GameState.tupleof[memberIndex]);
    printf("%s is a %s\n", memberName.ptr, typeof(member).stringof.ptr);
}

/*
The code above will print the following:
isKeyDown is a bool[5]
keycode is a int[5]
player is a Entity
texture_hero is a SDL_Texture*
texture_bg is a SDL_Texture*
*/

There's quite a few things to talk about in the few lines of code above. First, we use the .tupleof property on a GameState instance to get a list of all its members and use a foreach loop to iterate over them. Next things get a little more complex. The enum keyword used in this way says that we wish to declare a value known at compile-time. This compile-time value is returned by the __traits expression. This is a special compile-time expression used to ask the compiler a variety of questions. The first argument to the __traits expression determines what sort of question we're asking. In this case we're asking for a string with the name (or identifier) of a given symbol. The symbol we wish to get the identifier for is the GameState member over which we're currently iterating. We can easily access that by again using the .tupleof property on the GameState (this time on the type itself rather than the instance) and then using the memberIndex variable to index into its list of members.

Now that we have the member name we'd like to print it to the console. However, strings in D are actually an array of characters and printf expects a c-string. To convert, we simply use the .ptr property of the string to access a raw pointer to the array and pass that instead. This works as expected as string literals in D are null terminated in order to be compatible with C. We also want to print the type of the member to the console. We do this by first inspecting the type of the member using the aptly named "typeof" declaration. We then get a string representation of this type by using the .stringof property and then use the .ptr property to make the string compatible with printf.

With what little we've shown, you can probably already imagine the sort of power this level of introspection provides. But we're not done yet! The typeof declaration is much more interesting (and useful) when it's used together with conditional compilation. We can use "static if" to state whether a particular block of code should be compiled or not based on how a constant expression is evaluated. The "is" expression can be used to test if two data types are the same and is very powerful when used with "static if". Now we can easily write code to process each SDL_Texture* member of the GameState struct:

GameState s;
foreach(memberIndex, ref member; s.tupleof)
{
    enum memberName = __traits(identifier, GameState.tupleof[memberIndex]);
    static if(is(typeof(member) == SDL_Texture*))
    {
        printf("%s is a %s\n", memberName.ptr, typeof(member).stringof.ptr);	
    }
}

/*
The code above will print the following:
texture_hero is a SDL_Texture*
texture_bg is a SDL_Texture*
*/

You may have already noticed this, but each SDL_Texture member of the GameState begins with "texture_" and ends with the name of a corresponding .png file in the assets folder. If we could only strip off the first eight characters from the name of the member we could easily generate an appropriate file name. This can be done by taking a slice of the memberName string:

GameState s;
foreach(memberIndex, ref member; s.tupleof)
{
    enum memberName = __traits(identifier, GameState.tupleof[memberIndex]);
    static if(is(typeof(member) == SDL_Texture*))
    {
        printf("%s is in %s\n", memberName.ptr, (memberName[8 .. $] ~ ".png").ptr);	
    }
}

/*
The code above will print the following:
texture_hero is in hero.png
texture_bg is in bg.png
*/

If we take everything introduced in this chapter so far and put it all together we have covered all we need to make a simple solution to our code duplication problem. Using a little bit of metaprogramming we can automatically generate code that will load a .png file into the appropriate SDL_Texture* member and flag if the texture failed to load. Edit the loadTextures function as follows:

bool loadTextures(GameState* s, SDL_Renderer* renderer)
{
    bool success = true;

    foreach(memberIndex, ref member; s.tupleof)
    {
        enum memberName = __traits(identifier, GameState.tupleof[memberIndex]);
        static if(is(typeof(member) == SDL_Texture*))
        {
            member = newTextureFromFile(renderer, "assets" ~ dirSep ~ memberName[8 .. $] ~ ".png");
            if(!member) success = false;
        }
    }

    return success;
}

That's all there is to it! It's not very complicated and solves our code duplication problem quite nicely. This also means we can add or remove SDL_Textures from the GameState struct and the loadTextures function will generate code to load each of them automatically. This level of introspection is the sort of thing programmers expect from interpreted languages such as JavaScript and Python so it may be surprising to see in a compiled language like this one. Code like this would be impossible to write in C and would be too grueling a task for most programmers to undertake in C++ (assuming it could be done at all). But this sort of thing is easy to do in D using just a few lines of code, no external tools required.

Now that we're generating code to load all the SDL_Textures in the GameState, let's generate code to unload them. Replace the code inside the unloadTextures function with the following:

    foreach(memberIndex, ref member; s.tupleof)
    {
        static if(is(typeof(member) == SDL_Texture*))
        {
            if(member) SDL_DestroyTexture(member);
        }
    }

This is very similar to what we did in the loadTextures function: We loop through all the GameState members looking for a pointer to an SDL_Texture. If we find one, we check to see if the pointer is valid and unload it if so.

And that ends our short introduction to metaprogramming in D. What we did wasn't exactly necessary, but hopefully it's sparked enough interest in the topic for you to try experimenting for yourself. Metaprogramming isn't a silver bullet, but there are times where it can help make code cleaner and easier to maintain.

Chapter Resources