Introduction To Isometric Engines
by Jim Adams
______ ____ _____ __ __ ____ _____ ____ ______ _____ /_ __/ / ___/ / __ / / /_/ / / __/ /_ _/ /__ / /_ __/ / ___/ / / /_/__ / / / / / / / / /_ / / //_// / / / / _/ /_ ___/ / / /_/ / / / / / / /_ / / / __ \ _/ /_ / /__ /____/ /____/ /_____/ /_/ /_/ /___/ /_/ /_/ /_/ /____/ /____/ __ __ ______ ___ _ _ ____ / / / / /_ __/ / __/ // // / __/ / /_/ / / / / /_ // // /_/__ \ / _/ /_ / /_ / // / __/ / \/ /____/ /___/ /_/\_/ /____/

Isometric Views
Explanation and Implemention
Tile and Sprite drawing in an Isometric View, Second Edition
By Jim Adams of Game Developers Network, Inc. (Jun 7,1996).
Copyright © 1996 by Jim Adams, All right reserved.
Graphics Illustrations by Lennart Steinke (Sep 1997)

The author, Jim Adams, gives full permission to duplicate
this file only for personal use. No part of this file
may be published without prior written permission by the author.

Notes:

Isometric can mean a multitude of view angles, but we are discussing the one made popular from games like Ultima and XCOM to name a couple.

All examples are not optimized for speed, but in a way to easily understand the concept. All improvements are left up to the reader. Please do not flood me with mail on how to improve the tile drawing routines and such, as I already know how to.

This file has an acompanying .ZIP file (ISO_SRC.ZIP) that contains the Isometric drawing engine with a sample program using it. This also contains some great libraries that you can compile using either BORLAND or WATCOM. (See 'library.txt' in LIBRARY.ZIP)


If you don't already know about tiled graphics, here it is in a nutshell. Sections of pixels, usually a rectangle, compose a tile, much like a floor tile. When you place these tiles together, they form a pattern. It is possible to take a tile with a brick pattern and put them together to create a bigger tile pattern.

So instead of storing raw bitmaps, you just use a map array to store the number of the tiles to draw to form the bigger picture. A typical drawing function would start at the top-left corner of the screen, moving right until the right edge is reached, then moving down a row to start again.

No on to the Isometric view type. Instead of using rectangular tiles, they are angled. When you draw them, instead of x going left to right and y going top to bottom, x now goes down-right and y goes down-left. The map is still left to right as x, top to bottom as y.

Take a look: (The x and y are map cords)

Now remember our display (video screen) is still rectangular, so a typical scene would look something like:

We achieve the view by using angled tiles. These tiles have width, height and depth. As the viewing angle depends on the width and height of the tile (which give us depth), we need to draw them using a certain ratio. So depth is not involved in the drawing, as we only need to worry about width and height.

A good angle to view uses a 2:1 ratio. This means for every two horizontal pixels drawn, there is one vertical pixel. We'll actually be using a 2.1:1. Our tile width will be 32, so we quickly figure our height is 32/2.1=15.23. So our final tile dimensions are 32x15.

This is the 'base' tile size with a height of 1. Remember, our tiles can have different heights. So a wall may be 32x90. The height doesn't change anything, but the width must stay as 32.

Let's take a look at the tile shape (in pixels):

1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 ---------------------------------------------------------------- 1| O O O O 2| O O O O O O O O 3| O O O O O O O O O O O O 4| O O O O O O O O O O O O O O O O 5| O O O O O O O O O O O O O O O O O O O O 6| O O O O O O O O O O O O O O O O O O O O O O O O 7| O O O O O O O O O O O O O O O O O O O O O O O O O O O O 8| O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O 9| O O O O O O O O O O O O O O O O O O O O O O O O O O O O 10| O O O O O O O O O O O O O O O O O O O O O O O O 11| O O O O O O O O O O O O O O O O O O O O 12| O O O O O O O O O O O O O O O O 13| O O O O O O O O O O O O 14| O O O O O O O O 15| O O O O

If you play with the shape a bit, you'll notice they piece together easily. Just draw one, go right 16 pixels, down 8, and draw another. But how do you know what block to draw where? Take a look at the image below, this time the map x and y cordinates are added: (top number is x, bottom is y):

Now it may seem like we want to draw down and right, but no. This is not the best way to do this. In fact, we still want to drawn left to right, top to bottom. What? I thought you said not to do it this way.

Well, it's a bit different. When we draw left to right, our tiles are spaced 32 pixels apart. As we move top to bottom, we only move 8 pixels at a time. Every other row, we pre-step 16 pixels left to make them piece together correctly.

So we'll draw the screen like this: (the numbers are the order in which the tiles are drawn)

