DirectX:DirectDraw:Tutorials:VB:DX7:Smooth Scrolling Tiles
Where would we be without smooth scrolling tile-based algorithms? There would be no Ultima.. no Final Fantasy (the early ones used tiles).. no Diablo.. NO STARCRAFT! (Can you tell I like Blizzard?) Tile scrolling methods have been in use for years, and there are some very advanced methods for implementing them.. but I'll stick to the basics this time around!
First, you'll need a Tile Set. This is simply a bitmap with all of your tiles clumped together. I'd suggest keeping them in a single row or column to begin with, for simplicity. If your tile sets are large however, you may prefer to change this.
Choose a nice width and height for your tiles, something that can cleanly divide your chosen screen dimensions is best. I've chosen 32x32 pixel tiles since this will give 20x15 tiles on a 640x480 display mode. Nice. Bear in mind: The size of your tiles plays an important role in determining the frame rate you'll achieve. Smaller tiles mean more blitting operations and a slower game, so be wary.
Ok, that's the tile set I constructed for this tutorial's sample source code. Don't ask me what the heck they're supposed to be, I'm not even sure! There are four tiles, each 32x32, stacked on top of one another.
Now we need a map. The map construct simply describes which tile will be displayed where.. it defines what the player will see as he walks through your world. The simplest possible "map" is a two dimentional array. Each element of the array will contain a reference to a tile.. this means that we need to assign each of our tiles some number (or other value). I'll number my tiles from 0-3 starting at the top and moving downward. This numbering system will come in handy later!
Sample data from a small map array (5x5):
0 2 3 3 1 2 2 2 1 0 3 0 3 1 2 3 1 1 0 1 2 3 2 1 2
This describes a 160x160 pixel area (the tile width and height are 32 pixels each, and our map is 5x5). If we make our map larger than the screen size however, we'll need to scroll around to see it! So we need a render routine that blits the scene based on the information in the map, the tile set, and some coordinates. These coordinates will describe the "center" of the screen--typically the location of the player object. By changing these coordinates gradually we should be able to produce the scrolling effect we desire.
It is important to note, the render routine will have to display more tiles than the screen can technically hold (more than the 20x15 tiles in a 640x480 display that I mentioned earlier). Sometimes you'll see part of a tile on one side of the screen and part of another on the other side--smooth scrolling requires that the map display shift by the pixel, NOT by the tile! Observe:
21x16 tiles are actually blitted, but some of them are clipped since they lie on the edges of the screen.
Alright enough theory, lets code this beast! We will need to loop through each of the tiles in our 21x16 grid, determine which tile to display (by consulting the "map"), clip the tile if necessary, and then blit it. First we'll tackle the tile lookup function:
Const SCREEN_WIDTH = 640
Const SCREEN_HEIGHT = 480
Const TILE_WIDTH = 32
Const TILE_HEIGHT = 32
Dim mbytMap(100, 100) As Byte Dim mintX as Integer Dim mintY as Integer
Private Function GetTile(intTileX As Integer, intTileY As Integer) As Integer
GetTile = mbytMap((intTileX + TILE_WIDTH \ 2 + mintX - SCREEN_WIDTH \ 2) \ TILE_WIDTH, _ (intTileY + TILE_HEIGHT \ 2 + mintY - SCREEN_HEIGHT \ 2) \ TILE_HEIGHT)
Okay, it's kinda ugly but it works. Let me explain. intTileX and intTileY are the X and Y coordinates of the tile about to be blitted. mbytMap is our map array (100x100 - lets assume it's filled with data already), and mintX and mintY are the "player" coordinates. Since the player is in the center of the screen, we must subtract SCREEN_WIDTH \ 2 and SCREEN_HEIGHT \ 2 as appropriate. Also, we need to add 1/2 of the TILE_WIDTH and TILE_HEIGHT values in order to ensure that we don't have rounding errors due to integer division near zero... just trust me :) Or try it without this, you'll notice that at the topmost or leftmost parts of the map, the tiles will mysteriously repeat.
After determining the combination of the player's coordinates and the desired tile's coordinates we must divide by the tile size in order to obtain the index value required for our map array. Easy, right?
Private Sub GetRect(bytTileNumber As Byte, ByRef intTileX As Integer, _
ByRef intTileY As Integer, ByRef rectTile As RECT)
With rectTile .Left = 0 .Right = TILE_WIDTH .Top = bytTileNumber * TILE_HEIGHT .Bottom = .Top + TILE_HEIGHT
If intTileX < 0 Then .Left = .Left - intTileX intTileX = 0 End If If intTileY < 0 Then .Top = .Top - intTileY intTileY = 0 End If If intTileX + TILE_WIDTH > SCREEN_WIDTH Then .Right = .Right + (SCREEN_WIDTH - _ (intTileX + TILE_WIDTH)) If intTileY + TILE_HEIGHT > SCREEN_HEIGHT Then .Bottom = .Bottom + (SCREEN_HEIGHT - _ (intTileY + TILE_HEIGHT)) End With
This function does a few things. It calculates the appropriate source rectangle for blitting purposes (clipped and all), and it also adjusts the X and Y coordinates if necessary. (Notice, most of the parameters are passed ByRef)
First, we take the bytTileNumber (either 0, 1, 2, or 3) returned by the map lookup and calculate the source rectangle size. Since our tile set is vertically linear, we can calculate the Top value by simply multiplying TILE_HEIGHT by bytTileNumber. The Left, Right, and Bottom values are simple to calculate thereafter.
Next, we must ensure that this tile clips to the size of the screen if necessary. If the X or Y coordinates are negative it is trivial to perform the clipping, but if the tile extends beyond the bottom or right side of the screen, it's a little tougher. We have to calculate the amount by which the tile extends beyond the screen and then subtract this from the current rectangle.
Private Sub DrawTiles()
Dim i As Integer Dim j As Integer Dim rectTile As RECT Dim bytTileNum As Byte Dim intX As Integer Dim intY As Integer
For i = 0 To CInt(SCREEN_WIDTH / TILE_WIDTH) For j = 0 To CInt(SCREEN_HEIGHT / TILE_HEIGHT) intX = i * TILE_WIDTH - mintX Mod TILE_WIDTH intY = j * TILE_HEIGHT - mintY Mod TILE_HEIGHT bytTileNum = GetTile(intX, intY) GetRect bytTileNum, intX, intY, rectTile msurfBack.BltFast intX, intY, msurfTiles, rectTile, DDBLTFAST_WAIT Next j Next i
At long last, here is our rendering routine! It steps through each "tile location" from zero to SCREEN_WIDTH / TILE_WIDTH and SCREEN_HEIGHT / TILE_HEIGHT. intX and intY are then calculated and contain the X and Y coordinates of the top-left corner of the next tile to be drawn on the screen. This is accomplished by multiplying our loop counter by the height or width of the tile and subtracting the current "offset" (the amount by which the "player" coordinates are offset from our grid). After this, our GetTile and GetRect functions are called, and finally... we blit!
That's all there is to know about basic tile scrolling. Click here to download this tutorial's sample source code. As I mentioned at the onset, there are more advanced and efficient techniques. If the frame rate is important to you, I would suggest this: Rather than blitting every tile to the backbuffer each frame, create an offscreen surface and blit to this only when necessary. This surface can then be used to refresh the backbuffer in one fell swoop (one large blit is faster than many small ones). This surface will need to be refreshed whenever the player moves beyond the bounds of his current tile. It requires a little more effort on your part (my source code does not take advantage of this method), but it should increase your FPS somewhat.