Your guide to learning game development
In the last chapter we greatly improved how the player character moves in response to player input. But don't things look just a little bland as they are now? One blue rectangle against a gray background isn't very appealing. So why don't we change that? Ordinarily it would take near the end of development before one would start rendering something other than placeholder assets, but we'll do this early on simply to make working on the project a little more lively. In this chapter we'll replace those solid colors with some terribly amateur art assets. These assets are licensed under the Creative Commons CC0 License so you can do anything you'd like with them if you wish. Go ahead and download these assets from here. Extract the contents of the zip file straight into the directory for this game project. Make sure the extracted "assets" directory is placed in the same location as the executable for this game.
Before we can start loading images into memory and converting them to a format SDL2 can render, we'll need to initialize the SDL2_image library. The following should go just below the other initialization code but before the call to initGame:
if (IMG_Init(IMG_INIT_PNG) != IMG_INIT_PNG)
{
fprintf(stderr, "IMG_Init Error: %s\n", IMG_GetError());
return 1;
}
scope(exit) IMG_Quit();
This should be a very familiar looking pattern. We call IMG_Init and pass in flags to tell the library which image loaders we want SDL2_image to initialize. In this case we want to be able to load .png files, so we pass in the IMG_INIT_PNG flag. IMG_Init returns flags indicating all the image loaders it managed to initialize. If the return value doesn't match the flags we passed then we know there was an error. We can't recover from this error so we have no choice but to abort the game if the library fails to load the features we requested. If the call to IMG_Init succeeds, we call IMG_Quit at the end of the scope so the library can free any resources it allocated.
Now that we have initialized SDL2_image we can write some code to load image files into memory and prepare them for rendering. We can make this easier on ourselves by writing this helper function:
SDL_Texture* newTextureFromFile(SDL_Renderer* renderer, const char* fileName)
{
SDL_Surface* surface = IMG_Load(fileName);
if(!surface)
{
fprintf(stderr, "IMG_ERROR: %s\n", IMG_GetError());
return null;
}
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
if (!texture)
{
fprintf(stderr, "SDL_ERROR: %s\n", SDL_GetError());
}
SDL_FreeSurface(surface);
return texture;
}
Let's take a broad overview of exactly how this function behaves. It takes a pointer to our rendering context and a string containing the name of the .png file we want to load. It returns a pointer to an SDL_Texture, which is a handle to an image SDL2 has prepared for rendering. Should the function fail to load an image for any reason it will log an error and return a null pointer.
Let's examine the function in greater detail. We begin by calling IMG_Load and give it the file name passed in as a parameter. On success the function will return a pointer to an SDL_Surface containing information about the image file, such as its dimensions and the RGB values of each pixel. On failure it will return a null pointer. In the case of failure we call IMG_GetError for a human readable description of the problem and log the result to the console before returning null. In the case of success, we call SDL_CreateTextureFromSurface so SDL2 can transform the raw image data contained in the SDL_Surface to a form the library can render. This function will also return a null pointer on failure, so in that case we want to log an error message before returning the result. We then free the SDL_Surface allocated by IMG_Load since we don't need that information anymore and finally return the SDL_Texture pointer.
We now have the means to load a texture from a file. All we have left to do is load the image for the player character into memory and render it to the screen. As the texture will need to persist for the duration of the game, we need to store the texture somewhere. Add the following member to the GameState:
SDL_Texture* texture_hero;
Right now we're dealing with only a single texture, but this will change in the future. To prepare for that, let's make a function that will load all the textures needed by our game:
bool loadTextures(GameState* s, SDL_Renderer* renderer)
{
bool success = true;
s.texture_hero = newTextureFromFile(renderer, "assets//hero.png");
if(!s.texture_hero) success = false;
return success;
}
Right now all this function does is load the "hero.png" file and prepares it for rendering using helper function we wrote earlier. Should the texture fail to load, the function returns "false", otherwise it will return "true". Unfortunately this code isn't platform-agnostic; Linux and Max OSX use forward slashes to signify directories in file paths. We can fix this easily enough by defining a constant string with the correct directory separator for the platform we compile against. Add the following just below the import statements:
version(Windows)
{
enum dirSep = "\\";
}
else
{
enum dirSep = "/";
}
In the code above we use D's version condition to test if we're compiling on Windows. We define a compile-time constant called dirSep and set it to a backslash if we are, otherwise we set it to a forward slash. In this way, dirSep will be set to the appropriate slash depending on the operating system for which the game is compiled. Now edit the asset loading code inside loadTextures to take advantage of this new contant:
s.texture_hero = newTextureFromFile(renderer, "assets" ~ dirSep ~ "hero.png");
The "~" symbol in D is the cat operator and it's used to combine (concatonate) arrays together (strings included). This means the code above will effectively paste a backslash or forward slash between the "assets" and "hero.png" strings, creating a file path appropriate for the given operating system.
We now have a function for loading all the textures used in the game, let's add a matching function for unloading the game's textures:
void unloadTextures(GameState* s)
{
if(s.texture_hero) SDL_DestroyTexture(s.texture_hero);
}
This code should be pretty self-explanitory. We test to see if the texture_hero member is a valid SDL_Texture pointer and if it is we pass it to SDL_DestroyTexture to let the library free any associated memory.
Now that we've written our texture management functions let's go ahead and make use of them. Below our call to initGame, add the following:
if(!loadTextures(&s, renderer)) return 1;
scope(exit) unloadTextures(&s);
The above code attempts to load all of the game's textures, aborting the game should it fail. On success the textures are unloaded at the end of the scope. Now that we have all the loading code in place, we're ready to render the player character! Modify the renderEntity function likes so:
void renderEntity(SDL_Renderer* renderer, SDL_Texture* texture, Entity* e, int sourceWidth, int sourceHeight)
{
SDL_Rect source = void;
source.w = sourceWidth;
source.h = sourceHeight;
source.x = 0;
source.y = 0;
SDL_Rect dest = void;
dest.x = cast(int)e.pos.x - (e.width / 2);
dest.y = cast(int)e.pos.y - (e.height / 2);
dest.w = sourceWidth;
dest.h = sourceHeight;
SDL_RenderCopy(renderer, texture, &source, &dest);
}
In this code we call the vaguely-named SDL_RenderCopy function. This function is used to render a texture, or a portion of one, to the game window. It takes the render context, the SDL_Texture we want to render, and two SDL_Rects. The first SDL_Rect is the source rect which describes the rectangular region of pixels within the texture that we'd like to render. This allows us to basically take a cut-out from a larger image and place it inside the window. It's typical for 2D video games to store many images (often called "sprites") in a single texture, especially if the sprites are used to form an animation. The next SDL_Rect is the desintation rect which you should already be familiar from earlier chapters; it tells SDL2 where in the game window we'd like to render the texture. For now the source rectangle will start at the top-left of the texture and extend to the width and height we pass in as parameters. The destination rectangle is set much the same way as our old renderEntity code, except now the width and height are no longer based on the bounds of the entity; instead they're based on the width and height of the source rectangle.
Let's cut and paste the rendering code from the end of the game loop and put it all inside its own function, modifying the call to renderEntity as needed:
void renderGame(GameState* s, SDL_Renderer* renderer)
{
SDL_SetRenderDrawColor(renderer, 168, 168, 178, 255);
SDL_RenderClear(renderer);
renderEntity(renderer, s.texture_hero, &s.player, s.player.width, s.player.height);
SDL_RenderPresent(renderer);
}
At the end of the game loop we should call the renderGame function where the old rendering code used to be:
renderGame(&s, renderer);
If you compile and run the game now, you should see the new player character sprite rendered instead of that old blue rectangle. Pretty cool, isn't it? But it's still a little bland with that gray background so let's add a background image we can render as well. Add another member to the GameState struct:
SDL_Texture* texture_bg;
Now that we want to load another texture we'll need to modify the loadTextures function. The simplest way to do that would be to add the following to the function:
s.texture_bg = newTextureFromFile(renderer, "assets" ~ dirSep ~ "bg.png");
if(!s.texture_bg) success = false;
Since we're loading a new texture, let's make sure we unload it as well by adding the following to unloadTextures:
if(s.texture_bg) SDL_DestroyTexture(s.texture_bg);
We want the background to be rendered below the player character. SDL2 rendering functions are processed in the order they are called. This means if you render two textures at the same location the second texture will be rendered over the first. This is an example of "back to front" order and is the principle behind the Painter's algorithm. But enough about theory, add the follow inside the renderGame function just before we rendered the player character:
SDL_Rect bgDest;
SDL_QueryTexture(s.texture_bg, null, null, &bgDest.w, &bgDest.h);
bgDest.x = windowWidth / 2 - (bgDest.w / 2);
bgDest.y = windowHeight / 2 - (bgDest.h / 2);
SDL_RenderCopy(renderer, s.texture_bg, null, &bgDest);
We start by using SDL_QueryTexture to give us the width and height of the background texture, which we use to set the size of the destination rectangle. We then center the destination rectangle in the game window and render the texture to it using SDL_RenderCopy. Notice that we're passing null as the source rectangle. Doing so will cause SDL_RenderCopy to render the entire texture.
Compile and run the game and you should now see the player character and a grassy plain behind. As poor as the art assets are, it is a sight better than those solid colors from before. And that's all we set out to do this chapter.