Lesson 3: SDL Extension Libraries

Up until now we’ve only been using BMP images as they’re the only type supported by the base SDL library, but being restricted to using BMP images isn’t that great. Fortunately there are a set of SDL extension libraries that add useful features to SDL, such as support for a wide variety of image types through SDL_image. The other available libraries are SDL_ttf which provides TTF rendering support, SDL_net which provides low level networking and SDL_mixer which provides multi-channel audio playback.

Installing an Extension Library

In this lesson we’ll just need SDL_image but the installation process for all the extension libraries is the same. Download the development library for your system from the project page and follow the instructions below to get SDL_image set up on your system.

  • Windows (MinGW or Visual Studio): Merge the extension library’s files into your existing SDL2 directory. You’ll also need to copy the SDL2_image, zlib and any image format dlls (such as libpng) over to your executable directory so that they’re available at runtime

  • Linux: Install from your package manager or by downloading the source and building with CMake.

  • Mac: Download the .dmg from the site and follow the Readme.

To use the libraries you’ll need to update your includes and build system to link against the new extension library.

Cmake

Grab the appropriate module from the repo and add it to your CMake modules directory (cmake/). You can then look up the new extension library using the same find_package method used to find SDL2 and include SDL_image.h in your source file.

find_package(SDL2_image REQUIRED)
include_directories(${SDL2_IMAGE_INCLUDE_DIR})
# Link this lesson against SDL2 and SDL2_image
add_executable(Lesson3 src/main.cpp)
target_link_libraries(Lesson3 ${SDL2_LIBRARY} ${SDL2_IMAGE_LIBRARY})
# Install target is the same as before


Visual Studio

Include SDL_image.h and add SDL2_image.lib to your linker dependencies.

GCC or Clang on Linux and MinGW on Windows

Include SDL2/SDL_image.h and add -lSDL2_image to the SDLLIB variable in your makefile.

Clang on Mac

Include SDL2_image/SDL_image.h and add -framework SDL2_image to the SDL variable in your makefile.

Setting up Tile Properties

In this lesson we’ll see how to load images with SDL_image along with looking into scaling our textures when they’re drawn and come up with a method to place tiles based on the window size and the tile size. To do this we’ll need to define a tile size constant, which we’ll add below our window constants.

const int SCREEN_WIDTH  = 640;
const int SCREEN_HEIGHT = 480;
//We'll just be using square tiles for now
const int TILE_SIZE = 40;


Loading Textures with SDL_image

SDL_image lets us load multiple types of images along with allowing us to load them directly to an SDL_Texture with IMG_LoadTexture. With this function almost all of our loadTexture code can be replaced and now we call IMG_LoadTexture to load the texture, check for errors and return. We can still use logSDLError to log errors from the SDL_image library as the IMG_GetError function is just a define of SDL_GetError.

/**
* Loads an image into a texture on the rendering device
* @param file The image file to load
* @param ren The renderer to load the texture onto
* @return the loaded texture, or nullptr if something went wrong.
*/
SDL_Texture* loadTexture(const std::string &file, SDL_Renderer *ren){
	SDL_Texture *texture = IMG_LoadTexture(ren, file.c_str());
	if (texture == nullptr){
		logSDLError(std::cout, "LoadTexture");
	}
	return texture;
}


Specifying Width and Height for Rendering

With SDL2 we’re able to scale textures when they’re rendered by specifying a width and height for the destination rectangle that differ from the texture’s width and height. However it’s also common to draw the textures without any scaling applied and it’d be a real pain if we had to specify the width and height each time we wanted to draw without scaling. To get around this we’ll create two versions of renderTexture. One will take the width and height along with the other parameters while another version will mirror our original and will use the texture’s width and height for the destination.

To set the texture’s width and height for drawing we simply write the width and height we want to the destination rect’s width and height fields instead of getting them from the texture.

/**
* Draw an SDL_Texture to an SDL_Renderer at position x, y, with some desired
* width and height
* @param tex The source texture we want to draw
* @param ren The renderer we want to draw to
* @param x The x coordinate to draw to
* @param y The y coordinate to draw to
* @param w The width of the texture to draw
* @param h The height of the texture to draw
*/
void renderTexture(SDL_Texture *tex, SDL_Renderer *ren, int x, int y, int w, int h){
	//Setup the destination rectangle to be at the position we want
	SDL_Rect dst;
	dst.x = x;
	dst.y = y;
	dst.w = w;
	dst.h = h;
	SDL_RenderCopy(ren, tex, NULL, &dst);
}


We’ll also create a function that provides the old functionality of drawing the texture without any scaling. This function will just get the width and height from the texture then call our new renderTexture function.

/**
* Draw an SDL_Texture to an SDL_Renderer at position x, y, preserving
* the texture's width and height
* @param tex The source texture we want to draw
* @param ren The renderer we want to draw to
* @param x The x coordinate to draw to
* @param y The y coordinate to draw to
*/
void renderTexture(SDL_Texture *tex, SDL_Renderer *ren, int x, int y){
	int w, h;
	SDL_QueryTexture(tex, NULL, NULL, &w, &h);
	renderTexture(tex, ren, x, y, w, h);
}


