|
Smooth
Scrolling Tiles
by Lucky
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!
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
D im 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)
End
Function
|
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
End Sub
|
| |
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
End
Sub
|
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!
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.
|