Heading image for post: Using Tiled and Canvas to Render Game Screens

Design Javascript

Using Tiled and Canvas to Render Game Screens

Profile picture of Shane Riley

A while back, I came across a great application called Tiled that can be used for creating layered scenes using a sprite map, and thought it would be a great way to quickly create scenes in Canvas-based games.

The idea behind this block of code is creating a way to go from Tiled data to the same scene rendered in Canvas, like this one for use in top-down view Javascript games.

To start with, let's grab a nice-looking sprite sheet from our friends at Open Game Art. We'll use a nice Zelda overworld-like sprite sheet. You can check out the Tiled tutorial for how to do the basics. We'll start by creating a new tileset. From the map menu, select new tileset. Find the sprite sheet we grabbed from Open Game Art, give it a name, and leave the default 32 x 32 tile size. You should see the tileset in the bottom right. This gives us the sprite stamps we need to create our scene.

Next, we'll make the layers we'll use to create our scene. Click the add layer icon below the layers pane or choose add tile layer from the layer menu. For this example, I created three layers: forest, mountain, and ground. The ground layer contains any grass or pathways, forest contains trees, tall grass, and small stones. Mountain contains the larger rock formations, including the cave entrance.

Once we've got the scene drawn, we can export the data necessary to render it to a canvas in JSON format. From the file menu, choose export as. From here, we can start writing the code necessary to generate the scene in canvas. We'll start by creating a scene object to hold our layers and methods for rendering them.

var scene = {
  layers: [],
  renderLayer: function(layer) { },
  renderLayers: function(layers) { },
  loadTileset: function(json) { },
  load: function(name) { }
}

Let's create a method to load the scene using jQuery's Ajax method. We'll assume the path will be to the maps directory, and we'll create an argument to pass in the scene name. When we're done loading the JSON data, we'll call the loadTileset method to preload our sprite sheet.

  load: function(name) {
    return $.ajax({
      url: "/maps/" + name + ".json",
      type: "JSON"
    }).done($.proxy(this.loadTileset, this));
  }

Note that we are using jQuery's proxy method to assign the context that the loadTileset method is being run with. We'll receive the same arguments passed to loadTileset as we normally would with the done callback, but this will refer to the scene object.

Why did we use the deferred .done() method rather than the success method in the Ajax object? This allows us to add multiple callbacks on the same Ajax request and manage the callbacks if necessary. Since the load method is returning the deferred object created by jQuery, we can attach any scene-specific callbacks to the returned object when we tell the scene to load.

The loadTileset method is easy enough. Create the tileset image, store a reference to it, then attach an onload event to render the scene's layers.

  loadTileset: function(json) {
    this.data = json;
    this.tileset = $("<img />", { src: json.tilesets[0].image })[0]
    this.tileset.onload = $.proxy(this.renderLayers, this);
  }

We assign the JSON data to the data property of our scene object, create an img DOM element and store it as scene.tileset, then set our proxied renderLayers method as our onload callback. This means our next method of interest will be renderLayers.

  renderLayers: function(layers) {
    layers = $.isArray(layers) ? layers : this.data.layers;
    layers.forEach(this.renderLayer);
  }

I've set up renderLayers to allow an optional argument to specify which layers we should be rendering. This allows us to call the method later and render either a subset of the layers of that scene or a mixture of layers from this scene and others.

Finally on to the bulk of the work, the renderLayer method. Let's start by making sure the layer we've been passed can and should be rendered. If so, we'll set up a scratch canvas to render to for a slight performance improvement. This method assumes you've created a canvas rendering context and assigned it to variable c, like var c = $("canvas")[0].getContext("2d");

  if (layer.type !== "tilelayer" || !layer.opacity) { return; }
  var s = c.canvas.cloneNode(),
        size = scene.data.tilewidth;
  s = s.getContext("2d");