Initialize SDL_image (Optional)

When loading an image for the first time SDL_image will automatically initialize the necessary image loading subsystem, however this will cause some delay in loading the image since SDL_image will have to perform its initialization setup first. If you’d like to initialize SDL_image earlier to avoid the delay when loading an image type for the first time you can do so with IMG_Init. IMG_Init will return a bitmask of all the currently initialized image loaders, so we’ll perform an and with the flags we passed to see if initialization was successful. Here we only initialize the PNG loader since that’s all we’ll need for this lesson. This initialization should be done after initializing SDL.

if ((IMG_Init(IMG_INIT_PNG) & IMG_INIT_PNG) != IMG_INIT_PNG){
	logSDLError(std::cout, "IMG_Init");
	SDL_Quit();
	return 1;
}


We then open a window and renderer the same as we did in lesson 2.

Loading the Textures

Since this lesson is to demonstrate loading PNGs we’ll be using new images for our background and foreground. We’ll also demonstrate that PNG transparency is preserved by using a foreground image with a transparent background drawn on top of a tiled background. Grab them both below.

Background Tile
Foreground

The images are loaded exactly the same with our loadTexture function. Be sure to update the file paths to match your project structure.

const std::string resPath = getResourcePath("Lesson3");
SDL_Texture *background = loadTexture(resPath + "background.png", renderer);
SDL_Texture *image = loadTexture(resPath + "image.png", renderer);
//Make sure they both loaded ok
if (background == nullptr || image == nullptr){
	cleanup(background, image, renderer, window);
	IMG_Quit();
	SDL_Quit();
	return 1;
}


Tiling the Background

Since our tiles are much smaller now we’ll need a lot more than 4 to cover the entire screen and typing their positions out by hand would be a real pain. Instead let’s generate the tile draw positions to fill the screen by using some math!

We can determine how many tiles each row will need by dividing the SCREEN_WIDTH by the TILE_SIZE. To determine the number of tiles per column we can do the same thing but for the SCREEN_HEIGHT. Since we’ll be filling a square area of tiles the total number of tiles will be tiles_per_row * tiles_per_col. We could use a single for loop to run through all the tiles, or nested for loops to fill each tile in a row for each row. I’ve chosen to go with a single loop.

In the loop we compute the x and y indices of the tile to determine where it should be placed. Since we’re drawing row by row in this method the x index will repeat each row, while the y index will increment after each row is filled and we move down to the next row. Thus we can calculate the x index using the absolute tile index modded with the number of tiles per row: x = tile_idx % tiles_per_row.

For example, if we were drawing a 2x2 grid of tiles we’d expect tile 0 to have the same x index as tile 2, which with this method it will: 0 % 2 == 0 and 2 % 2 == 0.

The y index should increase after an entire row of tiles has been placed, so every tiles_per_row tiles. Since we’re using integers we can take advantage of integer truncation and compute this as y = tile_idx / tiles_per_row. So on our 2x2 grid example: row 0 will have tiles 0 and 1: 0/2 == 0 and 1/2 == 0, and row 1 will have tiles 2 and 3: 2/2 == 1 and 3/2 == 1 giving the correct y indices.

All that’s left to do is convert the indices into the pixel coordinates of the tile, which is done by multiplying the x and y indices by the tile size, and our tiling loop is done!

Note: All of this rendering code will be placed within our main loop, similar to lesson 1.

//Determine how many tiles we'll need to fill the screen
int xTiles = SCREEN_WIDTH / TILE_SIZE;
int yTiles = SCREEN_HEIGHT / TILE_SIZE;

//Draw the tiles by calculating their positions
for (int i = 0; i < xTiles * yTiles; ++i){
	int x = i % xTiles;
	int y = i / xTiles;
	renderTexture(background, renderer, x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE,
		TILE_SIZE);
}


Drawing the Foreground

Our foreground image is drawn the same as before, centered in the screen.

int iW, iH;
SDL_QueryTexture(image, NULL, NULL, &iW, &iH);
int x = SCREEN_WIDTH / 2 - iW / 2;
int y = SCREEN_HEIGHT / 2 - iH / 2;
renderTexture(image, renderer, x, y);


We’ll then present the renderer and wait a few seconds before exiting just as we did in Lesson 2.

SDL_RenderPresent(renderer);
SDL_Delay(2000);


Cleaning Up

Clean up is the same as in lesson 2 with one added line to quit SDL_image by calling IMG_Quit.

cleanup(background, image, renderer, window);
IMG_Quit();
SDL_Quit();


End of Lesson

If everything went well you should see this draw to your window.


If you have any issues check your error log and check back through the lesson. Feel free to send an email or tweet for any questions you may have.

I’ll see you again soon in Lesson 4: Handling Events!