Welcome! In this article I'll be briefly going over the development of the tile engine that is used in the enclosed game demo, Terran. Hopefully I'll be able to give you a few ideas on developing your own tile engine. If you're new to DirectX you might be a bit fuzzy on how to go about doing such a thing, but I think you'll see that it's actually not terribly difficult once you get used to it.
Before you jump in and start coding, you have to have some idea of what it is you're trying to create. This includes answering questions like, "How many layers will my maps have?", "What size should my maps be?", "Exactly what types of data should my maps contain?", "How am I going to store maps in a file?", and so on. Since you're most likely working on a game, the answers to these questions should be somewhere in your design document. There are any number of ways you can choose to implement your engine, so it's worth your time to consider all sorts of different possibilities and find something that will work the best for what you want to create. As an example, I'll show you what I came up with.
"How many layers will my maps have?"
First let me clear up the question a little bit: by layers I mean layers
of graphics on the screen. The simplest maps have only one layer: the whole map is drawn in one pass, with one tile in each map position. Complicated
maps might have separate layers for backgrounds, background objects, items that can be picked up, foreground objects, foreground effects... I've seen
some engines where I could just swear the person who wrote it was just trying to come up with as many layers as possible. :)
For my maps, I decided on three layers of tiles. The first one to be drawn is background: stuff like grass, water, tree stumps, etc. This first layer basically contains anything that doesn't need to have various backgrounds behind it. The second layer is more background objects, but since a layer has already been drawn, objects in the second layer can appear on various backgrounds. For example, flowers go in the second layer. This way, I can put flowers on all sorts of different backgrounds: grass, shaded grass, pathways, etc. without creating different flowers for each case. The third and final layer of tiles is made of foreground objects. These are things that the player can walk behind, like the very tops of trees, mountains, and houses. All the characters are drawn between the second and third layers, so that they appear to be behind anything that's in the third layer.
Let me show you an example of this to make it a little clearer. This is a small section of a map from the game, drawn one layer at a time so you can see exactly what's going on.
![]() |
Layer 1: Here you see only the tiles from layer 1, which constitute the background for the map. None of these tiles will ever have anything behind them, which is why they are in layer 1. |
![]() |
Layer 2: Now layer 2 is added. Color keys are used for layers 2 and up, so that the tiles don't have to be square objects. Notice how the flower can appear on different backgrounds because it is above layer 1. The second row of the tree is also in layer 2, so that trees can grow near different backgrounds such as grass, coastlines, or other trees. |
![]() |
Characters: Characters are drawn next, so that they appear in front of the first two layers, but behind the third. An important thing to remember here is that characters must be drawn in order from top to bottom so that they occlude each other properly. Hence before drawing characters, Terran sorts all the visible characters by their y-values. |
![]() |
Layer 3: Drawing this layer completes the image. Since the characters have already been drawn, notice how the top of the tree occludes the character to give a sense of depth. Other things that can go in layer 3 are the tops of mountains or houses, the arch above a doorway, etc. So depth is actually relatively easy to implement! |
You can use any number of layers for your game, but I recommend using two at an absolute minimum: one for background and one for foreground. Now let's move on to the other questions I posed in the beginning of this section.
"What size should my maps be?"
For starters, you obviously don't want to make
every map the same size. Your world map will probably not end up being the same size as the inside of a little hut! So the question becomes not picking
an arbitrary map size, but choosing a maximum map size. I would just recommend using the size of your largest map. This is Win32, after all -- you have
a lot more memory to work with than if you've been programming games in DOS.
The only time this might not be a good idea is if you have a lot of layers and some insanely huge maps. Just because you have lots of memory to work with doesn't mean it's infinite, after all. So simply declaring an array to hold a 2000x2000 map with ten layers is out of the question. Why would you have ten layers? I have no idea -- I'm exaggerating to make a point. :) But anyway, in this case there are a few things you can do. You can either divide your world map into several sections, or you can create some sort of scheme where if a map is larger than your data structure, you simply load a part of it, and when the player is approaching the edge of that part, you load a new part so the player is now in the center. This gives the illusion that the map is all present in memory at once, so you might want to try it if you've got some really ambitious maps that can't fit in memory all at once.
In my case, the largest map is the world map, which is around 300 tiles across by 220 tiles down. I use a single byte to hold each tile, so I just declared an array to hold anything up to this largest size: (300 tiles) x (220 tiles) x (3 layers) = 198 KB. Not bad at all.
"Exactly what types of data should my maps contain?"
This one isn't so clear right
away, because there are a million different things you can do with it. So let's take a look at some of the things you see on game maps, and then we'll
figure out which ones might be good to include in the actual map files.
The tiles themselves are what define the map, so obviously those have to be included in the map file. Almost all of the rest of these items are optional. You can either place them in the map file, or use a script to place them on the map at runtime. Personally I like to take the latter approach, because that makes the maps a bit more flexible. What if you want the same map to have different items and characters at different points of the game? The solution might not be too pretty if you have all the objects stuck in the map file... so I prefer to use scripts.
In Terran, scripts are used to place items and NPCs. The actual map file contains the tile data, as well as two other things. Since the enemies won't appear directly on the map, the map files contain rectangular regions on the map that define what types of enemies can be encountered within that region. These regions may also contain data like what the encounter rate is for that particular area. The second thing included in the map file is any scripts that are linked to the map. If the player steps on a tile with a script linked to it, that script executes. That's how I control moving from one map to another, triggering story sequences, etc.
"How am I going to store maps in a file?"
Once you've answered the previous
question about what to include in your map files, it's pretty easy to come up with a file format you can use for them. An example might be something like
the basic format below. Mine is very similar, only with the sections for items and NPCs removed.
--HEADER-- - File identifier (flags this file as a game map file) - Dimensions of map in tiles - Number of layers in map - Number of scripts linked to map - Number of enemy regions on map - Number of items on map - Number of NPCs on map --DATA-- - Tile data - Script data - Region data - Item data - NPC data
So defining the file format is easy... it's generating the map files that's the hard part! For this you'll almost certainly want to create some sort of map editor... unless you want to sit around for the next ten years with your tile lists trying to enter maps in Notepad. :) I don't have space in this article to go over making a map editor, but basically it should contain a view for each layer, so you can add tiles to any layer you want; functions to link items, scripts, enemies, and such to your maps; and any other nifty features you want to throw in to make things a little easier on yourself. Here's a screenshot of the editor I developed for Terran, in action:
Cool, hey? :) The interface is mouse-driven for the most part, although you can also use the keyboard to scroll the screen around, which helps out a lot. One other thing I'll mention that's good to include: see that little button with the eye on it, under the preview window? If you turn that on, the editor overlays a grid of red pixels on the areas that can't be reached by the player. This is a great thing to include to make sure that you are allowing access to the areas you want the player to be able to reach, and effectively blocking off one path from another.
Now that we've more or less figured out what kinds of features and specifications our maps will include, the next thing to consider is how to represent them in memory. After we've got that down, coding the actual engine isn't too big of a deal. Let's take a look at some data structures, shall we?
Here there are three things we should consider. First, how do we store a map in memory? Second, how do we store data for an individual tile? And finally, how do we keep track of the player? Let's start with the map. Obviously you need to provide room to store all the information that's in your file. That's about it! So all you need to do is create a structure (or a class if you're using C++) that will hold all the info you need. Take a look at the one I'm using in Terran:
typedef struct MAPDATA_type { int xMax, yMax; // map size in tiles int xCamera, yCamera; // camera location in pixels int xMaxCamera, yMaxCamera; // max camera coordinates int nNPCCount; // number of NPCs int nScriptCount; // number of linked scripts LPNPC lpnpc; // linked NPCs LPSCRIPT lpscr; // linked scripts } MAPDATA, FAR* LPMAPDATA; BYTE map[300][220][3]; // the actual tilesThe array for the actual tile data is declared separately, so I can use multiple MAPDATA structures at once if I ever need to. As for the elements of the structure, ome of this stuff is included in the map file, and some of it isn't, so let me go through this a little.
int xMax, yMax
: These are the actual map dimensions. They are stored in the map file.
int xCamera, yCamera
: These are the current coordinates of the "camera," and so they are changing
constantly. The initial values are set by whatever script loaded the map. Each frame, the map is drawn with the upper-left hand
corner of the screen at the camera coordinates. Thus, the minimum camera coordinates are (0, 0).
int xMaxCamera, yMaxCamera
: These are the maximum camera coordinates, and are calculated based on the size of the map. Each
tile in Terran is 32x32, so the width of the map in pixels is 32 * xMax, and the height is 32 * yMax. But the camera is located at the upper-left corner of
the screen, so we have to subtract the screen dimensions. The maximum camera coordinates are thus calculated in this way:
map.xMaxCamera = (map.xMax - 19) * 32; if (map.xMaxCamera < 0) map.xMaxCamera = 0; yMaxCamera = (map.yMax - 14) * 32; if (map.yMaxCamera < 0) map.yMaxCamera = 0;
The 19 and 14 are subtracted from the map dimensions because the screen resolution is 640x480, and thus a full screen is 20 tiles across by 15 tiles down. This accounts for the fact that the camera is positioned in the upper-left corner of the screen. If a map is ever encountered that is smaller than the screen, the maximum camera coordinates are set to (0, 0) so we don't get negative values.
int nNPCCount, nScriptCount
: These are the number of NPCs and scripts currently on the map,
respectively. The latter is included in the map file; the former is not. The number of NPCs will be set by the same script that is loading the map.
LPNPC lpnpc
: This pointer will be an array of NPC structures, each of which describes the location
and behavior of a single character. I won't get into the NPC structure here.
LPSCRIPT lpscr
: This will be an array of SCRIPT structures, which simply hold the index of each
script (used for locating the correct script file), and the tile it is linked to.
So that's not so bad. Now let's consider how to hold information about our tiles. What do we need to know about tiles? For starters, we need to know where on the DirectDraw surface each tile is located. There will probably be a pattern here that you can use just as easily, but I prefer to actually include a RECT for each tile, because then if you ever want to use tiles of variable sizes, the ability to do so is there. You also need to know whether or not that tile can be walked on. This will define where a player can go on the map. That's the bare minimum you need. So let's take a look at what Terran is using:
typedef struct TILE_type { RECT rcLocation; // location on DirectDraw surface int bWalkOK; // can the tile be walked on? int nAnimSpeed; // animation speed in frames DWORD dwFlags; // approach flags TILE_type *lpNext; // next tile in animation } TILE, FAR* LPTILE;
Some of this stuff is pretty simple to figure out, but more of it needs explanation, so here's the member list:
RECT rcLocation
: This is the location on the surface, which we already talked about.
int bWalkOK
: This simply tells whether or not the tile can be walked on. My variable name suggests
a Boolean value, but you can do other things with this as well. For instance, if you were creating a real-time strategy game, you might want to use this
field to not only say whether or not a unit can move on this tile, but how quickly or efficiently as well.
int nAnimSpeed
: This is used for tile animations. If the tile is not animated, this member is 0. If the tile
is animated, such as water or fire, then this member is the number of frames to delay before displaying the next tile in the animation.
DWORD dwFlags
: You can stuff just about anything you want in a parameter like this, but I'm using it
for approach information. That is, when the character is walking on this tile, how does his location change? For instance, if you're walking left onto a tile
depicting a staircase, you don't just walk straight onto it... you walk up the stairs! The dwFlags member is used to specify if and how a character's location
changes when traversing this tile.
TILE_type *lpNext
: If the tile is animated, this is a pointer to the TILE structure representing the next
tile in the animation. So basically, in addition to having a TILE structure for each tile, you can also string the structures together in linked lists to account
for animation.
To keep track of my tilesets, each of which have a maximum of 256 tiles, I have an array of 256 TILEs to keep track of the tile data itself, and an array of 256 LPTILEs that point to the corresponding structures. Then, when an animation needs to be advanced, I can simply do this:
lptile[x] = lptile[x]->lpNext;
And the animation advances by one frame for every instance of that tile on the map, without even having to touch the map data itself. Is that easy or what?
Finally, we need to have information about the player. For the purposes of showing a map on the screen and being able to wander around on it, we really
don't need anything except the player's location. My player structure has all sorts of information about stats, spellbooks, inventory, etc. that is irrelevant
as far as the tile engine is concerned, so let's just use xPlayer
and yPlayer
to keep track of our player.
All right! Now that we've got data structures out of the way, we can render a map, no problem.
For the sake of not bombarding you with too many things at once, I'm going to show you a simplified version of the map-rendering function. We're not going to be dealing with the dwFlags parameter for each TILE structure, and we're not going to use any NPCs, so we don't have to go to the trouble of figuring out which ones are on the screen, and ordering them correctly. With that stuff out of the picture, drawing the map becomes a matter of following these steps:
That doesn't sound too bad, does it? And once you can figure that stuff out, adding the other, slightly more complicated aspects of the engine shouldn't be much of a problem for you. Anyway, let's take a look at how you find the tile ranges.
I already said that our standard tile size is 32x32, and the screen size is 640x480, so a full screen is 20 tiles across by 15 tiles down. However, this assumes that the boundaries of the tiles line up perfectly with the boundaries of the screen. For example, suppose the leftmost tile is only halfway on the screen. Then there's going to be another tile halfway on the screen at the extreme right side of the screen, making for 21 tiles altogether. Similarly, there may be 16 rows of tiles vertically rather than 15, if the upper row is only halfway on the screen. How do we account for this? It's simple. If the camera's x-coordinate is divisible by 32, the tile boundaries line up with the screen boundaries horizontally, and there will be 20 tiles across. Otherwise, there will be 21 tiles across. Similarly, if the camera's y-coordinate is divisible by 32, there will be 15 tiles down; otherwise, 16.
Now, how about finding the index of the tile to use to start with? Consider this: if the camera's x-coordinate is anywhere between 0 and 31, then the first column of tiles (column 0) is going to be at least partially visible. As soon as the camera's x-coordinate becomes 32, that first column of tiles is completely off the screen, and we use column 1 instead. Look at that for awhile and you'll realize pretty quickly that the first column of visible tiles is given by the camera's x-coordinate, divided by 32, where we truncate the decimal rather than rounding. The same goes for the rows of tiles: the first row will be given by the camera's y-coordinate divided by 32.
The last thing we need to do, just in case we have a map smaller than the screen size, is to make sure that the ending values for our tile ranges do not exceed the maximums that are stored in our map. Have a look at the code that does all this:
// set original destination RECT for first tile -- aligned with the // upper-left corner of the screen RECT rcDest = {0, 0, 32, 32}; // find default tile range -- divide camera coordinates by 32 and // use the default of 21 tiles across and 16 tiles down int xStart = map.xCamera >> 5; int yStart = map.yCamera >> 5; int xEnd = xStart + 20; int yEnd = yStart + 15; int x, y; // now check if the camera coordinates are divisible by 32 x = map.xCamera & 0x0000001F; y = map.yCamera & 0x0000001F; if (!x) { // if xCamera is divisible by 32, use only 20 tiles across xEnd--; } else { // otherwise move destination RECT to the left to clip the // first column of tiles rcDest.left-=x; rcDest.right-=x; } if (!y) { // if yCamera is divisible by 32, use only 15 tiles down yEnd--; } else { // otherwise move destination RECT up to clip the first row // of tiles rcDest.top-=y; rcDest.bottom-=y; } // finally make sure we're not exceeding map limits if (xEnd > map.xMax) xEnd = map.xMax; if (yEnd > map.yMax) yEnd = map.yMax;
All right, now we've got the starting and ending indices for the tiles we need to blit, and the RECT for the first tile's destination on the screen. I already showed you the one-line method for updating animations. All you need to add is something to make sure it only happens according to the animation speed set by the nAnimSpeed member of the TILE structure. What I like to do is just to have an array of counters, say nCounters[10], and update them each frame like this:
for (x=2; x<10; x++) { if (++nCounters[x] == x) nCounters[x] = 0; }
That way, the counter in position x in the array is equal to 0 once every x frames. So when you're updating animations, just check to see if the appropriate counter is equal to 0, and if it is, advance the animation. Remember that if a TILE's nAnimSpeed is equal to 0, you don't need to do this because it's not an animated tile. All right, now we're ready to start drawing! All we need are two nested for loops, one to draw the columns and one to draw the rows. In the inner loop, we update the destination RECT, and blit the tile. That's it. The code is very straightforward:
BYTE byTile; // store original rcDest RECT RECT rcTemp; rcTemp = rcrcDest; // plot the first layer for (x=xStart; x<=xEnd; x++) { for (y=yStart; y<=yEnd; y++) { // blit the tile byTile = tiles[x][y][0]; lpddsBack->Blt(&rcrcDest, lpddsTileset, &tile_ptrs[byTile]->rcLocation, DDBLT_WAIT, NULL); // advance rcDest RECT rcDest.bottom += 32; rcDest.top += 32; } // reset rcDest RECT to top of next column rcDest.left += 32; rcDest.right += 32; rcDest.bottom -= ((yEnd - yStart + 1) << 5); rcDest.top -= ((yEnd - yStart + 1) << 5); }
That's all you need. This code draws the entire first layer, advancing the destination RECT as it goes without having to re-calculate the whole thing each time. Notice that at the end of the outer loop, we can't just decrease the y-values of the RECT by 480 because we might be dealing with a map that is smaller than the screen size. I won't even bother showing the code for the second and third layers, because it's nearly identical to this, and so it would just be redundant. These are the only differences:
With that, you can easily draw the whole map. The only thing that remains to figure out is where to place the character, which, as it turns out, takes a little work.
What I'm going to show you is simply the way to draw the main character. Drawing NPCs is done in almost the exact same way, so it will be easy for you to add once you understand the main character. There's also the matter of sorting the NPCs into the correct order, but again, I'm not going to get into that here. Now, before I can show you the way to draw the character, I have to show you a little more of the player structure. Here it is, with all the inventory, stats, and spells members removed:
typedef struct PLAYER_type { int xTile, yTile; int xOffset, yOffset; int xMovement, yMovement; int nDirection } PLAYER, FAR* LPPLAYER; #define DIRECTION_SOUTH 0 #define DIRECTION_NORTH 3 #define DIRECTION_EAST 6 #define DIRECTION_WEST 9
The first pair of members is the indices of the tile on which the player is currently standing. The second pair represents the offset, in pixels, from that tile. The offset is increased when the player is walking away from that tile. The final pair represents the number of tiles that the player is going to move in total. In regular gameplay, this will always be in the range [-1, 1]. However, in story sequences when scripts are moving characters around, this can be any number of tiles. The nDirection member takes one of the DIRECTION_ constants and represents the direction the character is facing.
Each character has twelve frames of animation: three each for four directions. Frames 0, 3, 6, and 9 are the character standing still. Frames 1, 4, 7, and 10 are the character with his right foot forward; and frames 2, 5, 8, and 11 are the character with his left foot forward. Based on the player's offset from his tile location, we must select the correct frame to give the animation sequence for walking. Each frame, the offset increases by four if it is positive, or decreases by four if it is negative. When the absolute value of the offset is 32, the character has moved an entire tile, and so the offset is returned to 0, and the character's tile location is updated.
Walking from one tile to the next corresponds to the character taking two steps. So this is how we choose the frame of animation to use: first, choose the frame given by nDirection. Second, alter that frame according to the relevant offset. If the character is moving horizontally (i.e. xMovement is not 0), then use the absolute value of xOffset. If the character is moving vertically (i.e. yMovement is not 0), use the absolute value of yOffset. This is how the frame is altered:
If abs(offset) is... | Alter frame in this way: |
0 | (no change) |
4, 8 | frame += 1 |
12, 16 | (no change) |
20, 24 | frame += 2 |
28 | (no change) |
That's just about all the information you need for the character, so now let's look at how to update the character information for each frame. Here's the code:
// update position if (player.xMovement > 0) { // handle movement east player.xOffset+=4; if (player.xOffset == 32) { player.xOffset = 0; player.xTile++; player.xMovement--; } } else if (player.xMovement < 0) { // handle movement west player.xOffset-=4; if (player.xOffset == -32) { player.xOffset = 0; player.xTile--; player.xMovement++; } } else if (player.yMovement > 0) { // handle movement south player.yOffset+=4; if (player.yOffset == 32) { player.yOffset = 0; player.yTile++; player.yMovement--; } } else if (player.yMovement < 0) { // handle movement north player.yOffset-=4; if (player.yOffset == -32) { player.yOffset = 0; player.yTile--; player.yMovement++; } } // fix camera to center position map.xCamera = (player.xTile << 5) + player.xOffset - 304; map.yCamera = (player.yTile << 5) + player.yOffset - 240; // adjust for map edges if (map.xCamera > map.xMaxCamera) map.xCamera = map.xMaxCamera; if (map.yCamera > map.yMaxCamera) map.yCamera = map.yMaxCamera; if (map.xCamera < 0) map.xCamera = 0; if (map.yCamera < 0) map.yCamera = 0;
This accomplishes two things. First, it sets all the members of the PLAYER structure to their correct values so as to walk the character from one tile to the next. Second, it updates the camera coordinates so that the player is always centered. It does this by subtracting half the screen's height from the player's y-coordinate, and half the screen's width minus half the character's width from the player's y-coordinate. (A character in Terran can be up to 32x64.) This gives you scrolling automatically! Since the camera is constantly being updated to center the character, the map-drawing code I showed you earlier will scroll smoothly as a result!
Now all you need to do to walk the character around is to set the values of xMovement and yMovement when the player presses an arrow key, and this code will do the rest. The last thing you need is to actually show the character on the screen. Since you're already familiar with all of the data members that are involved, I'll just go ahead and show you the code for this:
// set the default source RECT for frame #0 RECT rcSrc = {0, 0, 32, 64}, rcDest; int nFrame; // now figure out which frame to use using the chart from earlier nFrame = player.nDirection; if (((player.xMovement) && ((abs(player.xOffset) == 4) || (abs(player.xOffset) == 8))) || ((player.yMovement) && ((abs(player.yOffset) == 4) || (abs(player.yOffset) == 8)))) nFrame++; if (((player.xMovement) && ((abs(player.xOffset) == 20) || (abs(player.xOffset) == 24))) || ((player.yMovement) && ((abs(player.yOffset) == 20) || (abs(player.yOffset) == 24)))) nFrame+=2; // update src RECT rcSrc.bottom += (nFrame << 6); rcSrc.top += (nFrame << 6); // set up dest RECT rcDest.left = (player.xTile << 5) + player.xOffset - map.xCamera; rcDest.top = (player.yTile << 5) + player.yOffset - map.yCamera - 32; rcDest.right = rcDest.left + 32; rcDest.bottom = rcDest.top + 64; // blit the character sprite lpddsBack->Blt(&rcDest, lpddsCharacters, &rcSrc, DDBLT_WAIT | DDBLT_KEYSRC, NULL);
As you can see, the destination on the screen is found by converting the player's tile location into pixels, taking the offsets into account, and then subtracting the camera coordinates. This is, in effect, transforming world coordinates into screen coordinates. Finally, 32 is subtracted from the y-coordinate because the character is as tall as two tiles, and so he must be drawn starting from the tile above the one he's actually standing on. Do this after you blit the second layer of the map, and the scrolling engine is complete!
Well, it took awhile, but we've covered just about everything. I've explained how to create maps, display them on the screen, move the character around, and find the correct camera coordinates for each frame so that everything animates nicely. With just a little bit of work, you should be able to adapt a similar method to your project, and add some advanced features to make your game a little more interesting. If you have any questions about anything in this article, or if you see something in Terran and you're just curious how it's being done, feel free to contact me:
Good luck with your games, and happy coding. :)
Copyright © 2000 by Joseph D. Farrell.