Tile maps

Tile maps


Tile maps are a very common tool for making lots of games. The idea behind it is that most levels are made of similar parts. The ground, for example, is likely to repeat itself a lot, with a few variations; there will be a few kinds of different trees repeated many times, and a few items such as stones and flowers or grass will appear many times, represented by the exact same sprite.

This means that using one big image to describe your level is not the most efficient solution size-wise. What you really want is to be able to give a list of all the unique elements and then describe how they are combined to generate your level.

Tile maps are the simplest implementation of this. They add a constraint though; all elements must be of the same size and placed on a grid. If you can work with those constraints, this solution becomes very efficient; that's the reason why so many old games were created with it.

We will start by implementing a very naive version of it and then show, at the end of the chapter, how we can make it faster in most situations without too much work.

To sum up, a tile map is made up of:

  • A series of images (what we call animations in our framework)

  • A bi-dimensional array describing what image goes where

The following figure illustrates this:

In addition to being useful for reducing the size of your game, tile maps offer the following advantages:

  • Detecting collisions with a tile map is very easy.

  • The array that describes how the tile map looks also contains semantic information about the level. For example, tiles 1 to 3 are ground tiles, while 4 to 6 are part of the scenery also. This will allow you to easily "read" the level and react to it.

  • It's very simple to generate random variation of levels. Just create the bi-dimensional array with a few rules, and your game will be different each time the player starts again!

  • Lots of open-source tools exist that help you create them.

However, you have to realize that there are some constraints too:

  • As all the elements composing the tile map have the same size, if you want to create a bigger element, you will have to decompose it into smaller parts, which could be tedious.

  • Even if done with a lot of talent, it will give a certain continual look to your game. If you want to avoid having some blocks that repeat around your level, tile maps are not for you.

Naive implementation

We already know how to create a sprite, so basically what we need in order to create a tile map is to generate the sprites that compose it. Just like gf.addSprite, our gf.addTilemap function will take the parent div, the ID of the generated tile map, and an object literal describing the options.

The options are the position of the tile map, the dimension of each tile, and the number of tiles that compose the tile map horizontally and vertically, the list of animations, and the bi-dimensional array describing the tile position.

We will iterate through the bi-dimensional array and create the sprite as needed. It's often convenient to have places without sprites in our tile map, so we will use the following conventions:

  • If all the entries have zeroes, it means that no sprites need to be created at this place

  • If all the places have a number greater than zero, it means that a sprite with an animation at the index corresponding to this number minus one in the animation array should be created

This is typically a place where you want to create your complete tile map before adding it to the document. We will use a cloned fragment to generate the div tag holding all the tiles and add to it the cloned fragment we used for sprites too. Only once all the tiles are created will we add the tile map to the document.

There is one more subtlety here. We will add two classes to our tiles, one that marks which columns the tile belong to, and another that marks which row it belongs to. Other than that, there are no big subtleties in the code for now:

gf.tilemapFragment = $("<div class='gf_tilemap' style='position: absolute'></div>");
gf.addTilemap = function(parent, divId, options){
    var options = $.extend({
        x: 0,
        y: 0,
        tileWidth: 64,
        tileHeight: 64,
        width: 0,
        height: 0,
        map: [],
        animations: []
    }, options);
    
    //create line and row fragment:
    var tilemap = gf.tilemapFragment.clone().attr("id",divId).data("gf",options);
    for (var i=0; i < options.height; i++){
        for(var j=0; j < options.width; j++) {
            var animationIndex = options.map[i][j];
            
            if(animationIndex > 0){
                var tileOptions = {
                    x: options.x + j*options.tileWidth,
                    y: options.y + i*options.tileHeight,
                    width: options.tileWidth,
                    height: options.tileHeight
                }
                var tile = gf.spriteFragment.clone().css({
                    left:   tileOptions.x,
                    top:    tileOptions.y,
                    width:  tileOptions.width,
                    height: tileOptions.height}
                ).addClass("gf_line_"+i).addClass("gf_column_"+j).data("gf", tileOptions);
                
                gf.setAnimation(tile, options.animations[animationIndex-1]);
                
                tilemap.append(tile);
            }
        }
    }
    parent.append(tilemap);
    return tilemap;
}

That's it for now. This will generate the whole tile map at initialization time. This means that very large tile maps will be slow. We will see at the end of the chapter how to generate only the part of the tile map that is visible.