SDL2/OpenGL in C: 2 - Rendering from a Tilesheet
Last time we learned how to render images to the game window, but currently we can only render images to the whole screen. Smaller images will only take up as much space as they need, but they will still render to the top left corner of the window. In this tutorial we are going to learn two things. We are going to learn how to pick an image out of a tile sheet or sprite sheet. The current program renders the entire image at once. Tile and sprite graphics often come in single images that contain all of the tiles or sprites for the set. We could break the image up into a bunch of small images, one for each tile/sprite, but this complicates things and is extremely inefficient. Say our program uses 100 tiles. If they are in separate files, that means we will need to loop through loading each individual file, and we will have 100 separate image surfaces, each with its corresponding metadata (height, width, color depth…). All of those metadata instances take up a lot of extra memory, while a single file only has one instance of metadata and requires only one load operation. SDL2 is designed to allow us to grab a small portion of an image and render just that portion, to make it easy and efficient to use tile and sprite sheets. The second thing we will learn to do is put the image at the exact position where we want it in the window. These two things pave the way for laying out scenes and for doing motion and animation.
In the last tutorial, we downloaded a tile sheet that contains a bunch of tiles along with an example image constructed from those tiles. Using this as a tile sheet would be a bit of a pain, because we don't want to use the example image as tiles, and it is displacing the actual tiles. In the comments of that post on OpenGameArt.org, someone took that tile sheet, removed the example image, and added some additional tiles to complete the set. Let's download that and use it instead, as it will use less memory and will be much easier to produce a mapping for. You can download it from this link. Save it in the assets subdirectory of the project directory. You might notice that the filename is very similar, but instead of the "1", it has a "0". This will make it easy to switch which image we are using in our code.
Now that we have the graphics we want to use, let's make a new C file for this tutorial. We can continue to reuse our previous code for now, so copy render.c to tiles.c.
cp render.c tiles.cWe also need to add our new program to the Makefile. Open the Makefile in an editor. To the all rule, add tiles.exe to its dependencies so that it looks like this.
all: test.exe render.exe tiles.exeNow, below the rule for render.exe we need a new rule for tiles.exe. It should look exactly like the rule for render.exe, except "render" should be replaced with "tiles" everywhere in the rule.
tiles.exe: tiles.c
gcc tiles.c -w -Wl,-subsystem,windows -lmingw32 -lSDL2main -lSDL2 -lSDL2_image -o tiles.exeAt this point you can test the new Makefile rule by running make. It should compile and leave you with tiles.exe, which currently does exactly the same thing as render.exe. Here’s what the full Makefile should look like now.
all: test.exe render.exe tiles.exe
test.exe: test.c
gcc test.c -w -Wl,-subsystem,windows -lmingw32 -lSDL2main -lSDL2 -o test.exe
render.exe: render.c
gcc render.c -w -Wl,-subsystem,windows -lmingw32 -lSDL2main -lSDL2 -lSDL2_image -o render.exe
tiles.exe: tiles.c
gcc tiles.c -w -Wl,-subsystem,windows -lmingw32 -lSDL2main -lSDL2 -lSDL2_image -o tiles.exe Now that we've got the build automation sorted for our new program, let's edit it! The very first thing we need to do is change our image loading command to load the new image. Find the line where IMG_Load() is called and change the 1 in the filename to 0, so that it looks like this.
SDL_Surface* image = IMG_Load("assets/generic_platformer_tiles_0.png");Directly under that, we are going to create a basic tile mapping. Note that there are many ways we could do this. We could create a function that takes a tile index and generates the mapping to it on the fly. This will use less memory, but it will significantly increase processing time during rendering. On a device with limited memory, this might be the best option, but on a modern desktop or laptop (or even tablet or smartphone), the rendering speed is far more likely to be the limiting factor than memory space, so we are going to use a static mapping.
Before we can write up the tile mapping, we need to understand how it needs to be formatted. Looking at the documentation for SDL_BlitSurface(), we don't find any information about the arguments for the function. Instead it indicates that SDL_BlitSurface() is an alias for SDL_UpperBlit(). So let's look at that. You can find the documentation for that here. We already know that the first and third arguments are pointers to SDL_Surface structs. In the previous tutorial we set the other two arguments to NULL. This time we will want to use these arguments. They are named srcrect and dstrect. The first indicates what portion of the source surface we want to copy, and the second one indicates where in the destination surface we want to copy it to. The type for both of these is (pointers to) SDL_Rect. We can find the definition for that right here. SDL_Rect is a struct type that contains four integers, named x, y, w, and h. An SDL_Rect instance defines a rectangular region, by upper left corner as well as width and height. So, say we want to create an SDL_Rect that maps to the first tile in our tile sheet (the upper left tile). We would set x and y both to 0, and we would set w and h to the width and height of one tile. For the second tile (the one directly to the right of the first), we would set x to one tile width instead of 0, and the rest would be the same. The SDL_Rect type is very simple and straightforward, and SDL2 has a bunch of useful functions that use this type for things like collision detection.
There's only one piece of information missing now, and that is the size of the tiles in our tile sheet. There are a number of ways you can figure this out. If there's no other option, you can always open the file in a graphics editor, zoom in, and manually count pixels, but this is a pain, and I don't want to do it. Instead, you can open a Windows Explorer window and navigate to the assets directory. Now, right click on the image file and select Properties. In the Properties window, go to the Details tab, and you'll find a section listing the image dimensions. In this case, they are 256x576. Now close that and open the file in Windows image viewer (just double click it). Count the number of tiles horizontally. This tile sheet is 8 tiles wide. 8 tiles divided by 256 pixels gives us 32 pixels per tile. Our tiles are 32 pixels wide. They appear square, but we should make sure they actually are rather than assuming. Counting vertically we find that the sheet is 18 tiles high. 18 tiles divided by 576 is also 32 pixels per tile. Now we know that our tiles are 32x32 in pixels, which is what we needed to produce our mapping.
We are only going to map the top 8 tiles for now. We could write up a for loop to initialize our tile mapping, but we will get into that later.1 We will start by creating an array of SDL_Rects named tiles, with 8 elements. Let's initialize it in the array declaration with the top 8 tiles. Since they are all at the top, y will always be 0. w and h will always be 32, because the tiles are all the same size. Only x will change, and it increments by the tile width (32) each time. Here is the resulting array declaration.
SDL_Rect tiles[8] = {
{.x = 0, .y = 0, .w = 32, .h = 32},
{.x = 32, .y = 0, .w = 32, .h = 32},
{.x = 64, .y = 0, .w = 32, .h = 32},
{.x = 96, .y = 0, .w = 32, .h = 32},
{.x = 128, .y = 0, .w = 32, .h = 32},
{.x = 160, .y = 0, .w = 32, .h = 32},
{.x = 192, .y = 0, .w = 32, .h = 32},
{.x = 224, .y = 0, .w = 32, .h = 32}
};If we wanted to add 8 more, we could copy the initialization lines, paste them right below the current ones, and change y to 32 in all of the new ones (and change the array size to 16 and add a comma to the eighth initialization). That would add the second row of tiles to our mapping. For this tutorial, we don't need those, so we won't do that. Now we can use &tiles[] with the appropriate index for the tile we want as the srcrect argument to render a specific tile.
Next we need to create an SDL_Rect to define where we want the tile to be rendered in the window. I should note that only x and y matter here. Setting w and h to something different from the source rectangle size does not scale the image to that size. The dimension elements of the SDL_Rect for the destination are completely ignored. That said, it might be a good idea to set them to the source image size, just for consistency. (It might also improve code readability.) Inside the outer while loop (our game loop), below the event handling loop, let's create a new SDL_Rect, which we will use to communicate where we want our tile rendered. This should go directly above the call to SDL_FillRect() that we use to clear the buffer. I'm going to have it render our first tile at (1, 1) in tile coordinates (which are in 32x32 pixel increments).
SDL_Rect dest_rect = {.x = 32, .y = 32, .w = 32, .h = 32};This will render the tile one tile height below the top of the window, and one tile width from the left edge of the window. Note that you don't have to render tiles in 32 pixel increments. If you want them to tile the window space neatly though, you will probably want to.
Now for using our SDL_Rects. A few lines further down you'll file the call to SDL_BlitSurface(). The first argument is image. The second one is NULL, but we want to replace that with an SDL_Rect mapping to one of the tiles in our tile sheet. Replace that NULL with &tiles[], and then put an index between 0 and 7 in the brackets. That index will choose which tile is rendered. The third argument is screen, and the fourth is another NULL. Replace that NULL with &dest_rect. Here's what it should look like.
SDL_BlitSurface(image, &tiles[0], screen, &dest_rect);At this point, you can save the file, build with make, and run ./tiles.exe, and you should see the tile rendered neatly in the window at the specified location.
Let's render a second tile. I'm going to reuse dest_rect here. I can see no reason right now to maintain a full mapping of all tile positions on the screen, so we can just change the x and y values in our existing SDL_Rect and reuse it in a second call to SDL_BlitSurface(). Here's an example of doing that. Put in whatever values you want and add this directly below the first blit.
dest_rect.x = 256;
dest_rect.y = 128;
SDL_BlitSurface(image, &tiles[3], screen, &dest_rect);You can add even more of these if you want. You can change the index used in the second argument to change which tile is rendered, and set the x and y elements of dest_rect to choose the position that it is rendered at.
Here is the complete code for this tutorial.
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <stdio.h>
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600
SDL_Window* window = NULL;
SDL_Surface* screen = NULL;
int main(int argc, char* args[]) {
// Initialize video system
SDL_Init(SDL_INIT_VIDEO); // Returns 0 on success
window = SDL_CreateWindow("SDL2 Tutorial",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
SCREEN_WIDTH,
SCREEN_HEIGHT,
SDL_WINDOW_SHOWN); // Returns NULL on fail
screen = SDL_GetWindowSurface(window);
// Load image (returns NULL on fail)
SDL_Surface* image = IMG_Load("assets/generic_platformer_tiles_0.png");
SDL_Rect tiles[8] = {
{.x = 0, .y = 0, .w = 32, .h = 32},
{.x = 32, .y = 0, .w = 32, .h = 32},
{.x = 64, .y = 0, .w = 32, .h = 32},
{.x = 96, .y = 0, .w = 32, .h = 32},
{.x = 128, .y = 0, .w = 32, .h = 32},
{.x = 160, .y = 0, .w = 32, .h = 32},
{.x = 192, .y = 0, .w = 32, .h = 32},
{.x = 224, .y = 0, .w = 32, .h = 32}
};
SDL_Event e;
uint8_t run = 1;
while (run) {
// Handle events
while (SDL_PollEvent(&e)) {
if(e.type == SDL_QUIT)
run = 0;
}
SDL_Rect dest_rect = {.x = 32, .y = 32, .w = 32, .h = 32};
// Render graphics
SDL_FillRect(screen, NULL, 0x00000000);
SDL_BlitSurface(image, &tiles[0], screen, &dest_rect);
dest_rect.x = 256;
dest_rect.y = 128;
SDL_BlitSurface(image, &tiles[3], screen, &dest_rect);
SDL_UpdateWindowSurface(window);
}
SDL_FreeSurface(image);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}There are enough tiles in that top row to build floating islands two tiles wide. Try building a few. There are also enough tiles at the top to build longer regions of ground at the bottom of the window.2
Next we will learn to do some input handling, so that users can interact with our programs. We won't get into full animation at this point, but we will need some kind of feedback so that we can see that our input is being handled, and that will involve moving some graphics around on the screen in response to our input. For now, play around with what we done so far. Maybe you can figure out how to make tiles move around without user input on your own!
Note that this is a tradeoff. Initializing with a for loop will take more CPU time during startup/loading, while initializing the way that I'm about to show you will make the executable larger, taking up a bit more memory. Again, on modern non-embedded systems, memory is likely to be the less scarce resource, potentially making this far better than the for loop.
If you want everything to line up neatly, you'll have to change the window dimensions to be multiples of 32. Remember that those are defined near the top of the file. 800 is already a multiple of 32, but 600 isn't. 640 is though, as are 608 and 576, so any of those would work.