Next we'll check to see if we've previously rendered the layers and stored them as images. If we haven't, we'll start rendering the tiles for that layer to the canvas.

  if (scene.layers.length < scene.data.layers.length) {
    layer.data.forEach(function(tile_idx, i) {
      if (!tile_idx) { return; }
      var img_x, img_y, s_x, s_y,
            tile = scene.data.tilesets[0];
      tile_idx--;
      img_x = (tile_idx % (tile.imagewidth / size)) * size;
      img_y = ~~(tile_idx / (tile.imagewidth / size)) * size;
      s_x = (i % layer.width) * size;
      s_y = ~~(i / layer.width) * size;
      s.drawImage(scene.tileset, img_x, img_y, size, size,
                          s_x, s_y, size, size);
    });
    scene.layers.push(s.canvas.toDataURL());
    c.drawImage(s.canvas, 0, 0);
  }

Our forEach method received the tile ID from the JSON layer object and an iteration index. If the tile ID is 0, there is no sprite to be rendered in that location, and we won't need to proceed further. If there is a tile ID, we'll start into the drawing method by setting our x and y coordinates for the sprite. To properly calculate this based on the tile ID, we'll need to use a 0-based index and as such we'll decrement the tile ID before performing our calculations.

The x and y coordinates of the sprite within the sprite sheet are calculated by thinking of the canvas as a two-dimensional matrix that we write to from left to right, top to bottom. The x value, therefore, is derived from the modulus of the tile ID and the sprite sheet's width divided by the width of the sprite, multiplied by the width of the sprite. As an example, if we were told to use sprite 4 for the current block in the scene matrix, our sprite sheet's width is 512 and our sprite width is 32, we'd have an equation that looks like this:

  img_x = (3 % (512 / 32)) * 32;

We do the same for the y value. The tile ID is divided by the result of the sprite sheet width divided by the size. This gives us the row we're grabbing the sprite from. Multiply that by the sprite size and you get the starting y value of the sprite. To ensure we don't use a floating point number and receive a blurred version of our sprite, we use the double bitwise not operator to drop the decimal places. Depending on your browsers, you may have a slight performance improvement by using the left bitwise shift, img_y = (tile_idx / (tile.imagewidth / size)) << 0 * size, but I've found that more developers know what you're doing with the double not. If both are confusing, pass it in to Math.floor instead.

The sprite's x and y position are now calculated in a similar fashion to the sprite sheet's x and y position. Finally, we get to rendering the portion of the sprite sheet to the canvas. We use the tileset image as the source, set the starting x and y position on the image, the width and height we're taking from that point, then use that cut of the image to draw from s_x, s_y at the same sprite size.

Once we've rendered each of the sprites to the canvas for that layer, we push a base-64 encoded version of the canvas in PNG format to the layers array on our scene object for quick redrawing. Using our scratch canvas as an image, we draw it to the game canvas starting at point 0, 0.

If our layers are already rendered to PNGs, we simply draw them to the canvas.

  else {
    scene.layers.forEach(function(src) {
      var i = $("<img />", { src: src })[0];
      c.drawImage(i, 0, 0);
    });
  }

And that's it! You should now see the same scene you created in Tiled rendered to the canvas, like my example from before. In my example, I'm using a 640 x 480 canvas to make it more like the screen size of the 16-bit era games. You can now use this backdrop for whatever manner of game you're making. I recommend keeping this as a separate canvas layered underneath the game canvas via CSS or exporting the contents of the canvas-rendered scene to a PNG and using that to redraw your scene. This will give you a tremendous performance boost allowing you to do more with your game without experiencing frame rate issues.

There's more that can be done to this to make it more universal, such as turning it into a prototype or giving it a new method to create separate instances (for now I use $.extend) and adding flipped sprite output. Adding options like sprite sheet path, map data path, and scene overrides (modify opacity, sprite dimensions, etc.) will make it easier to control the output. If time and desire permit, I'll be adding these features in later and making a formal repository for it. For now, you can grab the code we've reviewed from this Gist.

More posts about Design Javascript Development

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project