A Brief z80 Assembly Tutorial

Chapter 9

Okay, the uncompressed, huge image data started bugging me, so let's compress it. I fetched Einar Saukas' excellent zx0 (github) decompressor and the windows compressor, compressed the image, included the decompressor source, replaced the raw data with the compressed one.

Compressed Image

keyselect_scr:
        INCBIN "keyselect.scr.zx0"

        INCLUDE "dzx0_standard.asm"

..and replaced the image copy ldir with a call to the decompressor:

        ld de, SCREEN
        ld hl, keyselect_scr
        call dzx0_standard    

Result: 3868 bytes, or 5329 bytes saved.

I also desaturated the image so it will compress a bit better. Whether the resulting image looks better or worse is a matter of taste.

New Assets

We need those minitiles for the minimap, but before doing that, I wanted a few new tiles. We know that the falling rock will be its own tile, so it might as well have a separate image too. When the rock hits the player the player goes splat, let's have a tile for that. I also want a wandering monster type, so four tiles go to the purple people eater, and finally let's have water and water source. Like so:

Again, the tiles are laid out top down to make them easier to parse.

With all the tiles done, I shrunk them with point sampling down to 8x8 and did some fixing by hand. The mini-tiles don't need to look too great, as they are just for preview.

The artwork done, I again converted these to binary with using my "png2bin" tool from my speccy github tool, and designed the colors straight in the source file.

tiles:
        BLOCK 32,0 	; empty tile
        INCBIN "tiles.dat"

        ; 0x00 0x00 = black      0x04 0x20 = green
        ; 0x01 0x08 = blue       0x05 0x28 = cyan
        ; 0x02 0x10 = red        0x06 0x30 = yellow
        ; 0x03 0x18 = magenta    0x07 0x38 = white
        ;      0x40 = bright          0x80 = blink
tilecolors:
        db 0x00, 0x00, 0x00, 0x00 ; 0 empty
        db 0x05, 0x05, 0x04, 0x04 ; 1 player
        db 0x10, 0x10, 0x10, 0x10 ; 2 ground
        db 0x57, 0x57, 0x57, 0x57 ; 3 bricks
        db 0x44, 0x44, 0x44, 0x44 ; 4 goo
        db 0x45, 0x05, 0x05, 0x45 ; 5 gem
        db 0x07, 0x07, 0x07, 0x07 ; 6 stone
        db 0x04, 0x04, 0x04, 0x04 ; 7 exit (closed)
        db 0x44, 0x44, 0x44, 0x44 ; 8 exit (open)
        db 0x07, 0x07, 0x07, 0x07 ; 9 falling stone
        db 0x07, 0x07, 0x02, 0x02 ; 10 splat stone
        db 0x03, 0x03, 0x03, 0x03 ; 11 people eater up
        db 0x03, 0x03, 0x03, 0x03 ; 12 people eater right
        db 0x03, 0x03, 0x03, 0x03 ; 13 people eater down
        db 0x03, 0x03, 0x03, 0x03 ; 14 people eater left
        db 0x0d, 0x0d, 0x0d, 0x0d ; 15 water
        db 0x07, 0x07, 0x0d, 0x0d ; 16 water source

minitiles:
        BLOCK 8,0 	; empty tile
        INCBIN "minitiles.dat"

minitilecolors:
        db 0x00 ; 0 empty
        db 0x05 ; 1 player
        db 0x10 ; 2 ground
        db 0x57 ; 3 bricks
        db 0x44 ; 4 goo
        db 0x45 ; 5 gem
        db 0x07 ; 6 stone
        db 0x04 ; 7 exit (closed)
        db 0x44 ; 8 exit (open)
        db 0x07 ; 9 falling stone
        db 0x07 ; 10 splat stone
        db 0x03 ; 11 people eater up
        db 0x03 ; 12 people eater right
        db 0x03 ; 13 people eater down
        db 0x03 ; 14 people eater left
        db 0x0d ; 15 water
        db 0x07 ; 16 water source    

Since the minitile colors are always the first ones from the tile colors, I could have saved a few bytes by just keep using those, but maybe it's cleaner this way.

Drawminitile got a few small changes to use the new minitile data; the changes are so trivial that I don't think it makes much sense to copy all of it here.

        ; One tile is 8 bytes
        add hl, hl ; *2
        add hl, hl ; *4
        add hl, hl ; *8
        ld de, minitiles
        add hl, de
        ld de, hl  ; hl now is pointing at the start of tile x

Since one tile is 8 bytes, *8 is now enough.

        ; Instead of looping, we'll plot each pixel separately..
        DUP 7
            ld a, (de)    ; Read pixels from data
            ld (hl), a    ; Write to screen
            inc de        ; Increment de and hl..
            inc h
        EDUP
            ld a, (de)    ; Read pixels from data
            ld (hl), a    ; Write to screen

One INC DE is gone from here, since we don't need to skip data anymore.

        pop hl ; tile index
        ld de, minitilecolors
        add hl, de  ; hl now points at tile color

