A Brief z80 Assembly Tutorial

Chapter 4

Time to tackle the inputs. We'll be adding a few new functions, so let's go through them in increasing complexity.

First off, we need a way to find the player on the map, so let's add a function for it.

Finding the Player

; findplayer - scans map and returns the player's position
; no parameters, destroys a, bc, returns position in hl
findplayer:                
        ld hl, map - 1
findloop:
        inc hl
        ld a, 1
        sub (hl)
        jr nz, findloop
        ld bc, 65536 - map
        add hl, bc
        ret    

I browsed through he instruction set looking for ways to compare two values, and there's this handy sub (hl) which we're using here. The loop doesn't even check when the map ends, so if there's no byte with value 1 in it, it will happily go traipsing through the memory until it does. Incidentally, if the player's tile is dirty, it won't be 1, which I found out by calling findplayer after calling dirtymap. Oops. You can try that too; afterwards if you tap up enough times you'll get back to the screen, with a slightly mangled player sprite.

Next, let's have a function that moves the player.

Moving the Player

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

First we clear the player's sprite off the map. Note that we're writing 0x80 instead of 0 here, to mark the tile as dirty. We could, as optimization, check if the player has moved at all and return if not; for now, drawing a tile or two every frame doesn't really matter.

We also read the variable movekey to register A. We've encoded the movement keys for up, down, left and right in the first four bits.

        bit 0, a
        jr z, notup
        ld bc, 65536 - 8
        add hl, bc
notup:

The function generally follows this pattern: check a bit, if it's not high, hop over the payload, otherwise run it. If the up button is down we add -8 to the player's position, moving up by one line of tiles.

        bit 1, a
        jr z, notdown
        ld bc, 8
        add hl, bc
notdown:
        bit 2, a
        jr z, notleft
        dec hl
notleft:
        bit 3, a
        jr z, notright
        inc hl
notright:

We do the same for down, left and right.

        ld (playerpos), hl
        ld bc, map
        add hl, bc
        ld (hl), 0x81
        ld a, 0
        ld (movekey), a
        ret    

Finally, we write the player's sprite back on the map, again with the dirty bit on.

And now, for the biggest function yet - scanning the inputs.

Scanning the Inputs

keydata:
        db 0,0,0,0,0,0,0,0,0,0
wasdown:
        db 0
movekey:
        db 0
inputmode:
        db 0

Let's start off with a bunch of new data. Keydata will contain the information we fetch from hardware about the state of the keys and buttons and whatnot. We won't actually need all of it, but for potential future re-use we might as well read all of it in.

Wasdown is used to stop the keys from repeating. In some games you might not want this, but for stonewalk we will. For a cheap controllable rate key repeat we could clear this to zero every N frames.

Movekey is the abstracted up/down/left/right data we used in moveplayer, above.

Inputmode is something that we'll leave as zero for now, but we use it here already. On the speccy it's usual that games start with a input selection screen (thanks to the lack of / plethora of joystick standards), so we'll support several input methods off the bat.

Anyway, off to scanning.

scaninputs:
        ld bc, 0xfefe
        in a, (c)
        ld (keydata + 0), a
        ld bc, 0xfdfe
        in a, (c)
        ld (keydata + 1), a
        ld bc, 0xfbfe
        in a, (c)
        ld (keydata + 2), a
        ld bc, 0xf7fe
        in a, (c)
        ld (keydata + 3), a
        ld bc, 0xeffe
        in a, (c)
        ld (keydata + 4), a
        ld bc, 0xdffe
        in a, (c)
        ld (keydata + 5), a
        ld bc, 0xbffe
        in a, (c)
        ld (keydata + 6), a
        ld bc, 0x7ffe
        in a, (c)
        ld (keydata + 7), a
        ld bc, 31 ; kempston
        in a, (c) 
        ld (keydata + 8), a

Here we use IN to read a bunch of ports and store the results in the keydata array for further study. We could do the port reads every time we want to check the status of a key, but this makes things a bit simpler.

Note that the first 8 bytes are read from ports that end with 0xfe. This is because everything on the speccy is linked to the port 0xfe.

There is a variant of IN that takes the port number as parameter, but that only works for 8 bit port numbers, so we're out of luck there.

Kempston is read from port 31, because that's an external, third party peripheral. Kempston was the most popular third party joystick standard, so it's prudent to support that.

        ld a, 0
        ld hl, inputmode
        sub (hl)
        jr z, input_wasd
        ld a, 1
        sub (hl)
        jp z, input_qaop
        ld a, 2
        sub (hl)
        jp z, input_kempston
        ; fallthrough to wasd