Because some of the tiles can be 'cut' be the edges of the screen, we clip them. You'll notice every other row we are drawing one more tile. This is because these are the tiles pre-stepped left and we need to compensate for this.

Got it? While it's easy to draw like this, we certainly can't update the map cordinates this way. So how do we do it? Well, take a quick look back at the zoom in with the map cords. Watch the x and y cords as you move right. You'll see that the x is increase by one and the y is decreased by one for every tile. It's a bit different for top to bottom. Since we are pre-stepping every other vertical tile like:

\ 0,0 \ MAP CORDS: 1,0 <---- increase x / 1,1 <-- increase y \ 2,1 <---- increase x / 2,2 <-- increase y

We will need to alter the addition of the x and y for every other vertical tile. What this means is if the vertical tile counter is even, we increase the map x when we move down. If the vertical tile counter is odd, we increase the map y when we move down.

So now we know how to draw the screen and how to track the map cordinates for each tile drawn. Now the hard part, putting this to work in a program.

Using your favorite map storage method (fixed array, variable array, link list, etc) we'll create a simple drawing function. For ease of explanation, I'll use a fixed array.

We'll use a 10x10 map array, with the ability to stack tiles on one another each with a different height. This gives us the ability to combine graphics tiles to create new ones.

For instance we want a wall and a wall with a candle. Instead of create two wall graphics tiles, we create one wall and one with a candle. Now you just draw the wall, then draw the candle on top of it. If you want the candle on something else, just draw over it.

So we need to set aside an array that holds the number of different tiles and heights. In C this would be:

struct MAP_STRUCTURE { char num_tiles; char tiles[10]; // assuming a max of 10 tiles per map cord char height[10]; // also assuming a max of 10 };

and an array for our map:

MAP_STRUCTURE map[10][10];

We want three different objects in the map:

    0) grass
    1) wall
    2) a tall wall

Look at our sample map:

0 1 2 3 4 5 6 7 8 9 0 O O O O O O O O O O 1 O . . . . . . . . O 2 O . . . . . . . . O . = grass (0) 3 O . . o o o o . . O o = wall (1) 4 O . . o . . o . . O O = tall wall (2) 5 O . . o . . o . . O 6 O . . o o o o . . O 7 O . . . . . . . . O 8 O . . . . . . . . O 9 O O O O O O O O O O

Now we have to define how the graphics tiles look. Well, the grass is easiest, just a 16x15 tile drawn with a grass pattern. The wall is a tile 16x50. Since we're going to use a stacked method of drawing, the tall wall will be two walls, one higher than the other.

So now we put the data in our map array as: (tile 0 = grass, tile 1 = wall)

map[0][0].num = 2; map[0][0].tile[0] = 1; map[0][0].height[0] = 0; map[0][0].tile[1] = 1; map[0][0].height[1] = 50; ... map[1][1].num = 1; map[1][1].tile[0] = 0; map[1][1].height[0] = 0; ...

And you get the idea. Our drawing loop will now go through each array in the map, using num to draw that many tiles there.

Also, since every tile can be a different size for both height and width, we need to create a handle position that is the same for all tiles. The bottom-right corner would do just fine. Just subtract the width and height from the screen x and y position before drawing it.