Again, simplification: *4 is gone.

General Physics Fixes

Since we're going to have different kinds of physics interactions than just falling, we'll need to scan the whole map, which means we need to check that we're not falling through the bottom of the map.

The changes to scan the whole map are trivial:

physofs:
        db 0, 176
        jr nz, physics_notlooped
        ld hl, 176
physics_notlooped:        

Stopping items from falling below the map requires a new check at the start of physics_drop.

physics_drop:
        ld a, ixl
        cp 16*11+map
        jr nc, physicsdone    

Since IX points at the map data and is not just the tile index, we also need to take the map address into account. Which is bad. After all, the map address is 16 bit. Let's move the map data to a 256 byte offset so we don't need to deal with this - after this change the bottom byte of the address and tile index is the same thing. (This is one of those things that would be really complicated in C, but trivial in assembly).

We'll remove the +map from above, and move the map definition to be the very last thing in the file, with ORG to say what address it should start from.

        ORG $fd00
map:
        BLOCK 16*12,0

Careful now. The interrupt jump table is at 0xfe00 - 0xff01 and the table points at 0xfdfd, meaning we should really leave the memory area 0xfdfd-0xff01 alone. Our map will now be at 0xfd00-0xfdc0, so it's fine. There's also plenty of loose bytes in that region, but unless we're really hurting for space, we should keep everything under 0xfd00 now.

While we're dealing with things like this, let's stop player from pushing rocks outside the screen. The movestone handling gets a new check at the start.

movestone:
        ld a, l
        and a, 0xf
        jr z, movedone ; left border; can't be pushed
        cp 15
        jr z, movedone ; right border; can't be pushed        

Since the player can't be between the border and a rock that's already at a border, the pushing is impossible.

All that out of the way, we can start adding more physics stuff. Let's start with the falling rock.

Making Player go Splat

Making the player go splat requires a few changes. We need to change the stone's state when it's moving, add handler for falling rock, and add a flag that says the player's inputs are no longer desired.

        cp 0
        jr nz, physicsdone
        ld a, (ix)
        cp 6 ; stone
        jr nz, notstone
        ld a, 9 ; falling stone
notstone:
        or 0x80

In the physics_drop after we see that an item is about to fall downwards we need to check if it's a stone or not, and if it is, we replace it with a falling stone. Running the game after this change is a bit boring as the falling stone doesn't do anything. The sprite changes, though.

        cp 5
        jr z, physics_drop
        cp 6
        jr z, physics_drop
        cp 9
        jr z, physics_stone

In the physics handler we'll check for the moving stones and jump to a new handler, physics_stone.

physics_stone:
        ld a, ixl
        cp 16*11
        jr nc, stone_stop
        ld a, (ix+16)
        cp 0
        jr z, stone_fall
stone_stop:
        ld (ix), 0x86 ; inert stone
        jp physicsdone
stone_fall:
        ld (ix), 0x80
        ld (ix+16), 0x89
        jp physicsdone    

The handler is pretty simple; we check if we're at the bottom and go back to inert stone if we can't, then check if there's empty space below us and if there is, move there, otherwise go inert. Testing this we see the sprite change from inert to moving back to inert like we wanted.

One thing to note is that we're using JP to physicsdone here, because our function has grown so large that JR can't reach it anymore. The assembler will complain in these cases, and the fix is trivial. I'll be doing a lot of these changes as the program grows, and won't be mentioning about it every time.

Next we'll deal with the game state. After adding a single byte variable playerdone, we need to clear it at the start of levelselect or the player won't be able to keep playing:

levelselect:
        ld a, 0
        ld (movekey), a
        ld (playerdone), a

To stop player from moving (but not from hitting space), the moveplayer function's start changes to:

moveplayer:
        ld a, (movekey)
        bit 4, a
        jp nz, levelselect
        
        ld a, (playerdone)
        cp 1
        ret z

What's left is changing the moving stone's behaviour to accept moving on top of the player.

        cp 0
        jr z, stone_fall
        cp 1
        jr z, stone_splat

Simply check if the falling stone's target is 1 (player)...

stone_splat:        
        ld (ix), 0x80
        ld (ix+16), 0x8a ; splat
        ld a, 1
        ld (playerdone), a
        jp physicsdone

...move the stone there (as the splat sprite), mark player as done, and that's it. The player has been splatted.

This chapter's version of the source as well as the new assets and source of the decompressor is available here.

Size is at 4385 bytes, which tells me that sjasmplus does not include zero bytes in the raw output (as the size should be closer to 32k if it was). The good news there is that we can keep using the .raw file for size estimates; the bad side is that we can't be sure if the zeroed data is actually zeroed. Doesn't matter in our case, but something to keep in mind. Since our optimized size at the start was 3868 bytes, we increased by 517 bytes, mostly from the new tile graphics.

Any comments etc. can be emailed to me.