Tiles and You!
So maybe you have or have not heard about tiles within the context of video games. For those of you who have, great, tread onwards into the more advanced areas of the article. But to those of you who have not, take some time to let the information below soak in. I strongly suggest that you spend time implementing and testing some solutions of your own in throwaway projects just to see the results in real time and for yourself. Many people are hands on learners and doing a lot of the experimentation yourself goes a long way to understanding the concepts.
I plan to make a few articles about various topics in tiles, tile maps, rendering, and interacting. Part 1 will focus on the concepts behind two popular formats of tile maps and rendering techniques. Part 2 will delve more deeply into rendering isometric tile maps with an explanation of the differences between world space and screen space. Finally, in Part 3, I will discuss topics such as isometric tile picking and moving entities within an isometric tile space.
Before all that though, let’s begin with the basics, shall we?
Cartesian Tile Maps
Maps are often stored and displayed in equally sized tiles to make life easier on the renderer and the artists. It is usually easier to begin understanding a tile map in an oblique projection, which is one of the simplest (but very effective) forms of projecting your tiles. Many popular games from the 80s, 90s, and even 00s take advantage of this projection due to its relatively simple mathematics. For example, in the Legend of Zelda on SNES, tiles were utilized to create structured maps with the ability to quickly re-use art assets and build customizable terrains. Notice how the bushes, rocks, fence posts, and walls can be evenly split into tiles of equal sizes? Can you see how this tile map could easily be stored in a data structure such as an array?
Imagine storing a tile map in a two dimensional (or even one dimensional with some cleverness) array. Each cell of the tile map represents one index into the array with the origin most commonly being [0, 0]. Since each tile has a specific width and height (usually equal, but does not have to be), it is quite simple to render this map by simply using multiplication.
int tilePositionX = tileIndexX * tileWidth;
int tilePositionY = tileIndexY * tileHeight;
draw(texture, tilePositionX, tilePositionY);
Tile at [2,1] with width 32 and height 32
tilePositionX = 2 * 32 = 64
tilePositionY = 1 * 32 = 32
tilePosition = (32, 64)
You can think of this as simply drawing your array to the screen with a specific texture based on the position. It is important to note that the calculated coordinates refer to the default origin of each tile which is the upper left corner. So, when you render your tile, it will be drawn with the upper left corner of the texture at the calculated position.
You can even reverse this simple translation to obtain the tile index based on where the mouse is pointing on the screen. Again, the math is quite simple.
int tileIndexX = tilePositionX / tileWidth;
int tileIndexY = tilePositionY / tileHeight;
Such a reversal (or inverse) is useful when you want to interact with the map through mouse movement or mouse clicking. This is how games highlight certain tiles that your mouse is hovering or allow you to place building on specific hovered tiles.
An Introduction to Isometric View
Another popular form of projection is the often misunderstood isometric projection. If you have played video games in the last 20 years, then you are probably familiar with the term “isometric”. Isometric projection is a form of axonometric projection in which the perspective is rotated to a point where all three coordinate axes are 120 degrees apart.
For the visual learners amongst us, take a look at the gray cube rotated and scaled to an isometric view. To achieve this effect, a normal top-down view of a cube (only one side is visible, a square) with equal width and length is rotated by 45 degrees along the z-axis and then ~35 degrees along the x-axis. Read more about this effect at the linked Wikipedia articles.
Unfortunately, true isometric projection does not go well with pixelated screen rendering. Without going into too much detail, suffice to say that the lines generated with true equidistant angles between the axes appear to be jagged and unpleasant. The red cube represents the more common form of projection seen in video games known as dimetric projection. The difference is that the vertical direction is squashed.
Be warned that there will be times in your endeavors and research regarding projections that many people will use isometric, dimetric, and trimetric terms interchangeably. While technically incorrect, most people will know what everyone is talking about due to the context of the game or pictures under discussion. Try not to get discouraged, but remember the subtle differences, because the math involved is different!
Isometric Tile Maps
The good news about isometric maps is that they are stored exactly the same as our previous oblique example! Literally nothing about the way we store the tile map needs to change. You can still represent the map as a two dimensional tile map with X and Y coordinates. The only difference is in the way you draw that map during your render process.
The first step to rendering an isometric view from your tile map is to first calculate the coordinates as we did in the example above for oblique projection. Next, you will rotate that position by 45 degrees in whichever direction you feel is appropriate (the image depicts a clockwise rotation of 45 degrees). The most common approach to achieving this is via a 2D rotation matrix.
After the rotation, you need to use a scale transformation in the Y-direction by a factor of 0.5. This will effectively cut the rendered image in half in that direction, thus achieving the pseudo-3D effect. Unfortunately, however, using the rotation matrix and scale transformation when individuals tiles need to be fit together with pixel perfect accuracy is not useful. This is due to the fact that rendering can only work on a whole integer basis (it does not make sense to draw only a partial pixel). Linear transformations will achieve very precise calculations, but there will be precision loss when rendering, which will cause gaps between the tiles.
As it turns out, the pixel perfect solution is actually much simpler than using linear algebra. The following function will convert positions of your tile map to an isometric position.
Vector ObliqueToIsometric(float obliqueX, float obliqueY, int tileWidth, int tileHeight)
float isometricX = obliqueX - obliqueY;
float isometricY = (obliqueX + obliqueY) / 2;
return new Vector(isometricX, isometricY);
Yes, it is that simple. In the next article, I will introduce the concepts of world space, screen space, and how to translate between the two. Believe it or not, you already know how to convert from world space to screen space, you just do not know it yet!