There are a lot of resources out there to help on hobble together a game using the SDL2 library. Some of them are useful, others are… well… not as useful. In my experience, this has made navigating some of the help topics and tutorials somewhat frustrating and time-consuming. Maybe it’s just me (I am only a mortal, after all), but I think that finding a definitive resource on loading and rendering a Tiled map has definitely fallen into that category.

Goals:

  1. Load and parse the Tiled map (.tmx) from disk using the tmxlite library.
  2. Load the tilesets used by the Tiled map so that they can be rendered.
  3. Calculate the position of each tile in the game world as well as its corresponding sprite on the tileset from the tiles’ GID.
  4. Render the map to the screen.

This is not a comprehensive guide to creating a Tiled engine; but it should give you a basic foundation to start learning how to use Tiled maps in your SDL2 game.

This article assumes that: you are proficient with C++, and that you are already familiar with linking to external libraries with whatever toolchain you’re comfortable with.

As you follow along, feel free to clone this repo that I made to see this code in action. It will provide a lot more context to this tutorial.

Loading and Parsing the Tiled map with tmxlite

The Tiled map format (.tmx) can be saved as XML, CSV, and base64 compression. The heavy lifting of parsing these formats is left to tmxlite (or whatever parsing library you decide to use).

NOTE: Make sure that your Tiled map has the tilesets “embedded” into the map (not saved as a .tsx file). When it’s embedded, the Tiled map will save the relative file path to the image file that was used for the Tiled map instead of a .tsx file. We are going to be loading the image file as a texture later on, so we need to know where it is. To change the tileset to embedded, click the “Embed Tileset” button toward the bottom left of your Tilesets pane.

Loading the Tiled map from disk is pretty easy with tmxlite; our load method for the level can start off pretty simple:

void level::load(const std::string& path, SDL_Renderer* ren) {
    // Load and parse the Tiled map with tmxlite
    tmx::Map tiled_map;
    tiled_map.load(path);
 
    // We need to know the size of the map (in tiles)
    auto map_dimensions = tiled_map.getTileCount();
    rows = map_dimensions.y;
    cols = map_dimensions.x;
    // We also need to know the dimensions of the tiles.
    auto tilesize = tiled_map.getTileSize();
    tile_width = tilesize.x;
    tile_height = tilesize.y;
}

Querying Tileset Information from the Tiled Map

When you made your map, you drew tiles on it from one or more tilesets. Our game needs to know where these tileset images are so that it can load them as a texture to render the map in the game. The tileset information is saved in the Tiled map. Let’s load them:

auto& map_tilesets = tiled_map.getTilesets();
for (auto& tset : map_tilesets) {
    auto tex = assets::instance()
        .load_texture(tset.getImagePath(), ren);
    tilesets.insert(std::pair<gid, SDL_Texture*&gt;(tset.getFirstGID(), tex));
}

We’ll need to store all the tilesets in some data structure, because we’ll be referencing them later on when we examine each Tile’s GID.

You could use a map like I did, or you could create some sort of tuple or struct to contain a collection of tilesets. The important information we need to store regarding each tileset is the firstgid (which is an integer) and the tileset texture.

But why do we care about the first GID?

Let’s assume we have 2 tilesets. Tileset A has 10 tiles in its set, and Tileset B has 15. Tileset A’s tile GIDS are: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10. These are all unique. That means that Tileset A’s firstgid would be 1.

The GID is not reset across tilesets, otherwise it would not be unique. This means that Tileset B’s firstgid would be 11. So, Tileset A has tile GIDs 1-10 and Tileset B has tiles with GIDs 11-26.

Since we’ve stored the firstgid for each tileset, we know that if we find a tile with a GID in the range [1, 10] that it should be drawn from the texture belonging to Tileset A. Similarly, if we come across a tile with a GID that falls in the range [11, 26], we should be drawing from the texture belonging to Tileset B.

In short, we need to know the first GID of a tileset so that we draw the correct tile from the correct tileset.

Collecting Tile Information from Each Layer

It’s time to do what we came here to do. Something about bubble gum. We need to walk through all of our layers (starting at the bottom layer) and collect all of the information we need from our tiles. The rest of our basic level loading logic will go in this loop:

