Drawing the MapNow we're getting somewhere! The first thing to do when we draw the map is to determine the range of tiles which are currently in the player's view, so we don't waste time trying to blit a whole bunch of tiles that are way off the screen somewhere. 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 that is visible 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 = mapdata.xCamera >> 5; int yStart = mapdata.yCamera >> 5; int xEnd = xStart + 20; int yEnd = yStart + 15; int x, y; // now check if the camera coordinates are divisible by 32 x = mapdata.xCamera & 0x0000001F; y = mapdata.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 > mapdata.xMax) xEnd = mapdata.xMax; if (yEnd > mapdata.yMax) yEnd = mapdata.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. There are two ways to do this. You can keep a running frame count for each tile, and then when that frame counter reaches the value of nAnimSpeed, then advance the animation and reset the counter. The other way is to simply use 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. This method takes a little less memory, and possibly less time to update, depending on how many animations you have. The downside is that you don't want to create too many of these counters, or you'll be using too much time to update them. For example, if you wanted to have an inn sign that blows in the wind every once in awhile, you might only want to run the animation once every 100 frames, and so a counter for each tile would be appropriate. 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 = rcDest; // plot the first layer for (x=xStart; x<=xEnd; x++) { for (y=yStart; y<=yEnd; y++) { // blit the tile byTile = byMap[x][y][0]; lpddsBack->Blt(&rcDest, 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 layer, 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. And it may not seem like it, but you now have all you need to know to create the tile-scrolling demo on your own! It's going to be a simple demo, but it is our first program that allows considerable customization and user interaction. There are all sorts of things you can add to this demo, and so it would be a great exercise to try and implement a few things on your own, like adding a third layer, or adding the ability to move from one map to another. If you're really feeling ambitious, you could even take a shot at a map editor! ClosingThat's it for today's installment... but we covered a lot! In one day, we've gone from simple knowledge of DirectDraw to being able to implement a scrolling, animated, multi-layer tile engine. Are you starting to see how powerful the DirectX API is? We actually haven't covered a whole lot of interfaces and functions, but we already have the tools to put a pretty good-looking game together. For now we've just got a blank world, but next time around, I'll be showing you how to add characters it, to make it come alive. The sample code for this article has a lot of stuff in it that you should take note of. For one thing, it draws on the material from just about every article in the series up to this point, so if you understand what's going on here, you're doing great! It demonstrates how to read and write a basic map file as well, which is something I've seen people ask about quite a bit. There are two programs included in the .ZIP file. One is a very simple example of writing a map. This is a Win32 Console Application, so keep that in mind if you decide to compile it. The other program is the actual tile engine. It has one source file, two headers file, and one resource script -- be sure to include them all when you compile. Also, remember to link ddraw.lib and dxguid.lib to the project. Finally, the bitmap needs to be in the same directory as the source files, and you should be OK. I've included the executable as well, just in case you have problems compiling the code. I wanted to keep the tileset small, and I spent about two minutes on the tiles that are in there, so forgive me for the abysmal quality of the graphics. :) As always, I'm open to questions on any of this stuff: E-mail: ironblayde@aeon-software.com Have fun experimenting with this stuff, and I'll see you in a few weeks. Copyright © 2000 by Joseph D. Farrell. All rights reserved. |