Next we'll check the input mode and jump to the appropriate input handler. Since this jump always goes to the exact same place, we could do the input selection via self modifying code.. or better yet, make it data driven and get rid of a lot of code.

Note that while the input_wasd label uses JR, the others use JP - this is because the jumps are too far for JR. It's usually a good idea to try JR first and see if the assembler complains, and change it to JP. (I wonder if the assembler already has a pseudoinstruction for "just jump, I don't care"? =)

        MACRO SCANKEY keybyte, keybit, outbit
            ld a, (keydata + keybyte)
            bit keybit, a
            jr z, .scan_key_down
            ld a, (wasdown)
            and a, 0xff ^ (1 << outbit)
            ld (wasdown), a
            jr .scan_done
.scan_key_down:
            ld a, (wasdown)
            bit outbit, a
            jr nz, .scan_done
            or a, 1 << outbit
            ld (wasdown), a
            ld a, (movekey)
            or a, 1 << outbit
            ld (movekey), a
.scan_done:
        ENDM

This is going to get really repetitive, so we'll do it with a macro. For every key we care about, we fetch the byte the key's bit is in, check if the bit is on, if the bit is zero it means the key is down. If the key is down we clear the bit from wasdown, and skip to the end of this macro.

If the key is down, we check if it was down previously, and if so, skip to the end; otherwise we mark the key was being down, and also set the bit in movekey so the player's movement will be triggered in moveplayer.

As a side note, I stumbled on a funny issue when writing that macro - if the MACRO keyword is at the start of the line without any whitespace before it, it will be taken as a label instead of the MACRO pseudoinstruction, causing all sorts of fun problems.

        ; 'W' : byte 2, bit 1
        ; 'A' : byte 1, bit 0
        ; 'S' : byte 1, bit 1
        ; 'D' : byte 1, bit 2
input_wasd:
        SCANKEY 2,1,0 ; up 
        SCANKEY 1,1,1 ; down
        SCANKEY 1,0,2 ; left
        SCANKEY 1,2,3 ; right        
        ret        

Then we use the macro to check each key separately.

        
        ; 'Q' : byte 2, bit 0
        ; 'A' : byte 1, bit 0
        ; 'O' : byte 5, bit 1
        ; 'P' : byte 5, bit 0
input_qaop:
        SCANKEY 2,0,0 ; up 
        SCANKEY 1,0,1 ; down
        SCANKEY 5,1,2 ; left
        SCANKEY 5,0,3 ; right        
        ret        

        ; kempston up    : byte 8, bit 2
        ; kempston down  : byte 8, bit 3
        ; kempston left  : byte 8, bit 0
        ; kempston right : byte 8, bit 1
input_kempston:        
        SCANKEY 8,2,0 ; up 
        SCANKEY 8,3,1 ; down
        SCANKEY 8,0,2 ; left
        SCANKEY 8,1,3 ; right        
        ret        

And the same for QAOP and Kempston inputs. QAOP is the traditional zx spectrum control scheme.

ERRATA: well, the kempston input as listed here doesn't work. I didn't test it. Shame on me. First, kempston is high active while keyboard is low active, so the bits need to be reversed - simply XOR 0x1f before storing the kempston data. Second, the input bits are wrong and should be 3,2,1,0 instead of 2,3,0,1. I'll update the last version of the source code with these fixes. Now, carry on..

Plugging it In

We naturally need to plug these in the main loop, so there's changes there:

        call findplayer
        ld (playerpos), hl
        call dirtymap
mainloop:
        ld a, 2
        out (0xfe), a

        call scaninputs
        call moveplayer       

        ld hl, map+63
        ld bc, 0x0808
maploop:    

Before the main loop we now call findplayer, store the player's position to our global variable and dirty the map. Inside the main loop we scan the inputs and move the player, before going into the map loop.

After this, we can use the WASD keys to move the player:

Note the little red blinks on the top border, where we need to do more work whenever the player hits a key. This will get worse when we have some level of physics in and rocks fall.

The raw binary is up to 1396 bytes; our repetitive key scanning function starts at 0x8167 and ends at 0x83A6, so it takes whopping 575 bytes. If we ever hit a situation where we need to squeeze some bytes out, this function will be a good candidate. We still have plenty of space, though, so let's not worry about it.

While we can move, nothing stops you from walking through walls or even outside the map yet. So there's still ways to go before this is a game..

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

Next up we'll see to restricting the player's movement.

Any comments etc. can be emailed to me.