______ ____ _____ __ __ ____ _____ ____ ______ _____
/_ __/ / ___/ / __ / / /_/ / / __/ /_ _/ /__ / /_ __/ / ___/
/ / /_/__ / / / / / / / / /_ / / //_// / / / /
_/ /_ ___/ / / /_/ / / / / / / /_ / / / __ \ _/ /_ / /__
/____/ /____/ /_____/ /_/ /_/ /___/ /_/ /_/ /_/ /____/ /____/
__ __ ______ ___ _ _ ____
/ / / / /_ __/ / __/ // // / __/
/ /_/ / / / / /_ // // /_/__
\ / _/ /_ / /_ / // / __/ /
\/ /____/ /___/ /_/\_/ /____/
Isometric Views
Explanation and Implemention. Second Edition :)
Tile and Sprite drawing in an Isometric View.
By Jim Adams of Game Developers Network, Inc. (Jun 7,1996).
Copyright (c) 1996 by Jim Adams, All right reserved.
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 means 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.
All text in this was typed with a mono-spaced editor (such as edit.com).
Certain programs adjust the width of the font spacing so the 'graphics'
I typed will not look correct. If you're using Windows, please select
a proper font to view it.
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)
Rectangular: Isometric:
- X - 0 0
0123456789 / 1 * 1 \
0 ********** Y 2 * * 2 X
| 1 * ** * / 3 * * 3 \
Y 2 * **** * 4 * * 4
| 3 * ** * 5 * * 5
4 * * * * 6
5 ********** * * * * 7
* * * * * 8
* * * * 9
* *
* *
* *
* *
* *
*
Now remember our display (video screen) is still rectangular, so a
typical scene would look something like:
------------------------
| \ Grass / |
| \ / |
| \ / | (slants show angle of tiles)
| \/ Water |
| Sand \ |
| \ |
------------------------
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
( From now on, when we draw, I'll represent a tile like: /\ )
( \/ )
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.
So, a screen drawn like this would look like:
+-----------+
|/\/\ |
|\/\/\ |
|/\/\/\ |
|\/\/\/\ |
|/\/\/\/\ |
|\/\/\/\/\ |
|/\/\/\/\/\ |
+-----------+
But how do you know what block to draw where? Take a look at the
screen again, this time zoomed in with the map x and y cordinates
added: (top number is x, bottom is y)
+-------------------+
| /\ |
| / 0 \ |
|/\ 0 /\ | Note:
| 0 \/ 1 \ |
| 1 /\ 0 /\ | / \
|\/ 1 \/ 2 \ | Y /\ X
| \ 1 /\ 0 /\ | / /\/\ \
| 1 \/ 2 \/ 3 \ | /\/\/\
| 2/ \ 1 /\ 0 / \ | \/\/\/
+-------------------+
So it looks like this in a map:
-X-
| 0 1 2 3
Y 1 X X X
| 2 X X X
3 X X X
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)
+-----------------+
|0 /\ 1 /\ 2 /\|3
| / \/ \/ |
4|/\ 5 /\ 6 /\ 7 | 8
| \/ \/ \/|
|9 /\ 10 /\ 11 /\|12
|\/ \/ \/ |
13|/\14 /\15 /\16 |17
| \/ \/ \/|
|18 /\19 /\20 / |21
+-----------------+
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<map[i][j].num;k++) {
if(map[i][j].layer == current_layer) {
// draw the tile
}
if(map[i][j].layer > 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
|