In my previous post, I showed an example of a tile map in a viewport, but didn’t go into the details of explaining how the map was generated or loaded. In this post, we’re going to have a look at that.
The map file itself was created using an open source application called Tiled. It’s a neat application, and definitely beats writing out tile maps by hand. If you’re planning to do much work with tile maps, it’s worth checking out.
What is a tile map?
At the most basic level, a tile map is simply a grid which says which graphic goes into which coordinate. This technique has been used in 2d computer games since ever, as it’s very straightforward. In most cases, the grid will be represented as an array (or array of arrays) of numbers, each number representing a specific tile.
Tiles can be individual images, but in most cases, they’ll be grouped in a single image, like you’d do for css sprites. When we render a map, we look at the tile map, find the number of the sprite we’re looking for, and paste it to the screen.
Loading the file
I’ve saved the map to json, so loading it is simply a case of using jQuery’s ajax call and specifying a callback, for example:
$.ajax("https://karlagius.com/files/html5/assets/maps/tilemap-demo-1.json", { dataType:"json", success: function(data) { tmxTilemap = new Map.TmxTileMap(data, { width:5, height:5 }, function() { repaint(); }); tmxTilemap.init(); }});
Reading the tile map
In Tiled’s json output, the tile grids are stored as an array in the data field of the map object; each row follows the other. For example, a 3×3 map looks like this:
[1, 2, 3, 6, 7, 8, 11, 12, 13]
The numbers are 1 based, so if we keep our tiles in a 0 based array, we must remember to subtract 1 from the number to get the right tile.
In my first attempt, I broke this up into a number of arrays based on the width and height of the map:
parseMap: () -> console.log "Reading map" @layers = [] for layer in data.layers map = [] for y in [0..data.height-1] map.push layer.data.slice y*(data.width), (y+1)*(data.width) @layers.push map
Slice copies sections of the array based on width (in this case 3) and the function creates a two dimensional array:
[ [ 1, 2, 3], [ 6, 7, 8], [11, 12, 13], ]
We can reference as follows: @layers[mapNumber][y][x]
While I was writing this, I realized that I only wrote the above because I was thinking in terms of columns and rows. In reality, there’s no need to break the array, as we can easily find the array index of a coordinate:
toGridCoordinate: (x, y) -> (y * @data.width) + x
The idea is that you can find the index of the first item in a row by skipping all the rows that come before it (y * @data.width), and from there you just need to pick the right column.
In the example above, if we want to get the middle square (1,1), we have
(1 * @data.width) + 1 -> (1 * 3) + 1 -> 4
Now we can jump directly to the right index without having to preprocess the map.
Getting the tile images
Ok, so now we can read the tile map, but that’s not going to do us much good without tile images. We need to read the tilesets from the map definition, and turn them into usable icons.
The tile set definitions come in this format:
"tilesets":[ { "firstgid":1, "image":"tiles.png", "imageheight":280, "imagewidth":200, "margin":0, "name":"tiles", "properties": { }, "spacing":0, "tileheight":40, "tilewidth":40 }]
For the purposes of this post, I’m going to ignore margin, spacing, and properties; I laid out my tile image so that all the tiles are together with no spacing. What we need from this definition are the size of the tile (tileWidth and tileHeight), the size of the image (imageHeight and imageWidth), and of course the source of the image (image).
Image loading is asynchronous, which can be a bit of a pain – we shouldn’t really start working before we have the images. We can use the Image object’s onload callback to let us know when the image has loaded (courtesy of html5 Canvas Tutorials):
loadImages: () -> console.log "Loading images" images = {}; loadedImages = 0; numImages = @data.tilesets.length for tileset in @data.tilesets images[tileset.image] = new Image() images[tileset.image].onload = () => if ++loadedImages >= numImages @processTiles images images[tileset.image].src = tileset.image
This counts the number of tilesets, and keeps count of the number of images that have finished loading. Once all the images have loaded, it then proceeds to process the images. Note the fat arrow on the event handler definition. In CoffeeScript, -> is normally used to describe a function. The fat arrow, => is used to bind the event handler to the current scope. This means that the processTiles function will be called in the scope the current instance, not on the window or the event.
Once we’ve loaded the image, we can start to pull the tiles from it. In my first attempt, I had started by cutting up the image into individual tiles, but this is really unnecessary. Since canvas takes source and destination coordinates as a parameter, it can easily grab parts of the image as directed, so we don’t need to create all the additional image objects. What we do need though, is to map every number to an image and coordinates on that image:
processTiles: (images) -> @tiles = [] for tileset in @data.tilesets image = images[tileset.image] horizontal = tileset.imagewidth / tileset.tilewidth vertical = tileset.imageheight / tileset.tileheight for v in [0..vertical-1] for h in [0..horizontal-1] @tiles.push new Map.Tile image, h * tileset.tilewidth, v * tileset.tileheight, tileset.tilewidth, tileset.tileheight
The idea is simple. We calculate the number of rows and columns in the image by dividing the image size by tile size, and then generate the coordinates for each one, storing it along with a reference to the image.
That’s it. Now we can start drawing maps by reading the layers and pasting them onto a canvas. However, there’s a little extra…
Chunking the map
While it’s perfectly possible to draw a map tile by tile, the truth is that calling drawImage for every individual tile is wasteful. That’s 100 calls for a 10×10 display, not counting any layers. To take the edge off this, we can pre-draw some sections of the map into ubertiles, and use them instead. This gives us a much reduced number of calls to draw, though it trades off on memory.
Creating the chunks involved a couple of traps which I had to learn about the hard way, mostly involving data urls, so here comes.
Apart from calculating the size and the number of the chunks we need, we’ll need a canvas to compose the image on. We could use one defined in the markup, but I prefer to create a new one that we can use in the background:
canvas = document.createElement "canvas" context = canvas.getContext "2d" canvas.width = @chunkSize.width * @data.tilewidth canvas.height = @chunkSize.height * @data.tileheight
Now we can iterate over the map, building up each chunk:
for v in [0..verticalChunks-1] for h in [0..horizontalChunks-1] for y in [0..@chunkSize.height-1] for x in [0..@chunkSize.width-1] mapX = x + (h * @chunkSize.width) mapY = y + (v * @chunkSize.height) tile = @tiles[layer.data[@toGridCoordinate mapX, mapY]-1] tile.draw context, x * tile.w, y * tile.h
This determines map coordinates in relation to the chunk, and draws it on the canvas we just created. Now, we need to store the image:
chunkImage = new Image() chunkImage.width = canvas.width chunkImage.height = canvas.height chunkImage.onload = () => if ++completeChunks >= numberOfChunks @callback() chunkImage.src = canvas.toDataURL "image/png" chunks.push { shape: new Geometry.Shape(chunkBounds), image: chunkImage, dimensions: @chunkSize}
The toDataURL method converts the image on the canvas into a base64 encoded png image, which we can store for later reference. It’s important to note that some browsers (notably, Chrome) have a security restriction on this, so it won’t work if you’re running off the local file system. The other important point is the use of the onload event, like we did for the tiles. I hadn’t realized it straight away, but the image object still needs time to load the image, even if we’re handing it right to it. This was causing the screen to grey out sometimes.
Other stuff
This post just scratches the surface of what can be done with Tiled tilemaps, and that’s just one application. There’s plenty more to explore; and remember that so far we’ve just looked at displaying a flat surface!
Tile map seems like a powerful option for developing simple 2d games that can be run on browser. Look forward to see more examples from you. Thanks for sharing the sample.
Thanks for information . If its possible , can I get the source code for this example.
Hi Ashish, and thank you for your message! The source code can be found in the sample for the previous post: https://karlagius.com/2013/03/23/a-2d-viewport-for-canvas/
The file you are looking for is https://karlagius.com/files/html5/assets/scripts/tilemap.js