auto&amp; map_layers = tiled_map.getLayers();
for (auto&amp; layer : map_layers) {
// We're only looking to render the tiles on the map, so if
// this layer isn't a tile layer, we'll move on.
if (layer-&gt;getType() != tmx::Layer::Type::Tile) {
continue;
}
 
auto* tile_layer = dynamic_cast<const tmx::TileLayer*&gt;(layer.get());
 
// Grab all of this layer's tiles.
auto&amp; layer_tiles = tile_layer-&gt;getTiles();

Simple enough. After we get the tiles and store them in layer_tiles, we’ll be visiting each x, y coordinate on our level, grabbing the tile that exists in that location from the current layer, and collecting all of the information we need to render the tile.

auto&amp; layer_tiles = tile_layer-&gt;getTiles();
 
for (auto y = 0; y < rows; ++y) {
    for (auto x = 0; x < cols; ++x) {
        ...
    }
    ...
}

Because our game is just a grid, a 2D array, we can visit each cell. This nested for-loop ensures that we visit each x,y coordinate in our level.

or (auto y = 0; y < rows; ++y) {
    for (auto x = 0; x < cols; ++x) {
        auto tile_index = x + (y * cols);
 
        ...

The first thing we have to do inside of our x,y loops is to retrieve information from our vector of tiles. Since a vector is not a 2D array, we have to convert our x,y coordinates into a single number. You’ll see this done sometimes in other programs where they’ve opted to not use a 2D array.

// Grab the GID of the tile we're at.
auto cur_gid = layer_tiles[tile_index].ID;
 
// If the GID is 0, that means it's an empty tile,
// we don't want to waste time on nothing, nor do we
// want to store an empty tile.
if (cur_gid == 0) {
    continue;
}

Now we grab the GID of the tile, and this is the number we need for all of the magic to happen. First thing’s first though, if the GID is 0, that means it’s an empty tile. No need to go through all the trouble for an empty tile. We can’t draw empty. We’ll move on.

auto tset_gid = -1;
for (auto&amp; ts : tilesets) {
    if (ts.first <= cur_gid) {
        tset_gid = ts.first;
        break;
    }
}
 
// If we didn't find a valid tileset, skip the tile. We can't
// render it if we don't have a tileset to pull from.
if (tset_gid == -1) {
    continue;

If it’s not 0, then there’s a tile there. If you recall from the previous section, the tile belongs to the first tileset whose first GID is <= to the tileset’s GID. If we cannot find a tileset that owns that GID, we will skip the rest of the logic with a continue as well.

Dissecting the Tile GID

Since the GID is not 0, there’s we’ve got to do something about it. This is the trickiest part about loading the Tiled map.

if (tset_gid == -1) {
    continue;
}
 
// Normalize the GID.
cur_gid -= tset_gid;

The first thing we do is “normalize” the GID. Remember back to when we were talking about tilesets and the first GID? The unique-ness of the GID means that the GID for tilesets is an increasing sequence, and doesn’t reset per tileset. We’ve already identified which tileset we are working with, so now we can “normalize” the GID.

Going off of the previous example with Tileset A and Tileset B, let’s assume we have a tile with a GID of 23. This falls in the range of Tileset B. For the math that follows to work, we want to convert our range of [11-26] to [1-15]. Tileset B’s first GID is 11, so our current GID: 23 - 11 = 12. So we know from Tileset B, which has 15 tiles, we are working with tile #12 on the tileset.

ur_gid -= tset_gid;
 
auto ts_width = 0;
auto ts_height = 0;
SDL_QueryTexture(tilesets[tset_gid],
        NULL, NULL, &ts_width, &ts_height);

Speaking of the tileset, now we need to know the width and height of the entire tileset. We’re going to be using those dimensions calculate where our tile is located on the tileset. So we ask SDL nicely for that information and store them in ts_width and ts_height.

SDL_QueryTexture(tilesets[tset_gid],
        NULL, NULL, &ts_width, &ts_height);
 
// Calculate the area on the tilesheet to draw from.
auto region_x = (cur_gid % (ts_width / tile_width)) * tile_width;
auto region_y = (cur_gid / (ts_width / tile_height)) * tile_height;
 
// Calculate the world position of our tile. This is easy,
// because we're using nested for-loop to visit each x,y
// coordinate.
auto x_pos = x * tile_width;
auto y_pos = y * tile_height;

Calculating where on the tileset is weird at first, but the math checks out.

For example: at (2, 2), I have a tile with the GID of 190. My first GID is 1. My tileset’s width is 256 pixels, the height is 1450 pixels. My tiles are 32×32.

gid = FirstGID - 1
= 190 - 1
= 189
 
region_x = (gid % (256 / 32)) * 32
= (189 % 8) * 32
= 5 * 32
= 160
region_y = (gid /(256 / 32)) *32
= (189 / 8) * 32
= 23 * 32
= 736

Sure enough, that GID of 189 corresponds to the top-left house tile. This is starting at the top-left of the tileset and working your way across and down.

Rendering the Map

Rendering the map is easy-peasy. In my example, I created a draw() function for the tile struct, so my level’s draw() method looks like:

void level::draw(SDL_Renderer* ren) {
    for (auto& tile : tiles) {
        tile.draw(ren);
    }
}

Conclusion

That’s all there is to it! It might not make sense immediately. Personally, grabbing a pencil and a notepad and working through it while referencing the Tiled map helped a lot.

If you haven’t already, check out the sample code that I put together to accompany this tutorial. It comes with a Tiled map and tileset ready to go. All you have to do is build and run.

The tileset I used in the example Tiled map was the result of Hyptosis’s hard work at opengameart.org. Check out their website or their opengameart profile.