Tilemaps are a way of creating complex levels without alot of data. A titlemap is basically a collection of numbers that represent what sprite to draw at that location. The tilemap data for the above screenshot is below. Note that the hero and the bullet are not represented on the tilemap. The tilemap data only contains the "map" sprites to be drawn:
.db $27,$00,$00,$00,$00,$00,$00,$00 .db $28,$00,$00,$00,$80,$00,$00,$00 .db $29,$00,$00,$00,$00,$00,$82,$00 .db $89,$00,$81,$00,$00,$00,$00,$00 .db $89,$00,$00,$00,$00,$00,$00,$00 .db $89,$00,$00,$00,$00,$80,$00,$28 .db $27,$87,$85,$85,$85,$85,$83,$29 .db $29,$88,$86,$86,$86,$86,$84,$27 .db $27,$28,$00,$00,$00,$00,$00,$28 .db $8c,$8b,$00,$00,$00,$80,$00,$27 .db $8c,$8b,$80,$00,$00,$00,$00,$27 .db $03,$8b,$00,$00,$00,$00,$27,$29 .db $04,$02,$00,$00,$81,$00,$28,$27 .db $05,$02,$00,$00,$00,$00,$28,$29If you look at the numbers and the picture closely, you can see that the numbers correlate to the sprites in the picture. Please note that the format for this tilemap is kind of different than what you would expect. It goes bottom to top, left to right, instead of the "traditional" left to right, top to bottom. The format was chosen because it simplified the coding for this game. There will be many times that you will redo your data, and probably not just once, to make it easier on your routines while programming. Data should fit the code, not vice-versa.
Notice how the same numbers are used for the same sprites. The
right part of the column in the middle is made up of sprite $85 and the
left part is made up of 86. The rocky ground is made up of sprites
$27 to $29. The water is made up of $89, the lava of $8c and $8b.
This is one huge advantage of tilemaps. A small set of sprites can
be put together in many, many combinations over and over to create huge
levels. This is the technique used by many of your favorite games:
Sqrxz, Super Mario 86, Insane Game, Slippy, etc.
Screen Sized Tilemaps
The simplest types of tilemaps are ones that are the size of the screen. You see these in games like Slippy, Vexed, etc. These could be used for an RPG as well, if you wanted to use "paged scrolling". Pages scrolling is where the screen changes to a different screen whenever you get to the edge. This kind of scrolling is much easier to implement than smooth scrolling and allows you to use more tricks to save space than is possible with a side scroller. It is also much faster and easier to draw because all of the Sprites are aligned. The screen is made up of 8 by 16 sprites (using 8x8 sprites):
Map: .db $8,$8,$3,$5,$1,$7,$0,$9,$2,$6,$9,$7,$4,$0,$5,$3 .db $9,$3,$7,$6,$6,$8,$5,$1,$7,$4,$1,$5,$2,$6,$4,$5 .db $4,$2,$1,$7,$4,$3,$1,$6,$8,$8,$5,$3,$7,$6,$5,$3 .db $2,$4,$3,$0,$4,$3,$2,$8,$2,$7,$7,$5,$9,$3,$1,$4 .db $0,$9,$8,$7,$9,$6,$6,$0,$8,$6,$4,$2,$6,$5,$1,$4 .db $3,$2,$3,$6,$0,$9,$1,$1,$6,$1,$0,$5,$9,$4,$5,$3 .db $4,$9,$8,$1,$2,$1,$4,$7,$2,$5,$6,$3,$6,$4,$1,$8 .db $3,$4,$3,$0,$8,$0,$0,$9,$6,$2,$6,$7,$3,$0,$1,$9Each byte of the map stands for the sprite to draw at that position. The map goes left to right, top to bottom. This is a good configuration because it simplifies creating the map, especially if it is made by hand and not with an editor. The sprites for the map will normally be in a contiguous block, meaning each sprite will follow right after the one before it:
Sprites: Sprite0: .db %00000000 .db %00000000 .db %00000000 .db %00000000 .db %00000000 .db %00000000 .db %00000000 .db %00000000 Sprite1: .db %00000000 .db %01111111 .db %01000000 .db %01000000 .db %01000000 .db %01000000 .db %01111111 .db %00000000 ...Having the sprites is just about the only possible way they could be. If they were all scattered out, the DrawMap routine would be a nightmare to program. The basic idea of drawing the screen is to go through the map tile by tile, multiply the current tile by the 8 (the size of the sprite in bytes), add that to the start of the sprites (which gives us the correct sprite to draw), determine the correct location on the screen then finally draw the sprite. If we were doing this in a high level language, it would look something like the pseudo code below:
FOR y = 1 to 8 FOR x = 1 to 16 sprite = Sprites + (Map[y][x] * 8) GridPutSprite(x, y, sprite) NEXT x NEXT yOf course, doing this in assembly takes a little more thinking, but it's not all that different. The basic idea stays the same, but the implementation is based around the actual assembly instructions. I would never say that my routine is the fastest way to draw a screen sized tilemap, but it is pretty fast:
;============================================================ ; DrawMap -- draws an 8x16 tilemap [Assembly Coder's Zenith] ; input: HL = pointer to tilemap to draw ; total: 34b/91529t (including ret and entire GridPutSprite) ;============================================================ DrawMap: ld de,$0000 ; start drawing at (0, 0) DrawMapL: push hl ; save map pointer ld l,(hl) ; get the current tile ld h,0 ; clear the upper byte add hl,hl ; multiply by 8 (* 2) add hl,hl ; * 4 add hl,hl ; * 8 ld bc,Sprites ; load the start of the sprites add hl,bc ; add togehter to get correct sprite push de ; save sprite draw location call GridPutSprite ; draw the sprite pop de ; restore sprite draw location pop hl ; restore map pointer inc hl ; go to next tile inc e ; move location to the right bit 4,e ; if the 5th bit (e = 16), we're done jr z,DrawMapL ; only jump if the row isn't complete ld e,0 ; draw at the left again inc d ; move location down a row bit 3,d ; check if it's done (e = 8) ret nz ; return if we're done jr DrawMapL ; jump back to the top of the loopThe code to call the routine would look something like this:
ld hl,Map ; point to the first byte of the map call DrawMap ; draw the mapAn example of all of this can be found here.
Working with Tilemaps
Tilemaps can be thought of as an array of memory. Each "element" is the value of the sprite to draw for that location. To access element N of the tilemap, you need to add N to the starting address of the map:
; input: a = number of tile to get ; returns: hl = pointer to tile GetTileN: ld e,a ; load N into low byte ld d,0 ; clear the high byte ld hl,Map ; load the starting address of the map add hl,de ; add the offset (N) to the map's address ret ; return
This routine is the most basic of tilemap routines. It will most likely not be useful, but is very important for understanding the concepts. The offset N is passed to the routine. It is loaded into the low byte of a 16-bit register, then the high byte of the register is cleared. The address or starting location of the map is loaded, and the offset N is added to it. The pointer to tile N of tilemap Map is returned in HL. The address would be used to load data from the map or modify the data already there.
Interacting with Sprites
Now, I'm sure that the first thing you want to do after you draw the map is to have a little guy walk around on the map. I'm going to assume that you are familiar the concept of sprites (drawing, moving, erasing, etc.). If not, then check out the sprites section. Therefore, I'm not going to load this tutorial with more sprite stuff than is necessary. We'll concentrate more on the actual tilemap stuff.
Moving a sprite around on a tilemap isn't very different from moving a sprite around normally. It just involves a little more checking. Since most putsprite routines use BC for the X,Y coords of the sprite, that's what we'll use here. The most used function for tilemaps will probably be GetTile. This routine returns a pointer to the tile at the sprite coords BC. So, if you have a sprite at (8, 9), it will return tile 17, or the 18th tile. Since tiles are 8x8, the tile number can be found by this formula:
tile = ((y / 8) * 16) + (x / 8)
The X and the Y coords are divided by the size of the sprite, which is 8. The Y coord is then multiplied by the width of the map, 16, and added to the X coord. The first thing that should jump out at you is that Y is divided by 8, then multiplied by 16. So why divide in the first place, my not just multiply by 2? Good question. The divisions are integer divisions, meaning that the result is truncated. So what really happens is that Y is rounded down (truncated) to the nearest 8, then multiplied by 2. In assembly terms, this means that the lower 3 bits are masked off. The assembly implementation of this routine is a good example of shifting for multiplication and divsion:
; bc = coords to get tile at (x, y) ; returns: hl = pointer to map tile GetTile: srl b ; divide x by 2 srl b ; / 4 srl b ; / 8 ld a,c ; get y coord and %11111000 ; clear lower 3 bits (truncate to 8) add a,a ; y * 2 add a,b ; add to x (was divided by 8) ld l,a ; load into low byte ld h,0 ; clear high byte ld bc,Map ; point to start of the map add hl,bc ; compute offset in map ret ; return
You would call this routine with your sprite coordinates in BC. It will return a pointer to the tile that the sprite is over in HL. A note about this is that the sprite's coords are not actually the center of the sprite, but the upper-left corner. If you want the center of the sprite, then you would center the coords first:
; bc = coords (x, y) to be centered ; returns: b + 3, c + 3 CenterCoords: ld a,3 ; the coords are off by 3 pixels add a,b ; add 3 to the x coord ld b,a ; save the x coord ld a,3 ; again, need to add 3 add a,c ; now center the y coord ld c,a ; save the new y coord ret ; all done
Perhaps the most interesting thing about assembly is that there is always another way to to do things. If you don't mind trashing HL, you can speed up the above routine by 1 t-state and make it 2 bytes shorter:
; bc = coords (x, y) to be centered ; returns: b + 3, c + 3 CenterCoords: ld hl,$0303 ; add 3 to each of the coords add hl,bc ; add to the coords ld b,h ; save the x coord