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.

constintSCREEN_WIDTH=640;constintSCREEN_HEIGHT=480;//We'll just be using square tiles for now
constintTILE_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(conststd::string&file,SDL_Renderer*ren){SDL_Texture*texture=IMG_LoadTexture(ren,file.c_str());if(texture==nullptr){logSDLError(std::cout,"LoadTexture");}returntexture;}

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
*/voidrenderTexture(SDL_Texture*tex,SDL_Renderer*ren,intx,inty,intw,inth){//Setup the destination rectangle to be at the position we want
SDL_Rectdst;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
*/voidrenderTexture(SDL_Texture*tex,SDL_Renderer*ren,intx,inty){intw,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.

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 TileForeground

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

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
intxTiles=SCREEN_WIDTH/TILE_SIZE;intyTiles=SCREEN_HEIGHT/TILE_SIZE;//Draw the tiles by calculating their positions
for(inti=0;i<xTiles*yTiles;++i){intx=i%xTiles;inty=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.