Aschenblog: Thoughts on Code and Fabrication

Simple Isometric Tiling

In this post I share a few simple javascript examples of isometric tiling commonly used to create the illusion of 3D graphics in games.

For years, side-scrolling video games use a projection where the camera is aligned along one of the axes. Many original game titles like Tetris, Zelda, Super Mario Bros place the camera either above the player looking down along a vertical axis or looking directly from the side. Basic techniques like shading and parallax scrolling (where foreground images scroll faster than background images) help to provide a sense of depth.

An isometric projection is a popular way of visualizing 3D objects on a 2D screen. This involves rotating the camera 45 degrees to one side and then angling down roughly 30 degrees. This approach is used in several role playing and strategy games (Sim City 2000 pictured below). Q*bert, released in 1982, was perhaps one of the first games that used isometric graphics.

Drawing the Grid

A common pattern is to use tiles that are two times wider than they are tall. Also, I find that lines with a slope of 2:1 (two pixels horizontally : one vertical) look better as pixel art. This ratio makes it relatively easy to calculate the screen position of a tile and to find the position of the mouse over a tile.

Here is a simple example with 128x64 tiles on a HTML5 canvas:

This snippet describes how to position the diamond / rhombus shapes in a grid (full source):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tileColumnOffset: 64,
tileRowOffset: 32,

redrawTiles: function() {
  for(var Xi = 0; Xi < this.Xtiles; Xi++) {
    for(var Yi = 0; Yi < this.Ytiles; Yi++) {
      var offX = Xi * this.tileColumnOffset / 2 + Yi * this.tileColumnOffset / 2 + this.originX;
      var offY = Yi * this.tileRowOffset / 2 - Xi * this.tileRowOffset / 2 + this.originY;

      // Draw tile outline
      var color = '#999';
      this.drawLine(offX, offY + this.tileRowOffset / 2, offX + this.tileColumnOffset / 2, offY, color);
      this.drawLine(offX + this.tileColumnOffset / 2, offY, offX + this.tileColumnOffset, offY + this.tileRowOffset / 2, color);
      this.drawLine(offX + this.tileColumnOffset, offY + this.tileRowOffset / 2, offX + this.tileColumnOffset / 2, offY + this.tileRowOffset, color);
      this.drawLine(offX + this.tileColumnOffset / 2, offY + this.tileRowOffset, offX, offY + this.tileRowOffset / 2, color);
    }
  }
}

When we use actual graphical tiles redrawTiles, the draw order will need to be changed so that tiles in the back are rendered before ones in the front. Let’s extend the work by adding colors, coordinates and some mouse listeners:

Here is the salient code that handles canvas drawing (full source):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
redrawTiles: function() {
  for(var Xi = (this.Xtiles - 1); Xi >= 0; Xi--) {
    for(var Yi = 0; Yi < this.Ytiles; Yi++) {
      this.drawTile(Xi, Yi);
    }
  }
},

drawTile: function(Xi, Yi) {
  var offX = Xi * this.tileColumnOffset / 2 + Yi * this.tileColumnOffset / 2 + this.originX;
  var offY = Yi * this.tileRowOffset / 2 - Xi * this.tileRowOffset / 2 + this.originY;

  // Draw tile interior
  if( Xi == this.selectedTileX && Yi == this.selectedTileY)
    this.context.fillStyle = 'yellow';
  else
    this.context.fillStyle = 'green';
  this.context.moveTo(offX, offY + this.tileRowOffset / 2);
  this.context.lineTo(offX + this.tileColumnOffset / 2, offY, offX + this.tileColumnOffset, offY + this.tileRowOffset / 2);
  this.context.lineTo(offX + this.tileColumnOffset, offY + this.tileRowOffset / 2, offX + this.tileColumnOffset / 2, offY + this.tileRowOffset);
  this.context.lineTo(offX + this.tileColumnOffset / 2, offY + this.tileRowOffset, offX, offY + this.tileRowOffset / 2);
  this.context.stroke();
  this.context.fill();
  this.context.closePath();

  // Draw tile outline
  var color = '#999';
  this.drawLine(offX, offY + this.tileRowOffset / 2, offX + this.tileColumnOffset / 2, offY, color);
  this.drawLine(offX + this.tileColumnOffset / 2, offY, offX + this.tileColumnOffset, offY + this.tileRowOffset / 2, color);
  this.drawLine(offX + this.tileColumnOffset, offY + this.tileRowOffset / 2, offX + this.tileColumnOffset / 2, offY + this.tileRowOffset, color);
  this.drawLine(offX + this.tileColumnOffset / 2, offY + this.tileRowOffset, offX, offY + this.tileRowOffset / 2, color);

  if(this.showCoordinates) {
    this.context.fillStyle = 'orange';
    this.context.fillText(Xi + ", " + Yi, offX + this.tileColumnOffset/2 - 9, offY + this.tileRowOffset/2 + 3);
  }
},

Determining the location of the mouse over a canvas tile (tileX, tileY) is determined by a few calculations in a mouse move listener:

1
2
3
4
5
$(window).on('mousemove', function(e) {
  e.pageX = e.pageX - self.tileColumnOffset / 2 - self.originX;
  e.pageY = e.pageY - self.tileRowOffset / 2 - self.originY;
  tileX = Math.round(e.pageX / self.tileColumnOffset - e.pageY / self.tileRowOffset);
  tileY = Math.round(e.pageX / self.tileColumnOffset + e.pageY / self.tileRowOffset);

Graphical Tiles

Open Game Art has over a hundred free tile sets worth investigating. However, Kenny.nl provides a handful of professional looking isometric tile sets. The quality is just excellent. He provides both individual isometric tiles and sprite sheets. XML metadata is also provided that indicates tile location and size in the sprite sheets.

The code is very similar between the last two examples. A few small tweaks are needed to ensure that tile height is consistent. Also, using tile sprite sheets would result in a performance enhancement due to the fact that each image is loaded individually with the current implementation. However, the goal here was to keep things as simple as possible.

The main Javascript files are:

  • isometric.js which initializes the map and handles rendering and event handling
  • map.js which stores a 2D array of map data and an array of image locations.

The source code and images for this are available on Github.

Additional Resources

Comments