Posts
Search
Contact
Cookies
About
RSS

Loading a TMX map with Raylib (the hard way!)

Added 5 Mar 2021, 1:48 a.m. edited 18 Jun 2023, 1:12 a.m.
https://www.youtube.com/watch?v=iVzXIRFhh7Y

I happened to look inside a TMX file from the rather nice tile map editor (Tiled) I made the mistake of thinking "oh that looks fairly easy to parse...". While there is a method to load TMX files with raylib it uses an external library, I thought hey I can just grab a single header "library" to parse XML and job done. I guess I just needed to scratch that low level programming itch again!

In the process I've ended up improving the xml parser (see the comments) and also made it so it can be used from multiple source units. Its the old routine of having an implementation define in just one of your units.

As I use Tiled more its uses become more apparent for instance why limit yourself to 2D, if you have a 3d terrain, use a tile map to position on the terrain all the 3D features of the level, you can even define polygons for example to act as trigger areas, even adding custom properties to them, anyhow its a nice tool but I digress.

I've kept all the parsing code in a separate unit to make the example really simple and clear. The actual rendering is the end users job, but you can easily access the loaded map through an array of layers each containing a 2D array of cells.

Before going any further I should point out that there is a great deal you can do with Tiled that I haven't bothered to implement and error checking is basically minimal (alright none it will segfault if it doesn't get what it expects!) You'll see from the comments what you can and can't do with maps using this loader. Beware the Kraken!

To load in a map its a simple as

Tmx* tmx = LoadTmx("data/map.tmx");

There are various nested structures within the TMX structure, but between the comments of each of the structures members, and the actual example you should be able to get something working with it fairly quickly. Drawing the whole map is as simple as:

    for (int l=0; l < tmx->numLayers; l++) {
        drawLayer(tmx, l, offsets[l]);
    }

While I have given a simple example for drawLayer, it does do parallax scrolling (only horizontally - the map is deliberatly 1 screen in height!) You could quite quickly make a platformer game with this. One tip would be to keep the players position in grid coordinates by using a decimal fraction you can move smoothly from tile to tile but converting to integers and you can immediately tell what tile you're on, and use that to work out if the player needs to fall for example.

You can also add a number of collision polygons to each tile, which means to check for collisions all you need to do is check against the nearest tiles for collisions, not the whole level, no need to partition your collision shapes into sub level areas, its already done per tile.

Before you grab the code and get hacking, let's take a look at the examples drawLayer function, its not massive and would have been shorter if I hadn't thrown in the parallax effect just for fun (yes I know I have odd ideas of fun!)

void drawLayer(Tmx* map, int l, float offset) 
{
    for (int y=0; y < map->mapHeight; y++) {
        // plus 2 because a part of tile must over lap both screen edges 
        // so you don't see them poping in or out!
        for (int x=0; x < (screenWidth / map->tileWidth)+2; x++) { 

            float xx = (int)offset + x;
            float yy = y;
            
            // wrap the map coordinates inside the layer data.
            if (xx < 1) xx += map->mapWidth;
            if (xx > map->mapWidth-1) xx -= map->mapWidth;
            
            int s = map->layers[l]->cells[(int)xx][(int)yy];

            // so we can scroll in increments smaller than the grid
            xx = x - (offset - (int)offset);
            SpriteSheet* ss = 0;
            
            // TODO at least mask off flip flags from GID...
            // idealy actually use the flags!
            for (int i=0; i < map->numTsets; i++) {
                if (s >= map->spriteSheets[i]->firstGid) {
                    ss = map->spriteSheets[i];
                }
            }
            if (ss) { // probably an unset tile in the layer (normal)
                s -= ss->firstGid;

                if (s>-1) drawSprite(ss, xx * map->tileWidth,
                                           (yy * map->tileHeight) + map->tileHeight,
                                            s, 0, 1.0, WHITE);
            }       
        }
    }
}

As you can see from the start of the highlighted part of the function actually getting the tile ID is very simple you just lift it straight out of a 2D array. There is however a slight wrinkle in that you have to work out which tile sheet (sprite sheets as I ended up calling them, yay for code reuse!) There is possibly a more efficient way to do this - for example the if statement in the loop could do with an else break!

Anyhow there's really not too much more I can say that will be of obvious use, what's needed is just to play around with the example and see what you can come up with.

If anyone adds to the loader I would appreciate a look at what you've done, as its only dependency is a single header "library" who know it could even make it as a Raylib example, and if it gets (a lot) more robust and covers more features maybe it could even be suitable as a new set of Raylib functions, but that all depends on you as to be honest, as it stands it really does everything I need from a level loader...

You can grab the loader source and example here

Enjoy!