A Brief z80 Assembly Tutorial

Chapter 5

Before tackling the thing we want, namely limiting the player's movement, let's change the map size. There's no real reason why we wouldn't just use the whole screen as the map, and we can fit 16x12 tiles on the screen, so let's do that.

Resizing the Level

map:
        db 2,2,6,3,5,2,2,2,2,2,2,2,2,2,2,2
        db 2,5,6,3,2,2,6,2,2,2,2,2,2,2,2,2
        db 2,2,2,3,2,6,2,2,2,2,2,2,2,2,2,2
        db 3,2,3,3,2,2,2,6,2,2,2,2,2,2,2,2
        db 2,2,2,6,2,2,2,2,2,2,2,2,2,2,2,2
        db 2,2,0,2,2,2,6,2,2,2,2,2,2,2,2,2
        db 0,1,0,2,6,2,2,7,2,2,2,2,2,2,2,2
        db 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
        db 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
        db 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
        db 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
        db 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2

The map grows to that size, which is pretty huge compared to our earlier 8x8 map. But it will allow for more interesting levels. We have the space, so why not use it. If we want smaller levels, we can just surround them with brick walls.

After fixing every place that cares about map size (dirtymap, map drawing loop, player movement), the full screen map is there. But wait..?

We're using a lot more time now, much more than we'd expect by growing the map. After all, we're not drawing anything more, we're just scanning through it. Maybe it's just that slow. But let's see if we can figure things out a bit and give different border color for the drawtile from the rest of the main loop.

More Border Colors

        ld a, 1
        out (0xfe), a
        call drawtile
        ld a, 2
        out (0xfe), a    

Here we set the border color to 1 (blue) and back to 2 (red) after the drawtile. The result shows how much time (and where) the drawtile takes.

Moving around the map also changes where the blue stripe happens - the higher you are on the map, the lower the blue stripe appears. So, just scanning the whole map takes quite a bit of time.

We could solve this by making a live list of tiles that need updating - so we wouldn't need to scan through the map - but I don't think this is a problem for now. Let's march on.

We need to limit the player's movement in a few ways: the player shouldn't be able to move outside the screen, and the legal tiles to move into are empty, ground, diamond and open exit. Anything else is forbidden: brick walls, stones, closed exit and goo.

Starting with the map edges, the moveplayer function gets a lot of changes, so let's start off the top.

Blocking Player at Map Edges

moveplayer:
        ld hl, (playerpos)
        ld bc, map
        add hl, bc
        ld (hl), 0x80
        ld hl, (playerpos)

        ; Trying to move up, check if we're not on the top row
        ld a, l
        cp 16
        jr c, notup
        
        ld a, (movekey)
        bit 0, a
        jr z, notup
        ld bc, 65536 - 16
        add hl, bc
notup:

Before trying to move up, we add a few lines to check if we're in the top row, and if we are, we skip the whole movement key check. We do this by loading the tile offset to the A register and using the CP instruction. CP is practically the same as SUB, but doesn't store the result. In this case it doesn't matter which we use as we don't care about the contents of the register afterwards, but if we were to chain several comparisons, it would be helpful.

Subtracting 16 from, say, 5 will overflow and set the carry flag, so we know we're in the top row and can skip the up movement.

        ; Moving down, check if we're not on the bottom row
        ld a, l
        cp 16*11
        jr nc, notdown
        
        ld a, (movekey)
        bit 1, a
        jr z, notdown
        ld bc, 16
        add hl, bc

Same thing for the bottom row. Here we just check that the carry flag isn't set.

notdown:
        ; Moving left, x coordinate may not be zero
        ld a, l
        and 0xf
        jr z, notleft
        
        ld a, (movekey)
        bit 2, a
        jr z, notleft
        dec hl

We won't allow movement to the left if the x coordinate is zero. This is done by separating the x coordinate bits off the tile index, which conveniently also sets the flags, so we don't need a separate comparison operator.

notleft:
        ; moving right, x coordinate may not be 15
        ld a, l
        and 0xf
        cp 15
        jr z, notright
        
        ld a, (movekey)
        bit 3, a
        jr z, notright
        inc hl
notright:
        ld a, 0
        ld (movekey), a    

And moving right, we skip if the X coordinate is 15. We need the CP instruction here as we're comparing with a specific value, and subtracting 15 from 15 will be zero, so the zero flag is set if we're on the rightmost column.

Now we have the potential target coordinate, next we need to check if the movement is to a legal tile.

Blocking Illegal Moves

        ; Now that we have a *potential* new position in hl, we need to
        ; figure out if it's legal.
        ld bc, hl
        ld de, map
        add hl, de
        ld a, (hl)
        ld hl, bc

To see if the tile is legal, we do the register gymnastics to get the target tile. Then we can check if it's fine to move to that tile, and act accordingly.

        cp 0
        jr z, moveok ; Empty tile is always fine
        cp 2
        jr z, moveok ; Ground is fine
        cp 5
        jr z, moveok ; Gems are fine
        cp 8
        jr z, moveok ; Open exit is fine
        jr movedone

This is where the CP instruction comes handy; we don't need to re-load the register all the time. Alternatively we could check for the tiles that block movement, but there are just as many of those. Additionally some of the tiles have a special meaning, like collecting gems or moving to the exit; we'll get back to those eventually. We also want to make it so that rocks can be pushed if there's empty space to move to.. which will need its own handler.

moveok:
        ld (playerpos), hl
movedone:
        ld hl, (playerpos)
        ld bc, map
        add hl, bc
        ld (hl), 0x81        
        ret    

And in the end we either update the player's position or don't, but in any case update the player's sprite in the map.

This chapter's version of the source is available here.

The raw binary is now up to 1603 bytes, up from 1396 of the last version. Still plenty of space.

Any comments etc. can be emailed to me.