In C, it would look something like:
(NOTE: This is not an Isometric drawing method. It's just to get you to understand the stacked drawing method.)

for(i=0;i < 10;i++) { for(j=0;j < 10;j++) { for(k=0;k < map[i][j].num;k++) { tile_to_draw = map[i][j].tile[k]; height_to_draw = map[i][j].tile[k]; width = block_width(tile_to_draw); height = block_height(tile_to_draw); block_draw(tile_to_draw,x-width,y-height-height_to_draw); } } }

All you have to do is apply the mapping cordinates as learned from above to get the Isometric view. You'll notice when you draw from top to bottom, left to right, it automatically covers up all tiles that are further away. There's no need for anything like a zbuffer or such.

To draw the map Isometric, follow this:

Start by setting the current screen x and y cords to 8,16. Remember to subtract the block width and height from those cords before you draw the tile.

The map cords are passed to the drawing function as the top-left tile to be drawn. We now start a loop for every vertical tile to be drawn. A 320x200 screen can draw 25 tiles vertically. We will actually need to draw more, as higher blocks would be clipped off, so we'll draw about 35 vertically.

We now setup some temporary cords that will hold the map cordinates (as they will get messed up). We now check if the vertical loop is an odd number, and if it is, pre-step the screen x left 16 pixels. Now start a loop that draws horizontally. There are 12 tiles that can be drawn across (13 for every other vertical line). Pull the tile to draw from the map.

Draw the tile/tiles using the structure method above. Move right 32 pixels, add 1 to the temp map cords, subtract 1 from the temp cords. Finish horizontal loop. Now move down 8 pixels. If this is an even vertical line, add one to map x, otherwise add one to map y. Finish the vertical loop. Voila, The screen is now drawn.

After you get a working engine, you'll notice that when you move in any direction, it 'jump's by 32 or 16 pixels. There's no 'smooth' scroll. Well, this is VERY simple to change. We still have a 10x10 map like:

-X- 0123456789 | 0 .......... Y 1 .......... | 2 .......... 3 .......... 4 .......... 5 .......... 6 .......... 7 .......... 8 .......... 9 ..........

but now, we increase the size inside a map cord to 16x16:

X0 X1 ................ ................ ................ ................ ................ ................ ................ ................ ................ ................ ................ ................ Y ................ ................ 0 ................ ................ ................ ................ ................ ................ ................ ................ ................ ................ ................ ................ ................ ................ ................ ................ ................ ................

The map still stays 10x10, but we can move around inside each cord while still saying inside it. This gives us a range of 160x160. We call this a fine cordinate system.

One thing to note: we cannot work with odd numbers using the fine cords as it creates some jumpy movement. Just try it later and see for yourself.

Now we make the following changes to our engine:

  • Take the x and y position that we want to be drawn as the top-left on the screen. These can range from 0-160. We'll call these vx and vy.

  • Divide these numbers by 16 to get the map cords we're standing in. We'll call these mx and my:
    • mx = px / 16
    • my = py / 16

  • We need to setup some variables to pre-step our tiles we draw to give the smooth-scroll effect, which we'll call prestep_x and prestep_y. We also need some temporary variable we'll call x_off and y_off. Calculate these by:
    • boolean "and"ing vx and vy by 15:
      x_off = x and 15
      y_off = y and 15


    • we calculate the presteps like:
      prestep_x = x_off - y_off
      prestep_y = (x_off / 2) + (y_off / 2)

      (these function are dependent on your tile size, in out case 32x15)

We now have our prestep cords. When we are ready to draw a tile by pulling it from the map using mx and my, we step left by prestep_x pixels and up by prestep_y pixels.

Ok, we now have a smooth-scroll. Here is where a problem comes up - sprites. You can't draw a sprite using these techniques as they may not line up on a 16x16 map boundry. A sprite may be overwritten by another tile being drawn.

So we want to the ability to draw our sprites at any cordinate. What we need to do is add the ability for our engine to draw in layers.

This will give it a tile drawing order. You draw flat ground objects such as grass first. Then go back and draw what's above that and so on. Note that large objects such as the tall wall need to be on one layer as they are considered one object.

It seems hard to think about, but it is really necessary if you don't want to maintain a zbuffer. Just work with it a bit and you'll finally understand it.

So we now have to add a layer to our structure:

struct MAP_STRUCTURE { char num_tiles; char tiles[10]; // assuming a max of 10 tiles per map cord char height[10]; // also assuming a max of 10 char layer[10]; // ditto }; MAP_STRUCTURE map[10][10]; now add the following to the map data: map[0][0].layer[0] = 1; map[0][0].layer[1] = 1; ... map[1][1].layer[0] = 0; ...

Now use a loop like this to draw: (in C)

current_layer = 0; max_layers = 0; while(1) { for(i=0;i<10;i++) { for(j=0;j<10;j++) { for(k=0;k max_layers) max_layers = map[i][j].layer; } } } current_layer++; if(current_layer >= max_layers) break; }

Now all you have to do is link in the map cordinate and screen cordinate of the sprite you want to draw on a certain layer and draw it. You can do this by comparing the current tile cord being drawn with the sprites map x and map y:
(spritex & y are map fine cords of sprite to be drawn)
(mx and my are the current map cord that is being drawn)

if(mx == sprite_x / 16 && my == sprite_y / 16) {

Then you just offset the sprite before drawing it at that position:

xo = sprite_x & 15; yo = sprite_y & 15; xx = xo - yo; yy = (xo/2) + (yo/2); block_draw(sprite_num,screenx-32+xx,screeny-16+yy);

Well, I'll leave you now to your newfound knowledge of Isometric views. If you have any questions or comments, please EMAIL or snail mail me.

      Jim Adams
      Game Developers Network, Inc
      1200 N Lamb Ste#124
      Las Vegas, NV 89110
      EMAIL: tcm@accessnv.com

Reprinted with permission

Discuss this article in the forums


Date this article was posted to GameDev.net: 9/16/1999
(Note that this date does not necessarily correspond to the date the article was written)

See Also:
General

© 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
Comments? Questions? Feedback? Click here!