How To Write ZX Spectrum Games – Chapter 12

October 2, 2013 Leave a comment

Timing

Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.

The Halt Instruction

We measure the speed of a Spectrum game by the amount of time it takes for a complete pass of the main loop, including all the jobs done by routines called within that loop.  The simplest way to introduce a delay is to insert halt instructions to wait for an interrupt at certain points in the main loop to wait for an interrupt.  As the Spectrum generates 50 interrupts per second, this means that main loops which have 1, 2 or 3 such pauses will run at 50, 25 or 17 frames per second respectively, so long as the other processing does not take up more than a frame to complete.  Generally speaking, it is not a good idea to have the player sprite moving more slowly than 17 frames per second.

Actually, the halt instruction can be quite handy.  In effect, it waits for the television scan line to reach the end of the screen.  This means that a good time to delete, move then re-display a sprite is immediately after a halt, because the scan line won’t catch up with the image, and there is no chance of flicker.  If you have your game’s status panel at the top of the screen, this means there is even further for the scan line to travel before it reaches the sprite area, and you can often squeeze in a couple of sprites after a halt without much danger of flickering.

The halt instruction can also be used in a loop to pause for longer periods.  The following code will pause for 100 fiftieths of a second – or two seconds:

       ld b,100            ; time to pause.
delay  halt                ; wait for an interrupt.
       djnz delay          ; repeat.

The Spectrum’s Clock

Unfortunately, halt is a blunt instrument. It always waits for the next interrupt, regardless of how long is left before the next one. Imagine a situation where your main loop takes 3/4 of a frame to do its processing most of the time, but every so often has periods where extra processing is involved, taking up an extra 1/2 a frame. Under these circumstances, a halt will keep the game at a constant 50 frames per second for the majority of the time, but as soon as the extra processing kicks in, the first interrupt has passed, and halt will wait for the next one, meaning that the game slows down to 25 frames per second periodically.

There is a way around this problem, and that is to count the number of frames that have elapsed since the last iteration of the main loop. On the Spectrum, the interrupt service routine in the ROM updates the Spectrum’s 24-bit frames counter 50 times per second, as well as doing other things. This counter is stored in the system variables at address 23672, so by checking this location once every iteration of the loop, we can tell how many interrupts have occurred since the last time we were at the same point. Naturally, if you want to write your own interrupt routines you will either have to use rst 56 to update the clock, or increment a frame counter yourself if you wish to use this method.

This routine is designed to stabilise a game to run at a more-or-less constant 25 frames per second:

wait   ld hl,pretim        ; previous time setting
       ld a,(23672)        ; current timer setting.
       sub (hl)            ; difference between the two.
       cp 2                ; have two frames elapsed yet?
       jr nc,wait0         ; yes, no more delay.
       jp wait
wait0  ld a,(23672)        ; current timer.
       ld (hl),a           ; store this setting.
       ret
pretim defb 0

Instead of simply sitting in a loop, you could perform some additional non-essential processing.  For example, I tend to cycle my sprites around the table I hold them in, changing the order in which they are displayed each loop to help prevent flickering.

Seeding Random Numbers

The Spectrum’s frame counter is useful for something else: it can be used to initialise the seed for random numbers.  Using the random number generator in the random numbers chapter, we can do this:

       ld a,(23672)        ; current timer.
       ld (seed),a         ; set first byte of random seed.

This is fine if we’re working on genuine hardware, and will ensure a game does not start with the same sequence of random numbers every time it is played. Unfortunately, emulator authors have a nasty habit of automatically loading tape files once opened – a practice which not only makes development difficult, it results in the machine always being in the same state every time a particular game is loaded, meaning random numbers can follow the same sequence every time that game is played. The solution for the games programmer is to wait for a debounced keypress as soon as our game has loaded, after which we can set our seed. This introduces a human element and ensures the random number generator is different every time.

Advertisements

How To Write ZX Spectrum Games – Chapter 11

October 2, 2013 Leave a comment

Enemy Movement

Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.

So we have our playfield, and can allow the player to manipulate a sprite around it, but what we now need are some enemy sprites for the player to avoid.  A new programmer could struggle here, but it really is far simpler than it first appears.

Patrolling Enemies

The easiest type of enemy to program is that with a fixed algorithm to follow, or a predetermined patrol route.  We covered one such technique in the Centipede game earlier.  Another very simple example is that found in games such as JetSet Willy, where a sprite travels in a single direction until it reaches the end of its patrol, then switches direction and heads back to its starting point, before changing direction again and starting the cycle again.  As you might imagine, these routines are incredibly easy to write.

Firstly we set up our alien structure table with minimum and maximum coordinate positions and the present direction.  It’s generally a good idea to comment these tables so we’ll do that too.

; Alien data table, 6 bytes per alien.
; ix     = graphic type, ie chicken/Amoebatron etc.
; ix + 1 = direction, 0=up, 1=right, 2=down, 3=left.
; ix + 2 = current x coordinate.
; ix + 3 = current y coordinate.
; ix + 4 = minimum x or y coord, depends on direction.
; ix + 5 = maximum x or y coord, depends on direction.

altab  defb 0,0,0,0,0,0
       defb 0,0,0,0,0,0
       defb 0,0,0,0,0,0

Then to manipulate our sprite we might write something like this:

       ld a,(ix+1)         ; alien movement direction.
       rra                 ; rotate low bit into carry.
       jr nc,movav         ; no carry = 0 or 2, must be vertical.

; direction is 1 or 3 so it's horizontal.

       rra                 ; rotate next bit into carry for test.
       jr nc,movar         ; direction 1 = move alien right.

; Move alien left.

moval  ld a,(ix+3)         ; get y coordinate.
       sub 2               ; move left.
       ld (ix+3),a
       cp (ix+4)           ; reached mimimum yet?
       jr z,movax          ; yes - change direction.
       jr c,movax          ; oops, gone past it.
       ret

; Move alien right.

movar  ld a,(ix+3)         ; get y coordinate.
       add a,2             ; move right.
       ld (ix+3),a
       cp (ix+5)           ; reached maximum yet?
       jr nc,movax         ; yes - change direction.
       ret

; Move alien vertically.

movav  rra                 ; test direction.
       jr c,movad          ; direction 2 is down.

; Move alien up.

movau  ld a,(ix+2)         ; get x coordinate.
       sub 2               ; move up.
       ld (ix+2),a
       cp (ix+4)           ; reached mimimum yet?
       jr z,movax          ; yes - change direction.
       ret

; Move alien down.

movad  ld a,(ix+2)         ; get x coordinate.
       add a,2             ; move down.
       ld (ix+2),a         ; new coordinate.
       cp (ix+5)           ; reached maximum yet?
       jr nc,movax         ; yes - change direction.
       ret

; Change alien direction.

movax  ld a,(ix+1)         ; direction flag.
       xor 2               ; switch direction, either
                           ; horizontally or vertically.
       ld (ix+1),a         ; set new direction.
       ret

If we wanted to go further we might introduce an extra flag to our table, ix+6, to control the speed of the sprite, and only move it, say, every other frame if the flag is set. While simple to write and easy on memory usage, this sort of movement is rather basic and predictable and of limited use. For more complicated patrolling enemies, for example the alien attack waves in a shoot-em-up, we need tables of coordinates and while the code is again easy to write, coordinate tables quickly chew up memory especially if both x and y coordinates are stored. To access such a table we need two bytes per sprite which act as a pointer to the coordinate table.

A typical section of code could look like this:

       ld l,(ix+2)         ; pointer low byte, little endian.
       ld h,(ix+3)         ; pointer high byte.
       ld c,(hl)           ; put x coordinate in c.
       inc hl              ; point to y coord.
       ld b,(hl)           ; put y coordinate in b.
       inc hl              ; point to next position.
       ld (ix+2),l         ; next pointer low byte.
       ld (ix+3),h         ; next pointer high byte.

The slightly more complicated example below demonstrates an 8-ship attack wave using a table of vertical coordinates. The horizontal position of each sprite moves left at a constant rate of 2 pixels per frame so there’s no need to bother storing it. It uses the shifter sprite routine from chapter 8 so the sprites are a little flickery, but that’s not important here.

mloop  halt                ; wait for TV beam.
       ld ix,entab         ; point to odd spaceships.
       call mship          ; move spaceships.
       halt
       ld ix,entab+4       ; point to even spaceships.
       call mship          ; move even spaceships.
       call gwave          ; generate fresh waves.
       jp mloop            ; back to start of loop.

; Move enemy spaceships.

mship  ld b,4              ; number to process.
mship0 push bc             ; store count.
       ld a,(ix)           ; get pointer low.
       ld l,a              ; put into l.
       ld h,(ix+1)         ; get high byte.
       or h                ; check pointer is set up.
       and a               ; is it?
       call nz,mship1      ; yes, process it then.
       ld de,8             ; skip to next-but-one entry.
       add ix,de           ; point to next enemy.
       pop bc              ; restore count.
       djnz mship0         ; repeat for all enemies.
       ret

mship1 push hl             ; store pointer to coordinate.
       call dship          ; delete this ship.
       pop hl              ; restore coordinate.
       ld a,(hl)           ; fetch next coordinate.
       inc hl              ; move pointer on.
       ld (ix),l           ; new pointer low byte.
       ld (ix+1),h         ; pointer high byte.
       ld (ix+2),a         ; set x coordinate.
       ld a,(ix+3)         ; fetch horizontal position.
       sub 2               ; move left 2 pixels.
       ld (ix+3),a         ; set new position.
       cp 240              ; reached the edge of the screen yet?
       jp c,dship          ; not at the moment, display at new position.
       xor a               ; zeroise accumulator.
       ld (ix),a           ; clear low byte of pointer.
       ld (ix+1),a         ; clear high byte of pointer.
       ld hl,numenm        ; number of enemies on screen.
       dec (hl)            ; one less with which to cope.
       ret

gwave  ld hl,shipc         ; ship counter.
       dec (hl)            ; one less.
       ld a,(hl)           ; check new value.
       cp 128              ; waiting for next attack?
       jr z,gwave2         ; attack is imminent so set it up.
       ret nc              ; yes.
       and 7               ; time to generate a new ship?
       ret nz              ; not yet it isn't.
       ld ix,entab         ; enemy table.
       ld de,4             ; size of each entry.
       ld b,8              ; number to check.
gwave0 ld a,(ix)           ; low byte of pointer.
       ld h,(ix+1)         ; high byte.
       or h                ; are they zero?
       jr z,gwave1         ; yes, this entry is empty.
       add ix,de           ; point to next ship slot.
       djnz gwave0         ; repeat until we find one.
       ret
gwave2 ld hl,wavnum        ; present wave number.
       ld a,(hl)           ; fetch current setting.
       inc a               ; next one along.
       and 3               ; start again after 4th wave.
       ld (hl),a           ; write new setting.
       ret
gwave1 ld hl,numenm        ; number of enemies on screen.
       inc (hl)            ; one more to deal with.
       ld a,(wavnum)       ; wave number.
       ld hl,wavlst        ; wave data pointers.
       rlca                ; multiple of 2.
       rlca                ; multiple of 4.
       ld e,a              ; displacement in e.
       ld d,0              ; no high byte.
       add hl,de           ; find wave address.
       ld a,(shipc)        ; ship counter.
       and 8               ; odd or even attack?
       rrca                ; make multiple of 2 accordingly.
       rrca
       ld e,a              ; displacement in e.
       ld d,0              ; no high byte.
       add hl,de           ; point to first or second half of attack.
       ld e,(hl)           ; low byte of attack pointer.
       inc hl              ; second byte.
       ld d,(hl)           ; high byte of attack pointer.
       ld (ix),e           ; low byte of pointer.
       ld (ix+1),d         ; high byte.
       ld a,(de)           ; fetch first coordinate.
       ld (ix+2),a         ; set x.
       ld (ix+3),240       ; start at right edge of screen.

; Display enemy ships.

dship  ld hl,shipg         ; sprite address.
       ld b,(ix+3)         ; y coordinate.
       ld c,(ix+2)         ; x coordinate.
       ld (xcoord),bc      ; set up sprite routine coords.
       jp sprite           ; call sprite routine.
shipc  defb 128            ; plane counter.
numenm defb 0              ; number of enemies.

; Attack wave coordinates.
; Only the vertical coordinate is stored as the ships all move left
; 2 pixels every frame.

coord0 defb 40,40,40,40,40,40,40,40
       defb 40,40,40,40,40,40,40,40
       defb 42,44,46,48,50,52,54,56
       defb 58,60,62,64,66,68,70,72
       defb 72,72,72,72,72,72,72,72
       defb 72,72,72,72,72,72,72,72
       defb 70,68,66,64,62,60,58,56
       defb 54,52,50,48,46,44,42,40
       defb 40,40,40,40,40,40,40,40
       defb 40,40,40,40,40,40,40,40
       defb 38,36,34,32,30,28,26,24
       defb 22,20,18,16,14,12,10,8
       defb 6,4,2,0,2,4,6,8
       defb 10,12,14,16,18,20,22,24
       defb 26,28,30,32,34,36,38,40
coord1 defb 136,136,136,136,136,136,136,136
       defb 136,136,136,136,136,136,136,136
       defb 134,132,130,128,126,124,122,120
       defb 118,116,114,112,110,108,106,104
       defb 104,104,104,104,104,104,104,104
       defb 104,104,104,104,104,104,104,104
       defb 106,108,110,112,114,116,118,120
       defb 122,124,126,128,130,132,134,136
       defb 136,136,136,136,136,136,136,136
       defb 136,136,136,136,136,136,136,136
       defb 138,140,142,144,146,148,150,152
       defb 154,156,158,160,162,164,166,168
       defb 170,172,174,176,174,172,170,168
       defb 166,164,162,160,158,156,154,152
       defb 150,148,146,144,142,140,138,136

; List of attack waves.

wavlst defw coord0,coord0,coord1,coord1
       defw coord1,coord0,coord0,coord1

wavnum defb 0              ; current wave pointer.

; Spaceship sprite.

shipg  defb 248,252,48,24,24,48,12,96,24,48,31,243,127,247,255,247
       defb 255,247,127,247,31,243,24,48,12,96,24,48,48,24,248,252

sprit7 xor 7               ; complement last 3 bits.
       inc a               ; add one for luck!
sprit3 rl d                ; rotate left...
       rl c                ; ...into middle byte...
       rl e                ; ...and finally into left character cell.
       dec a               ; count shifts we've done.
       jr nz,sprit3        ; return until all shifts complete.

; Line of sprite image is now in e + c + d, we need it in form c + d + e.

       ld a,e              ; left edge of image is currently in e.
       ld e,d              ; put right edge there instead.
       ld d,c              ; middle bit goes in d.
       ld c,a              ; and the left edge back into c.
       jr sprit0           ; we've done the switch so transfer to screen.

sprite ld a,(xcoord)       ; draws sprite (hl).
       ld (tmp1),a         ; store vertical.
       call scadd          ; calculate screen address.
       ld a,16             ; height of sprite in pixels.
sprit1 ex af,af'           ; store loop counter.
       push de             ; store screen address.
       ld c,(hl)           ; first sprite graphic.
       inc hl              ; increment poiinter to sprite data.
       ld d,(hl)           ; next bit of sprite image.
       inc hl              ; point to next row of sprite data.
       ld (tmp0),hl        ; store in tmp0 for later.
       ld e,0              ; blank right byte for now.
       ld a,b              ; b holds y position.
       and 7               ; how are we straddling character cells?
       jr z,sprit0         ; we're not straddling them, don't bother shifting.
       cp 5                ; 5 or more right shifts needed?
       jr nc,sprit7        ; yes, shift from left as it's quicker.
       and a               ; oops, carry flag is set so clear it.
sprit2 rr c                ; rotate left byte right...
       rr d                ; ...through middle byte...
       rr e                ; ...into right byte.
       dec a               ; one less shift to do.
       jr nz,sprit2        ; return until all shifts complete.
sprit0 pop hl              ; pop screen address from stack.
       ld a,(hl)           ; what's there already.
       xor c               ; merge in image data.
       ld (hl),a           ; place onto screen.
       inc l               ; next character cell to right please.
       ld a,(hl)           ; what's there already.
       xor d               ; merge with middle bit of image.
       ld (hl),a           ; put back onto screen.
       inc hl              ; next bit of screen area.
       ld a,(hl)           ; what's already there.
       xor e               ; right edge of sprite image data.
       ld (hl),a           ; plonk it on screen.
       ld a,(tmp1)         ; temporary vertical coordinate.
       inc a               ; next line down.
       ld (tmp1),a         ; store new position.
       and 63              ; are we moving to next third of screen?
       jr z,sprit4         ; yes so find next segment.
       and 7               ; moving into character cell below?
       jr z,sprit5         ; yes, find next row.
       dec hl              ; left 2 bytes.
       dec l               ; not straddling 256-byte boundary here.
       inc h               ; next row of this character cell.
sprit6 ex de,hl            ; screen address in de.
       ld hl,(tmp0)        ; restore graphic address.
       ex af,af'           ; restore loop counter.
       dec a               ; decrement it.
       jp nz,sprit1        ; not reached bottom of sprite yet to repeat.
       ret                 ; job done.
sprit4 ld de,30            ; next segment is 30 bytes on.
       add hl,de           ; add to screen address.
       jp sprit6           ; repeat.
sprit5 ld de,63774         ; minus 1762.
       add hl,de           ; subtract 1762 from physical screen address.
       jp sprit6           ; rejoin loop.
scadd  ld a,(xcoord)       ; fetch vertical coordinate.
       ld e,a              ; store that in e.

; Find line within cell.

       and 7               ; line 0-7 within character square.
       add a,64            ; 64 * 256 = 16384 = start of screen display.
       ld d,a              ; line * 256.

; Find which third of the screen we're in.

       ld a,e              ; restore the vertical.
       and 192             ; segment 0, 1 or 2 multiplied by 64.
       rrca                ; divide this by 8.
       rrca
       rrca                ; segment 0-2 multiplied by 8.
       add a,d             ; add to d give segment start address.
       ld d,a

; Find character cell within segment.

       ld a,e              ; 8 character squares per segment.
       rlca                ; divide x by 8 and multiply by 32,
       rlca                ; net calculation: multiply by 4.
       and 224             ; mask off bits we don't want.
       ld e,a              ; vertical coordinate calculation done.

; Add the horizontal element.

       ld a,(ycoord)       ; y coordinate.
       rrca                ; only need to divide by 8.
       rrca
       rrca
       and 31              ; squares 0 - 31 across screen.
       add a,e             ; add to total so far.
       ld e,a              ; de = address of screen.
       ret

xcoord defb 0              ; display coord.
ycoord defb 0              ; display coord.
tmp0   defw 0              ; workspace.
tmp1   defb 0              ; temporary vertical position.

; Enemy spaceship table, 8 entries x 4 bytes each.

entab  defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0

Intelligent Aliens

So far we have dealt with predictable drones, but what if we want to give the player the illusion that enemy sprites are thinking for themselves?  One way we could start to do this would be to give them an entirely random decision making process.

Here is the source code for Turbomania, a game originally written for the 1K coding competition in 2005.  It’s very simple, but incorporates purely random movement.  Enemy cars travel in a direction until they can no longer move, then select another direction at random.  Additionally, a car may change direction at random even if it can continue in its present direction.  It’s very primitive of course, just take a look at the mcar routine and you’ll see exactly what I mean.

       org 24576

; Constants.

YELLOW equ 49              ; dull yellow attribute.
YELLOB equ YELLOW + 64     ; bright yellow attribute.

; Main game code.

; Clear the screen to give green around the edges.

       ld hl,23693         ; system variable for attributes.
       ld (hl),36          ; want green background.

waitk  ld a,(23560)        ; read keyboard.
       cp 32               ; is SPACE pressed?
       jr nz,waitk         ; no, wait.
       call nexlev         ; play the game.
       jr waitk            ; SPACE to restart game.

; Clear down level data.

nexlev call 3503           ; clear the screen.
       ld hl,rmdat         ; room data.
       ld de,rmdat+1
       ld (hl),1           ; set up a shadow block.
       ld bc,16            ; length of room minus first byte.
       ldir                ; copy to rest of first row.
       ld bc,160           ; length of room minus first row.
       ld (hl),b           ; clear first byte.
       ldir                ; clear room data.

; Set up the default blocks.

       ld c,15             ; last block position.
popbl0 ld b,9              ; last row.
popbl1 call filblk         ; fill the block.
       dec b               ; one column up.
       jr z,popbl2         ; done column, move on.
       dec b               ; and again.
       jr popbl1
popbl2 dec c               ; move on row.
       jr z,popbl3         ; done column, move on.
       dec c               ; next row.
       jr popbl0

; Now draw the bits unique to this level.

popbl3 ld b,7              ; number of blocks to insert.
popbl5 push bc             ; store counter.
       call random         ; get a random number.
       and 6               ; even numbers in range 0-6 please.
       add a,2             ; make it 2-8.
       ld b,a              ; that's the column.
popbl4 call random         ; another number.
       and 14              ; even numbers 0-12 wanted.
       cp 14               ; higher than we want?
       jr nc,popbl4        ; yes, try again.
       inc a               ; place it in range 1-13.
       ld c,a              ; that's the row.
       call filblk         ; fill block.
popbl6 call random         ; another random number.
       and 14              ; only want 0-8.
       cp 9                ; above number we want?
       jr nc,popbl6        ; try again.
       inc a               ; make it 1-9.
       ld b,a              ; vertical coordinate.
       call random         ; get horizontal block.
       and 14              ; even, 0-14.
       ld c,a              ; y position.
       call filblk         ; fill in that square.
       pop bc              ; restore count.
       djnz popbl5         ; one less to do.

       xor a               ; zero.
       ld hl,playi         ; player's intended direction.
       ld (hl),a           ; default direction.
       inc hl              ; point to display direction.
       ld (hl),a           ; default direction.
       inc hl              ; player's next direction.
       ld (hl),a           ; default direction.
       out (254),a         ; set border colour while we're at it.
       call atroom         ; show current level layout.

       ld hl,168+8*256     ; coordinates.
       ld (encar2+1),hl    ; set up second car position.
       ld h,l              ; y coord at right.
       ld (encar1+1),hl    ; set up first car position.
       ld l,40             ; x at top of screen.
       ld (playx),hl       ; start player off here.
       ld hl,encar1        ; first car.
       call scar           ; show it.
       ld hl,encar2        ; second car.
       call scar           ; show it.
       call dplayr         ; show player sprite.
       call blkcar         ; make player's car black.

; Two-second delay before we start.

       ld b,100            ; delay length.
waitt  halt                ; wait for interrupt.
       djnz waitt          ; repeat.

mloop  halt                ; electron beam in top left.
       call dplayr         ; delete player.

; Make attributes blue ink again.

       call gpatts         ; get player attributes.
       defb 17,239,41      ; remove green paper, add blue paper + ink.
       call attblk         ; set road colours.

; Move player's car.

       ld a,(playd)        ; player direction.
       ld bc,(playx)       ; player coordinates.
       call movc           ; move coords.
       ld hl,(dispx)       ; new coords.
       ld (playx),hl       ; set new player position.

; Can we change direction?

       ld a,(playi)        ; player's intended direction.
       ld bc,(playx)       ; player coordinates.
       call movc           ; move coords.
       call z,setpn        ; set player's new direction.

; Change direction.

       ld a,(nplayd)       ; new player direction.
       ld (playd),a        ; set current direction.

       call dplayr         ; redisplay at new position.

; Set attributes of car.

       call blkcar         ; make player car black.

; Controls.

       ld a,239            ; keyboard row 6-0 = 61438.
       ld e,1              ; direction right.
       in a,(254)          ; read keys.
       rra                 ; player moved right?
       call nc,setpd       ; yes, set player direction.
       ld e,3              ; direction left.
       rra                 ; player moved left?
       call nc,setpd       ; yes, set player direction.
       ld a,247            ; 63486 is port for row 1-5.
       ld e,0              ; direction up.
       in a,(254)          ; read keys.
       and 2               ; check 2nd key in (2).
       call z,setpd        ; set direction.
       ld a,251            ; 64510 is port for row Q-T.
       ld e,2              ; direction down.
       in a,(254)          ; read keys.
       and e               ; check 2nd key from edge (W)..
       call z,setpd        ; set direction.

; Enemy cars.

       ld hl,encar1        ; enemy car 1.
       push hl             ; store pointer.
       call procar         ; process the car.
       pop hl              ; restore car pointer.
       call coldet         ; check for collisions.
       ld hl,encar2        ; enemy car 2.
       push hl             ; store pointer.
       halt                ; synchronise with display.
       call procar         ; process the car.
       pop hl              ; restore car pointer.
       call coldet         ; check for collisions.

; Count remaining yellow spaces.

       ld hl,22560         ; address.
       ld bc,704           ; attributes to count.
       ld a,YELLOB         ; attribute we're seeking.
       cpir                ; count characters.
       ld a,b              ; high byte of result.
       or c                ; combine with low byte.
       jp z,nexlev         ; none left, go to next level.

; End of main loop.

       jp mloop

; Black car on cyan paper.

blkcar call gpatts         ; get player attributes.
       defb 17,232,40      ; remove red paper/blue ink, add blue paper.

; Set 16x16 pixel attribute block.

attblk call attlin         ; paint horizontal line.
       call attlin         ; paint another line.
       ld a,c              ; vertical position.
       and 7               ; is it straddling cells?
       ret z               ; no, so no third line.

attlin call setatt         ; paint the road.
       call setatt         ; and again.
       ld a,b              ; horizontal position.
       and 7               ; straddling blocks?
       jr z,attln0         ; no, leave third cell as it is.
       call setatt         ; set attribute.
       dec l               ; back one cell again.
attln0 push de             ; preserve colours.
       ld de,30            ; distance to next one.
       add hl,de           ; point to next row down.
       pop de              ; restore colour masks.
       ret

; Set single cell attribute.

setatt ld a,(hl)           ; fetch attribute cell contents.
       and e               ; remove colour elements in c register.
       or d                ; add those in b to form new colour.
       ld (hl),a           ; set colour.
       inc l               ; next cell.
       ret

; Collision detection, based on coordinates.

coldet call getabc         ; get coords.
       ld a,(playx)        ; horizontal position.
       sub c               ; compare against car x.
       jr nc,coldt0        ; result was positive.
       neg                 ; it was negative, reverse sign.
coldt0 cp 16               ; within 15 pixels?
       ret nc              ; no collision.
       ld a,(playy)        ; player y.
       sub b               ; compare against car y.
       jr nc,coldt1        ; result was positive.
       neg                 ; it was negative, reverse sign.
coldt1 cp 16               ; within 15 pixels?
       ret nc              ; no collision.
       pop de              ; remove return address from stack.
       ret

setpd  ex af,af'
       ld a,e              ; direction.
       ld (playi),a        ; set intended direction.
       ex af,af'
       ret

setpn  ld a,(playi)        ; new intended direction.
       ld (nplayd),a       ; set next direction.
       ret

; Move coordinates of sprite in relevant direction.

movc   ld (dispx),bc       ; default position.
       and a               ; direction zero.
       jr z,movcu          ; move up.
       dec a               ; direction 1.
       jr z,movcr          ; move up.
       dec a               ; direction 2.
       jr z,movcd          ; move up.
movcl  dec b               ; left one pixel.
       dec b               ; left again.
movc0  call chkpix         ; check pixel attributes.
       ld (dispx),bc       ; new coords.
       ret
movcu  dec c               ; up a pixel.
       dec c               ; and again.
       jr movc0
movcr  inc b               ; right one pixel.
       inc b               ; right again.
       jr movc0
movcd  inc c               ; down a pixel.
       inc c               ; once more.
       jr movc0

; Check pixel attributes for collision.
; Any cells with green ink are solid.

chkpix call ataddp         ; get pixel attribute address.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       inc hl              ; next square to the right.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       inc hl              ; next square to the right.
       ld a,b              ; horizontal position.
       and 7               ; straddling cells?
       jr z,chkpx1         ; no, look down next.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
chkpx1 ld de,30            ; distance to next cell down.
       add hl,de           ; point there.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       inc hl              ; next square to the right.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       inc hl              ; next square to the right.
       ld a,b              ; horizontal position.
       and 7               ; straddling cells?
       jr z,chkpx2         ; no, look down next.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
chkpx2 ld a,c              ; distance from top of screen.
       and 7               ; are we straddling cells vertically?
       ret z               ; no, move is therefore okay.
       add hl,de           ; point there.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       inc hl              ; next square to the right.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       inc hl              ; next square to the right.
       ld a,b              ; horizontal position.
       and 7               ; straddling cells?
       ret z               ; no, move is fine.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       ret                 ; go ahead.

chkpx0 pop de              ; remove return address from stack.
       ret

; Fill a block in the map.

; Get block address.

filblk ld a,b              ; row number.
       rlca                ; multiply by 16.
       rlca
       rlca
       rlca
       add a,c             ; add to displacement total.
       ld e,a              ; that's displacement low.
       ld d,0              ; no high byte required.
       ld hl,rmdat         ; room data address.
       add hl,de           ; add to block address.

; Block address is in hl, let's fill it.

       ld (hl),2           ; set block on.
       ld de,16            ; distance to next block down.
       add hl,de           ; point there.
       ld a,(hl)           ; check it.
       and a               ; is it set yet?
       ret nz              ; yes, don't overwrite.
       ld (hl),1           ; set the shadow.
       ret

; Draw a screen consisting entirely of attribute blocks.

atroom ld hl,rmdat         ; room data.
       ld a,1              ; start at row 1.
       ld (dispx),a        ; set up coordinate.
       ld b,11             ; row count.
atrm0  push bc             ; store counter.
       ld b,15             ; column count.
       ld a,1              ; column number.
       ld (dispy),a        ; set to left of screen.
atrm1  push bc             ; store counter.
       ld a,(hl)           ; get next block type.
       push hl             ; store address of data.
       rlca                ; double block number.
       rlca                ; and again for multiple of 4.
       ld e,a              ; displacement to block address.
       ld d,0              ; no high byte required.
       ld hl,blkatt        ; block attributes.
       add hl,de           ; point to block we want.
       call atadd          ; get address for screen position.
       ldi                 ; transfer first block.
       ldi                 ; and the second.
       ld bc,30            ; distance to next row.
       ex de,hl            ; switch cell and screen address.
       add hl,bc           ; point to next row down.
       ex de,hl            ; switch them back.
       ldi                 ; do third cell.
       ldi                 ; fourth attribute cell.
       ld hl,dispy         ; column number.
       inc (hl)            ; move across one cell.
       inc (hl)            ; and another.
       pop hl              ; restore room address.
       pop bc              ; restore column counter.
       inc hl              ; point to next block.
       djnz atrm1          ; do rest of row.
       inc hl              ; skip one char so lines are round 16.
       ld a,(dispx)        ; vertical position.
       add a,2             ; look 2 cells down.
       ld (dispx),a        ; new row.
       pop bc              ; restore column counter.
       djnz atrm0          ; do remaining rows.
       ret

; Background block attributes.

blkatt defb YELLOB,YELLOB  ; space.
       defb YELLOB,YELLOB
       defb YELLOW,YELLOW  ; shadow space.
       defb YELLOB,YELLOB
       defb 124,68         ; black/white chequered flag pattern.
       defb 68,124

; Calculate address of attribute for character at (dispx, dispy).

atadd  push hl             ; need to preserve hl pair.
       ld hl,(dispx)       ; coords to check, in char coords.
       add hl,hl           ; multiply x and y by 8.
       add hl,hl
       add hl,hl
       ld b,h              ; copy y coord to b.
       ld c,l              ; put x coord in c.
       call ataddp         ; get pixel address.
       ex de,hl            ; put address in de.
       pop hl              ; restore hl.
       ret

; Get player attributes.

gpatts ld bc,(playx)       ; player coordinates.

; Calculate address of attribute for pixel at (c, b).

ataddp ld a,c              ; Look at the vertical first.
       rlca                ; divide by 64.
       rlca                ; quicker than 6 rrca operations.
       ld l,a              ; store in l register for now.
       and 3               ; mask to find segment.
       add a,88            ; attributes start at 88*256=22528.
       ld h,a              ; that's our high byte sorted.
       ld a,l              ; vertical/64 - same as vertical*4.
       and 224             ; want a multiple of 32.
       ld l,a              ; vertical element calculated.
       ld a,b              ; get horizontal position.
       rra                 ; divide by 8.
       rra
       rra
       and 31              ; want result in range 0-31.
       add a,l             ; add to existing low byte.
       ld l,a              ; that's the low byte done.
       ld a,(hl)           ; get cell contents.
       ret                 ; attribute address now in hl.

; Move car - change of direction required.

mcarcd ld a,(hl)           ; current direction.
       inc a               ; turn clockwise.
       and 3               ; only 4 directions.
       ld (hl),a           ; new direction.

; Move an enemy car.

mcar   push hl             ; preserve pointer to car.
       call getabc         ; fetch coordinates and direction.
       call movc           ; move the car.
       pop hl              ; refresh car pointer.
       jr nz,mcarcd        ; can't move there, turn around.
       inc hl              ; point to x.
       ld a,c              ; store x pos in c.
       ld (hl),a           ; x position.
       inc hl              ; point to y.
       ld (hl),b           ; new placing.
       or b                ; combine the two.
       and 31              ; find position straddling cells.
       cp 8                ; are we at a valid turning point?
       ret nz              ; no, can't change direction.
       ld a,r              ; crap random number.
       cp 23               ; check it's below this value.
       ret nc              ; it isn't, don't change.
       push hl             ; store car pointer.
       call random         ; get random number.
       pop hl              ; restore car.
       dec hl              ; back to x coordinate.
       dec hl              ; back again to direction.
       and 3               ; direction is in range 0-3.
       ld (hl),a           ; new direction.
       ret

; Fetch car coordinates and direction.

getabc ld a,(hl)           ; get direction.
       inc hl              ; point to x position.
       ld c,(hl)           ; x coordinate.
       inc hl              ; point to y.
       ld b,(hl)           ; y position.
       ret

; Process car to which hl points.

procar push hl             ; store pointer.
       push hl             ; store pointer.
       call scar           ; delete car.
       pop hl              ; restore pointer to car.
       call mcar           ; move car.
       pop hl              ; restore pointer to car.

; Show enemy car.

scar   call getabc         ; get coords and direction.
       jr dplay0

; Display player sprite.

dplayr ld bc,(playx)       ; player coords.
       ld a,(playd)        ; player direction.
dplay0 rrca                ; multiply by 32.
       rrca
       rrca
       ld e,a              ; sprite * 32 in low byte.
       ld d,0              ; no high byte.
       ld hl,cargfx        ; car graphics.
       add hl,de           ; add displacement to sprite.
       jr sprite           ; show the sprite.

; This is the sprite routine and expects coordinates in (c ,b) form,
; where c is the vertical coord from the top of the screen (0-176), and
; b is the horizontal coord from the left of the screen (0 to 240).
; Sprite data is stored as you'd expect in its unshifted form as this
; routine takes care of all the shifting itself.  This means that sprite
; handling isn't particularly fast but the graphics only take 1/8th of the
; space they would require in pre-shifted form.

; On entry HL must point to the unshifted sprite data.

sprit7 xor 7               ; complement last 3 bits.
       inc a               ; add one for luck!
sprit3 rl d                ; rotate left...
       rl c                ; ...into middle byte...
       rl e                ; ...and finally into left character cell.
       dec a               ; count shifts we've done.
       jr nz,sprit3        ; return until all shifts complete.

; Line of sprite image is now in e + c + d, we need it in form c + d + e.

       ld a,e              ; left edge of image is currently in e.
       ld e,d              ; put right edge there instead.
       ld d,c              ; middle bit goes in d.
       ld c,a              ; and the left edge back into c.
       jr sprit0           ; we've done the switch so transfer to screen.

sprite ld (dispx),bc       ; store coords in dispx for now.
       call scadd          ; calculate screen address.
       ld a,16             ; height of sprite in pixels.
sprit1 ex af,af'           ; store loop counter.
       push de             ; store screen address.
       ld c,(hl)           ; first sprite graphic.
       inc hl              ; increment poiinter to sprite data.
       ld d,(hl)           ; next bit of sprite image.
       inc hl              ; point to next row of sprite data.
       ld (sprtmp),hl      ; store it for later.
       ld e,0              ; blank right byte for now.
       ld a,b              ; b holds y position.
       and 7               ; how are we straddling character cells?
       jr z,sprit0         ; we're not straddling them, don't bother shifting.
       cp 5                ; 5 or more right shifts needed?
       jr nc,sprit7        ; yes, shift from left as it's quicker.
       and a               ; oops, carry flag is set so clear it.
sprit2 rr c                ; rotate left byte right...
       rr d                ; ...through middle byte...
       rr e                ; ...into right byte.
       dec a               ; one less shift to do.
       jr nz,sprit2        ; return until all shifts complete.
sprit0 pop hl              ; pop screen address from stack.
       ld a,(hl)           ; what's there already.
       xor c               ; merge in image data.
       ld (hl),a           ; place onto screen.
       inc l               ; next character cell to right please.
       ld a,(hl)           ; what's there already.
       xor d               ; merge with middle bit of image.
       ld (hl),a           ; put back onto screen.
       inc l               ; next bit of screen area.
       ld a,(hl)           ; what's already there.
       xor e               ; right edge of sprite image data.
       ld (hl),a           ; plonk it on screen.
       ld a,(dispx)        ; vertical coordinate.
       inc a               ; next line down.
       ld (dispx),a        ; store new position.
       and 63              ; are we moving to next third of screen?
       jr z,sprit4         ; yes so find next segment.
       and 7               ; moving into character cell below?
       jr z,sprit5         ; yes, find next row.
       dec l               ; left 2 bytes.
       dec l               ; not straddling 256-byte boundary here.
       inc h               ; next row of this character cell.
sprit6 ex de,hl            ; screen address in de.
       ld hl,(sprtmp)      ; restore graphic address.
       ex af,af'           ; restore loop counter.
       dec a               ; decrement it.
       jp nz,sprit1        ; not reached bottom of sprite yet to repeat.
       ret                 ; job done.
sprit4 ld de,30            ; next segment is 30 bytes on.
       add hl,de           ; add to screen address.
       jp sprit6           ; repeat.
sprit5 ld de,63774         ; minus 1762.
       add hl,de           ; subtract 1762 from physical screen address.
       jp sprit6           ; rejoin loop.

; This routine returns a screen address for (c, b) in de.

scadd  ld a,c              ; get vertical position.
       and 7               ; line 0-7 within character square.
       add a,64            ; 64 * 256 = 16384 (Start of screen display)
       ld d,a              ; line * 256.
       ld a,c              ; get vertical again.
       rrca                ; multiply by 32.
       rrca
       rrca
       and 24              ; high byte of segment displacement.
       add a,d             ; add to existing screen high byte.
       ld d,a              ; that's the high byte sorted.
       ld a,c              ; 8 character squares per segment.
       rlca                ; 8 pixels per cell, mulplied by 4 = 32.
       rlca                ; cell x 32 gives position within segment.
       and 224             ; make sure it's a multiple of 32.
       ld e,a              ; vertical coordinate calculation done.
       ld a,b              ; y coordinate.
       rrca                ; only need to divide by 8.
       rrca
       rrca
       and 31              ; squares 0 - 31 across screen.
       add a,e             ; add to total so far.
       ld e,a              ; hl = address of screen.
       ret

; Pseudo-random number generator.
; Steps a pointer through the ROM (held in seed), returning the contents
; of the byte at that location.

random ld hl,(seed)        ; pointer to ROM.
       res 5,h             ; stay within first 8K of ROM.
       ld a,(hl)           ; get "random" number from location.
       xor l               ; more randomness.
       inc hl              ; increment pointer.
       ld (seed),hl        ; new position.
       ret

; Sprite graphic data.
; Going up first.

cargfx defb 49,140,123,222,123,222,127,254,55,236,15,240,31,248,30,120
       defb 29,184,108,54,246,111,255,255,247,239,246,111,103,230,3,192

; Second image looks right.

       defb 60,0,126,14,126,31,61,223,11,238,127,248,252,254,217,127
       defb 217,127,252,254,127,248,11,238,61,223,126,31,126,14,60,0

; Third is pointing down.

       defb 3,192,103,230,246,111,247,239,255,255,246,111,108,54,29,184
       defb 30,120,31,248,15,240,55,236,127,254,123,222,123,222,49,140

; Last car looks left.

       defb 0,60,112,126,248,126,251,188,119,208,31,254,127,63,254,155
       defb 254,155,127,63,31,254,119,208,251,188,248,126,112,126,0,60

; Variables used by the game.

       org 32768

playi  equ $               ; intended direction when turn is possible.
playd  equ playi+1         ; player's current direction.
nplayd equ playd+1         ; next player direction.
playx  equ nplayd+1        ; player x.
playy  equ playx+1         ; player's y coordinate.

encar1 equ playy+1         ; enemy car 1.
encar2 equ encar1+3        ; enemy car 2.

dispx  equ encar2+3        ; general-use coordinates.
dispy  equ dispx+1
seed   equ dispy+1         ; random number seed.
sprtmp equ seed+2          ; sprite temporary address.
termin equ sprtmp+2        ; end of variables.

rmdat  equ 49152

If you have assembled this game and tried it out you will realise that it quickly becomes boring. It is very easy to stay out of the reach of enemy cars to cover one side of the track, then wait until the cars move and cover the other side. There is no hunter-killer aspect in this algorithm so the player is never chased down. What’s more, this routine is so simple cars will reverse direction without warning. In most games this is only acceptable if a sprite reaches a dead end and cannot move in any other direction.

Perhaps we should instead be writing routines where aliens interact with the player, and home in on him. Well, the most basic algorithm would be something along the lines of a basic x/y coordinate check, moving an alien sprite towards the player. The routine below shows how this might be achieved, the homing routine almov is the one which moves the chasing sprite around. Try guiding the number 1 block around the screen with keys ASD and F, and the number 2 block will follow you around the screen. However, in doing this we soon discover the basic flaw with this type of chase – it is very easy to trap the enemy sprite in a corner because the routine isn’t intelligent enough to move backwards in order to get around obstacles.

; Randomly cover screen with yellow blocks.

       ld de,1000          ; address in ROM.
       ld b,64             ; number of cells to colour.
yello0 push bc             ; store register.
       ld a,(de)           ; get first random coord.
       and 127             ; half height of screen.
       add a,32            ; at least 32 pixels down.
       ld c,a              ; x coord.
       inc de              ; next byte of ROM.
       ld a,(de)           ; fetch value.
       inc de              ; next byte of ROM.
       ld b,a              ; y coord.
       call ataddp         ; find attribute address.
       ld (hl),48          ; set attributes.
       pop bc              ; restore loop counter.
       djnz yello0         ; repeat a few times.

       ld ix,aldat         ; alien data.
       call dal            ; display alien.
       call dpl            ; display player.

mloop  halt                ; wait for electron beam.
       call dal            ; delete alien.
       call almov          ; alien movement.
       call dal            ; display alien.
       halt                ; wait for electron beam.
       call dpl            ; delete player.
       call plcon          ; player controls.
       call dpl            ; display the player.
       jp mloop            ; back to start of main loop.

aldat  defb 0,0,0          ; alien data.

; Display/delete alien.

dal    ld c,(ix)           ; vertical position.
       ld b,(ix+1)         ; horizontal position.
       ld (xcoord),bc      ; set sprite coordinates.
       ld hl,algfx         ; alien graphic.
       jp sprite           ; xor sprite onto screen.

; Display/delete player sprite.

dpl    ld bc,(playx)       ; coordinates.
       ld (xcoord),bc      ; set the display coordinates.
       ld hl,plgfx         ; player graphic.
       jp sprite           ; xor sprite on or off the screen.

; Player control.

plcon  ld bc,65022         ; port for keyboard row.
       in a,(c)            ; read keyboard.
       ld b,a              ; store result in b register.
       rr b                ; check outermost key.
       call nc,mpl         ; player left.
       rr b                ; check next key.
       call nc,mpr         ; player right.
       rr b                ; check next key.
       call nc,mpd         ; player down.
       rr b                ; check next key.
       call nc,mpu         ; player up.
       ret
mpl    ld hl,playy         ; coordinate.
       ld a,(hl)           ; check value.
       and a               ; at edge of screen?
       ret z               ; yes, can't move that way.
       sub 2               ; move 2 pixels.
       ld (hl),a           ; new setting.
       ret
mpr    ld hl,playy         ; coordinate.
       ld a,(hl)           ; check value.
       cp 240              ; at edge of screen?
       ret z               ; yes, can't move that way.
       add a,2             ; move 2 pixels.
       ld (hl),a           ; new setting.
       ret
mpu    ld hl,playx         ; coordinate.
       ld a,(hl)           ; check value.
       and a               ; at edge of screen?
       ret z               ; yes, can't move that way.
       sub 2               ; move 2 pixels.
       ld (hl),a           ; new setting.
       ret
mpd    ld hl,playx         ; coordinate.
       ld a,(hl)           ; check value.
       cp 176              ; at edge of screen?
       ret z               ; yes, can't move that way.
       add a,2             ; move 2 pixels.
       ld (hl),a           ; new setting.
       ret

; Alien movement routine.

almov  ld a,(playx)        ; player x coordinate.
       ld c,(ix)           ; alien x.
       ld b,(ix+1)         ; alien y.
       cp c                ; check alien x.
       jr z,alv0           ; they're equal, do horizontal.
       jr c,alu            ; alien is below, move up.
ald    inc c               ; alien is above, move down.
       jr alv0             ; now check position for walls.
alu    dec c               ; move down.
alv0   call alchk          ; check attributes.
       cp 56               ; are they okay?
       jr z,alv1           ; yes, set x coord.
       ld c,(ix)           ; restore old x coordinate.
       jr alh              ; now do horizontal.
alv1   ld (ix),c           ; new x coordinate.
alh    ld a,(playy)        ; player horizontal.
       cp b                ; check alien horizontal.
       jr z,alok           ; they're equal, check for collision.
       jr c,all            ; alien is to right, move left.
alr    inc b               ; alien is to left, move right.
       jr alok             ; check for walls.
all    dec b               ; move right.
alok   call alchk          ; check attributes.
       cp 56               ; are they okay?
       ret nz              ; no, don't set new y coord.
       ld (ix+1),b         ; set new y.
       ret

; Check attributes at alien position (c, b).

alchk  call ataddp         ; get attribute address.
       ld a,3              ; cells high.
alchk0 ex af,af'           ; store loop counter.
       ld a,(hl)           ; check cell colour.
       cp 56               ; is it black on white?
       ret nz              ; no, can't move here.
       inc hl              ; cell right.
       ld a,(hl)           ; check cell colour.
       cp 56               ; is it black on white?
       ret nz              ; no, can't move here.
       inc hl              ; cell right.
       ld a,(hl)           ; check cell colour.
       cp 56               ; is it black on white?
       ret nz              ; no, can't move here.
       ld de,30            ; distance to next cell down.
       add hl,de           ; look there.
       ex af,af'           ; height counter.
       dec a               ; one less to go.
       jr nz,alchk0        ; repeat for all rows.
       ld (ix),c           ; set new x.
       ld (ix+1),b         ; set new y.
       ret

; Calculate address of attribute for pixel at (c, b).

ataddp ld a,c              ; Look at the vertical first.
       rlca                ; divide by 64.
       rlca                ; quicker than 6 rrca operations.
       ld l,a              ; store in l register for now.
       and 3               ; mask to find segment.
       add a,88            ; attributes start at 88*256=22528.
       ld h,a              ; that's our high byte sorted.
       ld a,l              ; vertical/64 - same as vertical*4.
       and 224             ; want a multiple of 32.
       ld l,a              ; vertical element calculated.
       ld a,b              ; get horizontal position.
       rra                 ; divide by 8.
       rra
       rra
       and 31              ; want result in range 0-31.
       add a,l             ; add to existing low byte.
       ld l,a              ; that's the low byte done.
       ret                 ; attribute address now in hl.

playx  defb 80             ; player coordinates.
playy  defb 120
xcoord defb 0              ; general-purpose coordinates.
ycoord defb 0

; Shifter sprite routine.

sprit7 xor 7               ; complement last 3 bits.
       inc a               ; add one for luck!
sprit3 rl d                ; rotate left...
       rl c                ; ...into middle byte...
       rl e                ; ...and finally into left character cell.
       dec a               ; count shifts we've done.
       jr nz,sprit3        ; return until all shifts complete.

; Line of sprite image is now in e + c + d, we need it in form c + d + e.

       ld a,e              ; left edge of image is currently in e.
       ld e,d              ; put right edge there instead.
       ld d,c              ; middle bit goes in d.
       ld c,a              ; and the left edge back into c.
       jr sprit0           ; we've done the switch so transfer to screen.

sprite ld a,(xcoord)       ; draws sprite (hl).
       ld (tmp1),a         ; store vertical.
       call scadd          ; calculate screen address.
       ld a,16             ; height of sprite in pixels.
sprit1 ex af,af'           ; store loop counter.
       push de             ; store screen address.
       ld c,(hl)           ; first sprite graphic.
       inc hl              ; increment poiinter to sprite data.
       ld d,(hl)           ; next bit of sprite image.
       inc hl              ; point to next row of sprite data.
       ld (tmp0),hl        ; store in tmp0 for later.
       ld e,0              ; blank right byte for now.
       ld a,b              ; b holds y position.
       and 7               ; how are we straddling character cells?
       jr z,sprit0         ; we're not straddling them, don't bother shifting.
       cp 5                ; 5 or more right shifts needed?
       jr nc,sprit7        ; yes, shift from left as it's quicker.
       and a               ; oops, carry flag is set so clear it.
sprit2 rr c                ; rotate left byte right...
       rr d                ; ...through middle byte...
       rr e                ; ...into right byte.
       dec a               ; one less shift to do.
       jr nz,sprit2        ; return until all shifts complete.
sprit0 pop hl              ; pop screen address from stack.
       ld a,(hl)           ; what's there already.
       xor c               ; merge in image data.
       ld (hl),a           ; place onto screen.
       inc l               ; next character cell to right please.
       ld a,(hl)           ; what's there already.
       xor d               ; merge with middle bit of image.
       ld (hl),a           ; put back onto screen.
       inc hl              ; next bit of screen area.
       ld a,(hl)           ; what's already there.
       xor e               ; right edge of sprite image data.
       ld (hl),a           ; plonk it on screen.
       ld a,(tmp1)         ; temporary vertical coordinate.
       inc a               ; next line down.
       ld (tmp1),a         ; store new position.
       and 63              ; are we moving to next third of screen?
       jr z,sprit4         ; yes so find next segment.
       and 7               ; moving into character cell below?
       jr z,sprit5         ; yes, find next row.
       dec hl              ; left 2 bytes.
       dec l               ; not straddling 256-byte boundary here.
       inc h               ; next row of this character cell.
sprit6 ex de,hl            ; screen address in de.
       ld hl,(tmp0)        ; restore graphic address.
       ex af,af'           ; restore loop counter.
       dec a               ; decrement it.
       jp nz,sprit1        ; not reached bottom of sprite yet to repeat.
       ret                 ; job done.
sprit4 ld de,30            ; next segment is 30 bytes on.
       add hl,de           ; add to screen address.
       jp sprit6           ; repeat.
sprit5 ld de,63774         ; minus 1762.
       add hl,de           ; subtract 1762 from physical screen address.
       jp sprit6           ; rejoin loop.
scadd  ld a,(xcoord)       ; fetch vertical coordinate.
       ld e,a              ; store that in e.

; Find line within cell.

       and 7               ; line 0-7 within character square.
       add a,64            ; 64 * 256 = 16384 = start of screen display.
       ld d,a              ; line * 256.

; Find which third of the screen we're in.

       ld a,e              ; restore the vertical.
       and 192             ; segment 0, 1 or 2 multiplied by 64.
       rrca                ; divide this by 8.
       rrca
       rrca                ; segment 0-2 multiplied by 8.
       add a,d             ; add to d give segment start address.
       ld d,a

; Find character cell within segment.

       ld a,e              ; 8 character squares per segment.
       rlca                ; divide x by 8 and multiply by 32,
       rlca                ; net calculation: multiply by 4.
       and 224             ; mask off bits we don't want.
       ld e,a              ; vertical coordinate calculation done.

; Add the horizontal element.

       ld a,(ycoord)       ; y coordinate.
       rrca                ; only need to divide by 8.
       rrca
       rrca
       and 31              ; squares 0 - 31 across screen.
       add a,e             ; add to total so far.
       ld e,a              ; de = address of screen.
       ret

tmp0   defw 0
tmp1   defb 0

plgfx  defb 127,254,255,255,254,127,252,127,248,127,248,127,254,127,254,127
       defb 254,127,254,127,254,127,254,127,248,31,248,31,255,255,127,254
algfx  defb 127,254,254,63,248,15,240,135,227,231,231,231,255,199,255,15
       defb 252,31,248,127,241,255,227,255,224,7,224,7,255,255,127,254

The best alien movement routines use a combination of random elements and hunter-killer algorithms. In order to overcome the problem in the listing above we need an extra flag to indicate the enemy’s present state or in this case its direction. We can move the sprite along in a certain direction until it becomes possible to switch course vertically or horizontally, whereupon a new direction is selected depending upon the player’s position. However, should it not be possible to move in the desired direction we go in the opposite direction instead. Using this method a sprite can find its own way around most mazes without getting stuck too often. In fact, to absolutely guarantee that the sprite will not get stuck we can add a random element so that every so often the new direction is chosen on a random basis rather than the difference in x and y coordinates.

 

Cranking up the Difficulty Levels

The weighting applied to the direction-changing decision will determine the sprite’s intelligence levels.  If the new direction has a 90% chance of being chosen on a random basis and a 10% chance based on coordinates the alien will wander around aimlessly for a while and only home in on the player slowly.  That said, a random decision can sometimes be the right one when chasing the player.  An alien on a more difficult screen might have a 60% chance of choosing a new direction randomly, and a 40% chance of choosing the direction based on the player’s relative position.  This alien will track the player a little more closely.  By tweaking these percentage levels it is possible to determine difficulty levels throughout a game and ensure a smooth transition from the simplest of starting screens to fiendishly difficult final levels.

How To Write ZX Spectrum Games – Chapter 10

September 8, 2013 Leave a comment

Scores and High Scores

Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.

More Scoring Routines

Up until now we have gotten away with an unsophisticated scoring routine.  Our score is held as a 16-bit number stored in a register pair, and to display it we have made use of the Sinclair ROM’s line number print/display routine.  There are two main drawbacks to this method, firstly we are limited to numbers 0-9999, and secondly it looks awful.

We could convert a 16-bit number to ASCII ourselves like this:

; Show number passed in hl, right-justified.

shwnum ld a,48 (or 32)     ; leading zeroes (or spaces).
       ld de,10000         ; ten thousands column.
       call shwdg          ; show digit.
       ld de,1000          ; thousands column.
       call shwdg          ; show digit.
       ld de,100           ; hundreds column.
       call shwdg          ; show digit.
       ld de,10            ; tens column.
       call shwdg          ; show digit.
       or 16               ; last digit is always shown.
       ld de,1             ; units column.
shwdg  and 48              ; clear carry, clear digit.
shwdg1 sbc hl,de           ; subtract from column.
       jr c,shwdg0         ; nothing to show.
       or 16               ; something to show, make it a digit.
       inc a               ; increment digit.
       jr shwdg1           ; repeat until column is zero.
shwdg0 add hl,de           ; restore total.
       push af
       rst 16              ; show character.
       pop af
       ret

This method works well, though we’re still limited to a five-digit score of no more than 65535. For a more professional-looking affair complete with any number of leading zeroes we need to hold the score as a string of ASCII digits.

I have used the same scoring technique for something like 15 years now, it isn’t terribly sophisticated but it’s good enough to do what we need. This method uses one ASCII character per digit, which makes it easy to display. Incidentally, this routine is taken from the shoot-em-up More Tea, Vicar?

score  defb '000000'
uscor  ld a,(hl)           ; current value of digit.
       add a,b             ; add points to this digit.
       ld (hl),a           ; place new digit back in string.
       cp 58               ; more than ASCII value '9'?
       ret c               ; no - relax.
       sub 10              ; subtract 10.
       ld (hl),a           ; put new character back in string.
uscor0 dec hl              ; previous character in string.
       inc (hl)            ; up this by one.
       ld a,(hl)           ; what's the new value?
       cp 58               ; gone past ASCII nine?
       ret c               ; no, scoring done.
       sub 10              ; down by ten.
       ld (hl),a           ; put it back
       jp uscor0           ; go round again.

To use this we point hl at the digit we would like to increase, place the amount we want to add in the b register, then call uscor. For example, to add 250 to the score requires 6 lines:

; Add 250 to the score.

       ld hl,score+3       ; point to hundreds column.
       ld b,2              ; 2 hundreds = 200.
       call uscor          ; increment the score.
       ld hl,score+4       ; point to tens column.
       ld b,5              ; 5 tens = 50.
       call uscor          ; up the score.

Simple, but it does the job. Pedants would no doubt point out that this could be done using BCD, and that all the opcodes for this are in the Z80 instruction set.

High Score Tables

High Score routines are not especially easy to write for a beginner, but once you have written one it can be re-used again and again.  The basic principle is that we start at the bottom of the table and work our way up until we find a score that is greater than, or equal to, the player’s score.  We then shift all the data in the table below that point down by one position, and copy our player’s name and score into the table at that point.

We can set the hl or ix register pair to the first digit of the bottom score in the table and work our way comparing each digit to the corresponding one in the player’s score.  If the digit in the player’s score is higher we move up a position, if it is lower we stop there and copy the player’s score into the table one place below.  If both digits are the same we move to the next digit and repeat the check until the digits are different or we have checked all the digits in the score.  If the scores are identical we place the player’s entry in the table one place below.  This is repeated until a score in the table is higher than the player’s score, or we reach the top of the table.

When first initialising a high score table it may be tempting to place your own name at the top with a score that is very difficult to beat.  Try to resist this temptation.  High score tables are for the player to judge his own performance, and there is no point in frustrating the player by making it difficult to reach the top position.

How To Write ZX Spectrum Games – Chapter 9

September 8, 2013 Leave a comment

Background Graphics

Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.

Displaying Blocks

Let us say that we want to write a single screen maze game.  We need to display the walls around which the player’s sprite is to be manipulated, and the best way to do this is to create a table of blocks which are transferred to the screen sequentially.  As we step through the table we find the address of the block graphic, calculate the screen address and dump the character to the screen.

We will start with the character display routine.  Unlike a sprite routine we need to deal with character positions, and luckily it is easier to calculate a screen address for a character position than it is for individual pixels.

There are 24 vertical and 32 horizontal character cell positions on the Spectrum screen, so our coordinates will be between (0,0) and (23,31).  Rows 0-7 fall in the first segment, 8-15 in the middle section and positions 16-23 in the third portioin of the screen.  As luck would have it, the high byte of the screen address for each segment increases by 8 from one segment to the next, so by taking the vertical cell number and performing an and 24 we immediately get the displacement to the start of relevant segment’s screen address right there.  Add 64 for the start of the Spectrum’s screen and we have the high byte of our address.  We then need to find the correct character cell within each segment, so we again take the vertical coordinate, and this time use and 7 to determine which of the seven rows we’re trying to find.  We multiply this by the character width of the screen – 32 – and add the horizontal cell number to find the low byte of the screen address.  A suitable example is below:

; Return character cell address of block at (b, c).

chadd  ld a,b              ; vertical position.
       and 24              ; which segment, 0, 1 or 2?
       add a,64            ; 64*256 = 16384, Spectrum's screen memory.
       ld d,a              ; this is our high byte.
       ld a,b              ; what was that vertical position again?
       and 7               ; which row within segment?
       rrca                ; multiply row by 32.
       rrca
       rrca
       ld e,a              ; low byte.
       ld a,c              ; add on y coordinate.
       add a,e             ; mix with low byte.
       ld e,a              ; address of screen position in de.
       ret

Once we have our screen address it is a straightforward process to dump the character onto the screen.  As long as we are not crossing character cell boundaries the next screen line will always fall 256 bytes after its predecessor, so we increment the high byte of the address to find the next line.

; Display character hl at (b, c).

char   call chadd          ; find screen address for char.
       ld b,8              ; number of pixels high.
char0  ld a,(hl)           ; source graphic.
       ld (de),a           ; transfer to screen.
       inc hl              ; next piece of data.
       inc d               ; next pixel line.
       djnz char0          ; repeat
       ret

As for colouring our block, we covered that in the chapter on simple attribute collision detection. The atadd routine will give us the address of an attribute cell at character cell (b, c).

Lastly, we need to decide which block to display at each cell position. Say we need 3 types of block for our game – we might use block type 0 for a space, 1 for a wall and 2 for a key. We would arrange the graphics and attributes for each block in separate tables in the same order:

blocks equ $

; block 0 = space character.

       defb 0,0,0,0,0,0,0,0

; block 1 = wall.

       defb 1,1,1,255,16,16,16,255

; block 2 = key.

       defb 6,9,9,14,16,32,80,32

attrs  equ $

; block 0 = space.

       defb 71

; block 1 = wall.

       defb 22

; block 2 = key.

       defb 70

As we step through our table of up to 24 rows and 32 columns of maze blocks we load the block number into the accumulator, and call the fblock and fattr routines below to obtain the source graphic and attribute addresses.

; Find cell graphic.

fblock rlca                ; multiply block number by eight.
       rlca
       rlca
       ld e,a              ; displacement to graphic address.
       ld d,0              ; no high byte.
       ld hl,blocks        ; address of character blocks.
       add hl,de           ; point to block.
       ret

; Find cell attribute.

fattr  ld e,a              ; displacement to attribute address.
       ld d,0              ; no high byte.
       ld hl,attrs         ; address of block attributes.
       add hl,de           ; point to attribute.
       ret

Using this method means our maze data requires one byte of RAM for every character cell. For a playing area of 32 cells wide and 16 blocks high this would mean each screen occupying 512 bytes of memory. That would be fine for a 20-screen platformer like Manic Miner, but if you want a hundred screens or more you should consider using bigger blocks so that less are required for each screen. By using character cell blocks which are 16 x 16 pixels instead of 8 x 8 in our example, each screen table would require only 128 bytes meaning more could be squeezed into the Spectrum’s memory.

How To Write ZX Spectrum Games – Chapter 8

September 8, 2013 4 comments

Sprites

Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.

Converting Pixel Positions to Screen Addresses

UDGs and character graphics are all very fine and dandy, but the better games usually use sprites and there are no handy ROM routines to help us here because Sir Clive didn’t design the Spectrum as a games machine.  Sooner or later a games programmer has to confront the thorny issue of the Spectrum’s awkward screen layout.  It’s a tricky business converting x and y pixel coordinates into a screen address but there are a couple of methods we might employ to do this.

Using a Screen Address Look-up Table

The first method is to use a pre-calculated address table containing the screen address for each of the Spectrum’s 192 lines such as this, or a similar variation:

       xor a               ; clear carry flag and accumulator.
       ld d,a              ; empty de high byte.
       ld a,(xcoord)       ; x position.
       rla                 ; shift left to multiply by 2.
       ld e,a              ; place this in low byte of de pair.
       rl d                ; shift top bit into de high byte.
       ld hl,addtab        ; table of screen addresses.
       add hl,de           ; point to table entry.
       ld e,(hl)           ; low byte of screen address.
       inc hl              ; point to high byte.
       ld d,(hl)           ; high byte of screen address.
       ld a,(ycoord)       ; horizontal position.
       rra                 ; divide by two.
       rra                 ; and again for four.
       rra                 ; shift again to divide by eight.
       and 31              ; mask away rubbish shifted into rightmost bits.
       add a,e             ; add to address for start of line.
       ld e,a              ; new value of e register.
       ret                 ; return with screen address in de.
.
.
addtab defw 16384
       defw 16640
       defw 16896
.
.

On the plus side this is very fast, but it does mean having to store each of the 192 line addresses in a table, taking up 384 bytes which might be better employed elsewhere.

Calculating Screen Addresses

The second method involves calculating the address ourselves and doesn’t require an address look-up table.  In doing this we need to consider three things: Which third of the screen the point is in, the character line to which it is closest, and the pixel line upon which it falls within that cell.  Judicious use of the and operand will help us to decide all three.  It is a complicated business however, so please bear with me as I endeavour to explain how it works.

We can establish which of the three screen segments a point is situated in by taking the vertical coordinate and masking away the six least significant bits to leave a value of 0, 64 or 128 each of the segments being 64 pixels apart.  As the high bytes of the 3 screen segment addresses are 64, 72 and 80 – a difference of 8 going from one segment to another – we take this masked value and divide it by 8 to give us a value of 0, 8 or 16.  We then add 64 to give us the high byte of the screen segment.

Each segment is divided into 8 character cell positions which are 32 bytes apart, so to find that aspect of our address we take the vertical coordinate and mask away the two most significant bits we used to determine the segment along with the three least significant bits which determine the pixel position.  The instruction and 56 will do nicely.  This gives us the character cell position as a multiple of 8, and as the character lines are 32 bytes apart we multiply this by 4 and place our number in the low byte of the screen address.

Finally, character cells are further divided into pixel lines 256 bytes apart, so we again take our vertical coordinate, mask away everything except the bits which determine the line using and 7, and add the result to the high byte.  That will give us our vertical screen address.  From there we take our horizontal coordinate, divide it by 8 and add it to our address.

Here is a routine which returns a screen address for (xcoord, ycoord) in the de register pair.  It could easily be modified to return the address in the hl or bc registers if desired.

scadd  ld a,(xcoord)       ; fetch vertical coordinate.
       ld e,a              ; store that in e.

; Find line within cell.

       and 7               ; line 0-7 within character square.
       add a,64            ; 64 * 256 = 16384 = start of screen display.
       ld d,a              ; line * 256.

; Find which third of the screen we're in.

       ld a,e              ; restore the vertical.
       and 192             ; segment 0, 1 or 2 multiplied by 64.
       rrca                ; divide this by 8.
       rrca
       rrca                ; segment 0-2 multiplied by 8.
       add a,d             ; add to d give segment start address.
       ld d,a

; Find character cell within segment.

       ld a,e              ; 8 character squares per segment.
       rlca                ; divide x by 8 and multiply by 32,
       rlca                ; net calculation: multiply by 4.
       and 224             ; mask off bits we don't want.
       ld e,a              ; vertical coordinate calculation done.

; Add the horizontal element.

       ld a,(ycoord)       ; y coordinate.
       rrca                ; only need to divide by 8.
       rrca
       rrca
       and 31              ; squares 0 - 31 across screen.
       add a,e             ; add to total so far.
       ld e,a              ; de = address of screen.
       ret

Shifting

Once the address has been established we need to consider how our graphics are shifted into position.  The three lowest bit positions of the horizontal coordinate indicate how many pixel shifts are needed.  A slow way to plot a pixel would be to call the scadd routine above, perform an and 7 on the horizontal coordinate, then right shift a pixel from zero to seven times depending on the result before dumping it to the screen.

A shifter sprite routine works in the same way.  The graphic image is taken from memory one line at a time, shifted into position and then placed on the screen before moving to the next line down and repeating the process.  We could write a sprite routine which calculated the screen address for every line drawn, and indeed the first sprite routine I ever wrote worked in such a way.  Fortunately it is much simpler to determine whether we’re moving within a character cell, crossing character cell boundaries, or crossing a segment boundary with a couple of and instructions and to increment or decrement the screen address accordingly.  Put simply, and 63 will return zero if the new vertical position is crossing a segment, and 7 will return zero if it is crossing a character cell boundary and anything else means the new line is within the same character cell as the previous line.

This is a shifter sprite routine which makes use of the earlier scadd routine.  To use it simply set up the coordinates in dispx and dispy, point the bc register pair at the sprite graphic, and call sprite.

sprit7 xor 7               ; complement last 3 bits.
       inc a               ; add one for luck!
sprit3 rl d                ; rotate left...
       rl c                ; ...into middle byte...
       rl e                ; ...and finally into left character cell.
       dec a               ; count shifts we've done.
       jr nz,sprit3        ; return until all shifts complete.

; Line of sprite image is now in e + c + d, we need it in form c + d + e.

       ld a,e              ; left edge of image is currently in e.
       ld e,d              ; put right edge there instead.
       ld d,c              ; middle bit goes in d.
       ld c,a              ; and the left edge back into c.
       jr sprit0           ; we've done the switch so transfer to screen.

sprite ld a,(dispx)        ; draws sprite (hl).
       ld (tmp1),a         ; store vertical.
       call scadd          ; calculate screen address.
       ld a,16             ; height of sprite in pixels.
sprit1 ex af,af'           ; store loop counter.
       push de             ; store screen address.
       ld c,(hl)           ; first sprite graphic.
       inc hl              ; increment pointer to sprite data.
       ld d,(hl)           ; next bit of sprite image.
       inc hl              ; point to next row of sprite data.
       ld (tmp0),hl        ; store in tmp0 for later.
       ld e,0              ; blank right byte for now.
       ld a,b              ; b holds y position.
       and 7               ; how are we straddling character cells?
       jr z,sprit0         ; we're not straddling them, don't bother shifting.
       cp 5                ; 5 or more right shifts needed?
       jr nc,sprit7        ; yes, shift from left as it's quicker.
       and a               ; oops, carry flag is set so clear it.
sprit2 rr c                ; rotate left byte right...
       rr d                ; ...through middle byte...
       rr e                ; ...into right byte.
       dec a               ; one less shift to do.
       jr nz,sprit2        ; return until all shifts complete.
sprit0 pop hl              ; pop screen address from stack.
       ld a,(hl)           ; what's there already.
       xor c               ; merge in image data.
       ld (hl),a           ; place onto screen.
       inc l               ; next character cell to right please.
       ld a,(hl)           ; what's there already.
       xor d               ; merge with middle bit of image.
       ld (hl),a           ; put back onto screen.
       inc hl              ; next bit of screen area.
       ld a,(hl)           ; what's already there.
       xor e               ; right edge of sprite image data.
       ld (hl),a           ; plonk it on screen.
       ld a,(tmp1)         ; temporary vertical coordinate.
       inc a               ; next line down.
       ld (tmp1),a         ; store new position.
       and 63              ; are we moving to next third of screen?
       jr z,sprit4         ; yes so find next segment.
       and 7               ; moving into character cell below?
       jr z,sprit5         ; yes, find next row.
       dec hl              ; left 2 bytes.
       dec l               ; not straddling 256-byte boundary here.
       inc h               ; next row of this character cell.
sprit6 ex de,hl            ; screen address in de.
       ld hl,(tmp0)        ; restore graphic address.
       ex af,af'           ; restore loop counter.
       dec a               ; decrement it.
       jp nz,sprit1        ; not reached bottom of sprite yet to repeat.
       ret                 ; job done.
sprit4 ld de,30            ; next segment is 30 bytes on.
       add hl,de           ; add to screen address.
       jp sprit6           ; repeat.
sprit5 ld de,63774         ; minus 1762.
       add hl,de           ; subtract 1762 from physical screen address.
       jp sprit6           ; rejoin loop.

As you can see, this routine utilises the xor instruction to merge the sprite onto the screen, which works in the same way that PRINT OVER 1 does in Sinclair BASIC.   The sprite is merged with any graphics already present on screen which can look messy.  To delete a sprite we just display it again and the image magically vanishes.

If we wanted to draw a sprite on top of something that is already on the screen we would need some extra routines, similar to the one above.  One would be required to store the graphics on screen in a buffer so that that portion of the screen could be re-drawn when the sprite is deleted.  The next routine would apply a sprite mask to remove the pixels around and behind the sprite using and or or, then the sprite could finally be applied over the top.  Another routine would be needed to restore the relevant portion of screen to its former state should the sprite be deleted.  However, this would take a lot of CPU time to achieve so my advice would be not to bother unless your game uses something called double buffering – otherwise known as the back screen technique, or you’re using a pre-shifted sprites, which we shall discuss shortly.

Another method you may wish to consider involves making sprites appear to pass behind background objects, a trick you may have seen in Haunted House or Egghead in Space.  While this method is handy for reducing colour clash it requires a sizeable chunk of memory.  In both games a 6K dummy mask screen was located at address 24576, and each byte of sprite data was anded with the data on the dummy screen before being xored onto the physical screen located at address 16384.  Because the physical screen and the dummy mask screen were exactly 8K apart it was possible to flip between them by toggling bit 5 of the h register.  To do this for the sprite routine above our sprit0 routine might look like this:

sprit0 pop hl              ; pop screen address from stack.
       set 5,h             ; address of dummy screen.
       ld a,(hl)           ; what's there already.
       and c               ; mask away parts behind the object.
       res 5,h             ; address of physical screen.
       xor (hl)            ; merge in image data.
       ld (hl),a           ; place onto screen.
       inc l               ; next character cell to right please.
       set 5,h             ; address of dummy screen.
       ld a,(hl)           ; what's there already.
       and d               ; mask with middle bit of image.
       res 5,h             ; address of physical screen.
       xor (hl)            ; merge in image data.
       ld (hl),a           ; put back onto screen.
       inc hl              ; next bit of screen area.
       set 5,h             ; address of dummy screen.
       ld a,(hl)           ; what's already there.
       and e               ; mask right edge of sprite image data.
       res 5,h             ; address of physical screen.
       xor (hl)            ; merge in image data.
       ld (hl),a           ; plonk it on screen.
       ld a,(tmp1)         ; temporary vertical coordinate.

Pre-shifted Sprites

A shifter sprite routine has one major drawback: its lack of speed.  Shifting all that graphic data into position takes time, and if your game needs a lot of sprites bouncing around the screen, you should consider using pre-shifted sprites instead.  This requires eight separate copies of the sprite image, one in each of the shifted pixel positions.  It is then simply a matter of calculating which sprite image to use based on the horizontal alignment of the sprite, calculating the screen address, and copying the sprite image to the screen.  While this method is much faster it is fantastically expensive in memory terms.  A shifter sprite routine requires 32 bytes for an unmasked 16×16 pixel sprite, a pre-shifted sprite requires 256 bytes for the same image.  Writing a Spectrum game is a compromise between speed and available memory.  In general I prefer to move my sprites 2 pixels per frame meaning the odd pixel alignments are not required.  Even so, my pre-shifted sprites still require 128 bytes of precious RAM.

You may not necessarily want the same sprite image in each pre-shifted position.  For example, by changing the position of a sprite’s legs in each of the pre-shifted positions a sprite can be animated to appear as if it is walking from left to right as it moves across the screen.  Remember to match the character’s legs to the number of pixels it is moved each frame.  If you are moving a sprite 2 pixels each frame it is important to make the legs move 2 pixels between frames.  Less than this will make the sprite appear as if it is skating on ice, any more and it will appear to be struggling for grip.  I’ll let you into a little secret here: believe it or not, this can actually affect the way a game feels so getting your animation right is important.

How To Write ZX Spectrum Games – Chapter 7

September 8, 2013 Leave a comment

Basic Alien Collision Detection

Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.

Coordinate Checking

Coordinate checking should be self-explanatory to most programmers, but is included here for the sake of completeness.  It is also the next step in the development of our Centipede game.

The simplest type of collision detection would be something like this, used to detect if two UDGs have collided:

       ld a,(playx)        ; player's x coordinate.
       cp (ix+1)           ; compare with alien x.
       ret nz              ; not the same, no collision.
       ld a,(playy)        ; player's y coordinate.
       cp (ix+2)           ; compare with alien y.
       ret nz              ; not the same, no collision.
       jp collis           ; we have a collision.

Okay, so that’s pretty simple but most games don’t use single-cell character graphics.  What if the aliens are four character cells wide by two high, and the player’s character is three squares high by three wide?  We need to check if any part of the alien has collided with any part of the player, so we need to check that the coordinates are within a certain range.  If the alien is less than two squares above the player, or less than 3 below him then the vertical coordinates match.  If the alien is also less than four squares to the left of the player, and less than three squares to the right then the horizontal position also matches and we have a collision.

Let’s write some code to do this.  We can start by taking the player’s vertical coordinate:

       ld a,(playx)        ; player's x coordinate.

Then subtract the alien’s vertical position:

      sub (ix+1)          ; subtract alien x.

Next, subtract one from the player’s height, and add it.

      add a,2             ; player is 3 high, so add 3 - 1 = 2.

If the alien is within range the result will be less than the combined height of the player and alien, so we perform the check:

       cp 5                ; combined heights are 3 + 2 = 5.
       ret nc              ; not within vertical range.

Similarly, we can follow this with the code for the horizontal check:

       ld a,(playy)        ; player's y coordinate.
       sub (ix+2)          ; subtract alien y.
       add a,2             ; player is 3 wide, so add 3 - 1 = 2.
       cp 7                ; combined widths are 3 + 4 = 7.
       ret nc              ; not within horizontal range.
       jp collis           ; we have a collision.

Of course, this method doesn’t just work for character-based graphics, it works perfectly well with sprites too, but more of those later. It’s time to finish our game with some collision detection. As our graphics are all single-character UDGs we don’t need anything fancy, a quick x=x and y=y check are all we need.

 We want a black screen.

       ld a,71             ; white ink (7) on black paper (0),
                           ; bright (64).
       ld (23693),a        ; set our screen colours.
       xor a               ; quick way to load accumulator with zero.
       call 8859           ; set permanent border colours.

; Set up the graphics.

       ld hl,blocks        ; address of user-defined graphics data.
       ld (23675),hl       ; make UDGs point to it.

; Okay, let's start the game.

       call 3503           ; ROM routine - clears screen, opens chan 2.

       xor a               ; zeroise accumulator.
       ld (dead),a         ; set flag to say player is alive.

; Initialise coordinates.

       ld hl,21+15*256     ; load hl pair with starting coords.
       ld (plx),hl         ; set player coords.
       ld hl,255+255*256   ; player's bullets default.
       ld (pbx),hl         ; set bullet coords.

       ld b,10             ; number of segments to initialise.
       ld hl,segmnt        ; segment table.
segint ld (hl),1           ; start off moving right.
       inc hl
       ld (hl),0           ; start at top.
       inc hl
       ld (hl),b           ; use B register as y coordinate.
       inc hl
       djnz segint         ; repeat until all initialised.

       call basexy         ; set the x and y positions of the player.
       call splayr         ; show player base symbol.

; Now we want to fill the play area with mushrooms.

       ld a,68             ; green ink (4) on black paper (0),
                           ; bright (64).
       ld (23695),a        ; set our temporary colours.
       ld b,50             ; start with a few.
mushlp ld a,22             ; control code for AT character.
       rst 16
       call random         ; get a 'random' number.
       and 15              ; want vertical in range 0 to 15.
       rst 16
       call random         ; want another pseudo-random number.
       and 31              ; want horizontal in range 0 to 31.
       rst 16
       ld a,145            ; UDG 'B' is the mushroom graphic.
       rst 16              ; put mushroom on screen.
       djnz mushlp         ; loop back until all mushrooms displayed.

; This is the main loop.

mloop  equ $

; Delete the player.

       call basexy         ; set the x and y positions of the player.
       call wspace         ; display space over player.

; Now we've deleted the player we can move him before redisplaying him
; at his new coordinates.

       ld bc,63486         ; keyboard row 1-5/joystick port 2.
       in a,(c)            ; see what keys are pressed.
       rra                 ; outermost bit = key 1.
       push af             ; remember the value.
       call nc,mpl         ; it's being pressed, move left.
       pop af              ; restore accumulator.
       rra                 ; next bit along (value 2) = key 2.
       push af             ; remember the value.
       call nc,mpr         ; being pressed, so move right.
       pop af              ; restore accumulator.
       rra                 ; next bit (value 4) = key 3.
       push af             ; remember the value.
       call nc,mpd         ; being pressed, so move down.
       pop af              ; restore accumulator.
       rra                 ; next bit (value 8) reads key 4.
       push af             ; remember the value.
       call nc,mpu         ; it's being pressed, move up.
       pop af              ; restore accumulator.
       rra                 ; last bit (value 16) reads key 5.
       call nc,fire        ; it's being pressed, move up.

; Now he's moved we can redisplay the player.

       call basexy         ; set the x and y positions of the player.
       call splayr         ; show player.

; Now for the bullet.  First let's check to see if it's hit anything.

       call bchk           ; check bullet position.

       call dbull          ; delete bullets.
       call moveb          ; move bullets.
       call bchk           ; check new position of bullets.
       call pbull          ; print bullets at new position.

; Now for the centipede segments.

       ld ix,segmnt        ; table of segment data.
       ld b,10             ; number of segments in table.
censeg push bc
       ld a,(ix)           ; is segment switched on?
       inc a               ; 255=off, increments to zero.
       call nz,proseg      ; it's active, process segment.
       pop bc
       ld de,3             ; 3 bytes per segment.
       add ix,de           ; get next segment in ix registers.
       djnz censeg         ; repeat for all segments.

       halt                ; delay.

       ld a,(dead)         ; was the player killed by a segment?
       and a
       ret nz              ; player killed - lose a life.

; Jump back to beginning of main loop.

       jp mloop

; Move player left.

mpl    ld hl,ply           ; remember, y is the horizontal coord!
       ld a,(hl)           ; what's the current value?
       and a               ; is it zero?
       ret z               ; yes - we can't go any further left.

; now check that there isn't a mushroom in the way.

       ld bc,(plx)         ; current coords.
       dec b               ; look 1 square to the left.
       call atadd          ; get address of attribute at this position.
       cp 68               ; mushrooms are bright (64) + green (4).
       ret z               ; there's a mushroom - we can't move there.

       dec (hl)            ; subtract 1 from y coordinate.
       ret

; Move player right.

mpr    ld hl,ply           ; remember, y is the horizontal coord!
       ld a,(hl)           ; what's the current value?
       cp 31               ; is it at the right edge (31)?
       ret z               ; yes - we can't go any further left.

; now check that there isn't a mushroom in the way.

       ld bc,(plx)         ; current coords.
       inc b               ; look 1 square to the right.
       call atadd          ; get address of attribute at this position.
       cp 68               ; mushrooms are bright (64) + green (4).
       ret z               ; there's a mushroom - we can't move there.

       inc (hl)            ; add 1 to y coordinate.
       ret

; Move player up.

mpu    ld hl,plx           ; remember, x is the vertical coord!
       ld a,(hl)           ; what's the current value?
       cp 4                ; is it at upper limit (4)?
       ret z               ; yes - we can go no further then.

; now check that there isn't a mushroom in the way.

       ld bc,(plx)         ; current coords.
       dec c               ; look 1 square up.
       call atadd          ; get address of attribute at this position.
       cp 68               ; mushrooms are bright (64) + green (4).
       ret z               ; there's a mushroom - we can't move there.

       dec (hl)            ; subtract 1 from x coordinate.
       ret

; Move player down.

mpd    ld hl,plx           ; remember, x is the vertical coord!
       ld a,(hl)           ; what's the current value?
       cp 21               ; is it already at the bottom (21)?
       ret z               ; yes - we can't go down any more.

; now check that there isn't a mushroom in the way.

       ld bc,(plx)         ; current coords.
       inc c               ; look 1 square down.
       call atadd          ; get address of attribute at this position.
       cp 68               ; mushrooms are bright (64) + green (4).
       ret z               ; there's a mushroom - we can't move there.

       inc (hl)            ; add 1 to x coordinate.
       ret

; Fire a missile.

fire   ld a,(pbx)          ; bullet vertical coord.
       inc a               ; 255 is default value, increments to zero.
       ret nz              ; bullet on screen, can't fire again.
       ld hl,(plx)         ; player coordinates.
       dec l               ; 1 square higher up.
       ld (pbx),hl         ; set bullet coords.
       ret

bchk   ld a,(pbx)          ; bullet vertical.
       inc a               ; is it at 255 (default)?
       ret z               ; yes, no bullet on screen.
       ld bc,(pbx)         ; get coords.
       call atadd          ; find attribute here.
       cp 68               ; mushrooms are bright (64) + green (4).
       jr z,hmush          ; hit a mushroom!
       ret

hmush  ld a,22             ; AT control code.
       rst 16
       ld a,(pbx)          ; bullet vertical.
       rst 16
       ld a,(pby)          ; bullet horizontal.
       rst 16
       call wspace         ; set INK colour to white.
kilbul ld a,255            ; x coord of 255 = switch bullet off.
       ld (pbx),a          ; destroy bullet.
       ret

; Move the bullet up the screen 1 character position at a time.

moveb  ld a,(pbx)          ; bullet vertical.
       inc a               ; is it at 255 (default)?
       ret z               ; yes, no bullet on screen.
       sub 2               ; 1 row up.
       ld (pbx),a
       ret

; Set up the x and y coordinates for the player's gunbase position,
; this routine is called prior to display and deletion of gunbase.

basexy ld a,22             ; AT code.
       rst 16
       ld a,(plx)          ; player vertical coord.
       rst 16              ; set vertical position of player.
       ld a,(ply)          ; player's horizontal position.
       rst 16              ; set the horizontal coord.
       ret

; Show player at current print position.

splayr ld a,69             ; cyan ink (5) on black paper (0),
                           ; bright (64).
       ld (23695),a        ; set our temporary screen colours.
       ld a,144            ; ASCII code for User Defined Graphic 'A'.
       rst 16              ; draw player.
       ret

pbull  ld a,(pbx)          ; bullet vertical.
       inc a               ; is it at 255 (default)?
       ret z               ; yes, no bullet on screen.
       call bullxy
       ld a,16             ; INK control char.
       rst 16
       ld a,6              ; 6 = yellow.
       rst 16
       ld a,147            ; UDG 'D' is used for player bullets.
       rst 16
       ret

dbull  ld a,(pbx)          ; bullet vertical.
       inc a               ; is it at 255 (default)?
       ret z               ; yes, no bullet on screen.
       call bullxy         ; set up bullet coordinates.
wspace ld a,71             ; white ink (7) on black paper (0),
                           ; bright (64).
       ld (23695),a        ; set our temporary screen colours.
       ld a,32             ; SPACE character.
       rst 16              ; display space.
       ret

; Set up the x and y coordinates for the player's bullet position,
; this routine is called prior to display and deletion of bullets.

bullxy ld a,22             ; AT code.
       rst 16
       ld a,(pbx)          ; player bullet vertical coord.
       rst 16              ; set vertical position of player.
       ld a,(pby)          ; bullet's horizontal position.
       rst 16              ; set the horizontal coord.
       ret

segxy  ld a,22             ; ASCII code for AT character.
       rst 16              ; display AT code.
       ld a,(ix+1)         ; get segment x coordinate.
       rst 16              ; display coordinate code.
       ld a,(ix+2)         ; get segment y coordinate.
       rst 16              ; display coordinate code.
       ret

proseg call segcol         ; segment collision detection.
       ld a,(ix)           ; check if segment was switched off
       inc a               ; by collision detection routine.
       ret z               ; it was, so this segment is now dead.
       call segxy          ; set up segment coordinates.
       call wspace         ; display a space, white ink on black.
       call segmov         ; move segment.
       call segcol         ; new segment position collision check.
       ld a,(ix)           ; check if segment was switched off
       inc a               ; by collision detection routine.
       ret z               ; it was, so this segment is now dead.
       call segxy          ; set up segment coordinates.
       ld a,2              ; attribute code 2 = red segment.
       ld (23695),a        ; set temporary attributes.
       ld a,146            ; UDG 'C' to display segment.
       rst 16
       ret
segmov ld a,(ix+1)         ; x coord.
       ld c,a              ; GP x area.
       ld a,(ix+2)         ; y coord.
       ld b,a              ; GP y area.
       ld a,(ix)           ; status flag.
       and a               ; is the segment heading left?
       jr z,segml          ; going left - jump to that bit of code.

; so segment is going right then!

segmr  ld a,(ix+2)         ; y coord.
       cp 31               ; already at right edge of screen?
       jr z,segmd          ; yes - move segment down.
       inc a               ; look right.
       ld b,a              ; set up GP y coord.
       call atadd          ; find attribute address.
       cp 68               ; mushrooms are bright (64) + green (4).
       jr z,segmd          ; mushroom to right, move down instead.
       inc (ix+2)          ; no obstacles, so move right.
       ret

; so segment is going left then!

segml  ld a,(ix+2)         ; y coord.
       and a               ; already at left edge of screen?
       jr z,segmd          ; yes - move segment down.
       dec a               ; look right.
       ld b,a              ; set up GP y coord.
       call atadd          ; find attribute address at (dispx,dispy).
       cp 68               ; mushrooms are bright (64) + green (4).
       jr z,segmd          ; mushroom to left, move down instead.
       dec (ix+2)          ; no obstacles, so move left.
       ret

; so segment is going down then!

segmd  ld a,(ix)           ; segment direction.
       xor 1               ; reverse it.
       ld (ix),a           ; store new direction.
       ld a,(ix+1)         ; y coord.
       cp 21               ; already at bottom of screen?
       jr z,segmt          ; yes - move segment to the top.

; At this point we're moving down regardless of any mushrooms that 
; may block the segment's path.  Anything in the segment's way will
; be obliterated.

       inc (ix+1)          ; haven't reached the bottom, move down.
       ret

; moving segment to the top of the screen.

segmt  xor a               ; same as ld a,0 but saves 1 byte.
       ld (ix+1),a         ; new x coordinate = top of screen.
       ret

; Segment collision detection.
; Checks for collisions with player and player's bullets.

segcol ld a,(ply)          ; bullet y position.
       cp (ix+2)           ; is it identical to segment y coord?
       jr nz,bulcol        ; y coords differ, try bullet instead.
       ld a,(plx)          ; player x coord.
       cp (ix+1)           ; same as segment?
       jr nz,bulcol        ; x coords differ, try bullet instead.

; So we have a collision with the player.

killpl ld (dead),a         ; set flag to say that player is now dead.
       ret

; Let's check for a collision with the player's bullet.

bulcol ld a,(pbx)          ; bullet x coords.
       inc a               ; at default value?
       ret z               ; yes, no bullet to check for.
       cp (ix+1)           ; is bullet x coord same as segment x coord?
       ret nz              ; no, so no collision.
       ld a,(pby)          ; bullet y position.
       cp (ix+2)           ; is it identical to segment y coord?
       ret nz              ; no - no collision this time.

; So we have a collision with the player's bullet.

       call dbull          ; delete bullet.
       ld a,22             ; AT code.
       rst 16
       ld a,(pbx)          ; player bullet vertical coord.
       inc a               ; 1 line down.
       rst 16              ; set vertical position of mushroom.
       ld a,(pby)          ; bullet's horizontal position.
       rst 16              ; set the horizontal coord.
       ld a,16             ; ASCII code for INK control.
       rst 16
       ld a,4              ; 4 = colour green.
       rst 16              ; we want all mushrooms in this colour!
       ld a,145            ; UDG 'B' is the mushroom graphic.
       rst 16              ; put mushroom on screen.
       call kilbul         ; kill the bullet.
       ld (ix),a           ; kill the segment.
       ld hl,numseg        ; number of segments.
       dec (hl)            ; decrement it.
       ret

; Simple pseudo-random number generator.
; Steps a pointer through the ROM (held in seed), returning
; the contents of the byte at that location.

random ld hl,(seed)        ; Pointer
       ld a,h
       and 31              ; keep it within first 8k of ROM.
       ld h,a
       ld a,(hl)           ; Get "random" number from location.
       inc hl              ; Increment pointer.
       ld (seed),hl
       ret
seed   defw 0

; Calculate address of attribute for character at (dispx, dispy).

atadd  ld a,c              ; vertical coordinate.
       rrca                ; multiply by 32.
       rrca                ; Shifting right with carry 3 times is
       rrca                ; quicker than shifting left 5 times.
       ld e,a
       and 3
       add a,88            ; 88x256=address of attributes.
       ld d,a
       ld a,e
       and 224
       ld e,a
       ld a,b              ; horizontal position.
       add a,e
       ld e,a              ; de=address of attributes.
       ld a,(de)           ; return with attribute in accumulator.
       ret

plx    defb 0              ; player's x coordinate.
ply    defb 0              ; player's y coordinate.
pbx    defb 255            ; player's bullet coordinates.
pby    defb 255
dead   defb 0              ; flag - player dead when non-zero.

; UDG graphics.

blocks defb 16,16,56,56,124,124,254,254    ; player base.
       defb 24,126,255,255,60,60,60,60     ; mushroom.
       defb 24,126,126,255,255,126,126,24  ; segment.
       defb 0,102,102,102,102,102,102,0    ; player bullet.

; Table of segments.
; Format: 3 bytes per entry, 10 segments.
; byte 1: 255=segment off, 0=left, 1=right.
; byte 2 = x (vertical) coordinate.
; byte 3 = y (horizontal) coordinate.

segmnt defb 0,0,0          ; segment 1.
       defb 0,0,0          ; segment 2.
       defb 0,0,0          ; segment 3.
       defb 0,0,0          ; segment 4.
       defb 0,0,0          ; segment 5.
       defb 0,0,0          ; segment 6.
       defb 0,0,0          ; segment 7.
       defb 0,0,0          ; segment 8.
       defb 0,0,0          ; segment 9.
       defb 0,0,0          ; segment 10.

But wait, why are there two tests for collision detection instead of one? Well, imagine the player’s gunbase is one character cell to the left of a centipede segment. The player is moving right and the segment is moving left. In the next frame the segment would move into the cell occupied by the player, and the player would move into the position occupied by the segment in the previous frame – player and centipede segment would move straight through each other and a single collision detection check would fail to pick this up. By checking for a collision after the player moves, and then again after the centipede segments have moved we can avoid the problem.

Collisions Between Sprites

Fair enough, most Spectrum games use sprites rather than UDGs so in the next chapter we shall see how sprites may be drawn.  For collision detection, the same principle of coordinate checking can be used to detect collisions between sprites.  Subtract the first sprite’s coordinates from those of the second, examine the difference and if it’s within the size range of the two sprites combined we have a collision on that axis.  A simple collision check for two 16×16 pixel sprites might look something like this:

; Check (l, h) for collision with (c, b), strict enforcement.

colx16 ld a,l              ; x coord.
       sub c               ; subtract x.
       add a,15            ; add maximum distance.
       cp 31               ; within x range?
       ret nc              ; no - they've missed.
       ld a,h              ; y coord.
       sub b               ; subtract y.
       add a,15            ; add maximum distance.
       cp 31               ; within y range?
       ret                 ; carry flag set if there's a collision.

There is a drawback with this method. If your sprites don’t entirely fill their 16×16 pixel boundaries then the collision detection will appear to be too strict, and collisions will happen when sprites are close together but not actually touching. A slightly less sensitive check would involve clipping the corners of the sprites into a more octagonal shape, particularly if your sprites have rounded corners. The routine below works by adding the x and y coordinate differences and checking that they are below a certain limit. For a collision between two 16×16 sprites the maximum coordinate distances are 15 pixels for each axis, so by checking that the x and y differences are 25 or less we are effectively shaving a 5x5x5 pixel triangle from each corner.

; Check (l, h) for collision with (c, b), cutting corners.

colc16 ld a,l              ; x coord.
       sub c               ; subtract x.
       jr nc,colc1a        ; result is positive.
       neg                 ; make negative positive.
colc1a cp 16               ; within x range?
       ret nc              ; no - they've missed.
       ld e,a              ; store difference.

       ld a,h              ; y coord.
       sub b               ; subtract y.
       jr nc,colc1b        ; result is positive.
       neg                 ; make negative positive.
colc1b cp 16               ; within y range?
       ret nc              ; no - they've missed.

       add a,e             ; add x difference.
       cp 26               ; only 5 corner pixels touching?
       ret                 ; carry set if there's a collision.

How To Write ZX Spectrum Games – Chapter 6

September 8, 2013 Leave a comment

Tables

Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.

Aliens Don’t Come One at a Time

Let us say, for the sake of example, we were writing a Space Invaders game featuring eleven columns, each containing five rows of invaders.  It would be impractical to write the code for each of the fifty-five aliens in turn, so we need to set up a table.  In Sinclair BASIC we might go about this by defining three arrays of fifty-five elements – one for the invaders’ x coordinates, one for y coordinates, plus a third status byte.  We could do something similar in assembler by setting up three tables of fifty-five bytes each in memory, then adding the number for each alien to the start of each table to access the individual element.  Unfortunately, that would be slow and cumbersome.

A far better method is to group the three data elements for each invader into a structure, and then have fifty-five of these structures in a table.  We can then point hl to the address of each invader, and know that hl points to the status byte, hl plus one points to the x coordinate, and hl plus two points to the y coordinate.  The code to display an alien might look something like this:

       ld hl,aliens        ; alien data structures.
       ld b,55             ; number of aliens.
loop0  call show           ; show this alien.
       djnz loop0          ; repeat for all aliens.
       ret
show   ld a,(hl)           ; fetch alien status.
       cp 255              ; is alien switched off?
       jr z,next           ; yes, so don't display him.
       push hl             ; store alien address on the stack.
       inc hl              ; point to x coord.
       ld d,(hl)           ; get coord.
       inc hl              ; point to y coord.
       ld e,(hl)           ; get coord.
       call disply         ; display alien at (d,e).
       pop hl              ; retrieve alien address from the stack.
next   ld de,3             ; size of each alien table entry.
       add hl,de           ; point to next alien.
       ret                 ; leave hl pointing to next one.

Using the Index Registers

The drawback with this routine is that we have to be very careful where hl is pointing to all the time, so it might be an idea to store hl in a two-byte temporary memory location before calling show, then restoring it afterwards, adding three at the end of the main loop, then performing the djnz instruction.  If we were writing for the Nintendo GameBoy with its cut-down Z80 this would probably represent our best option.  On machines with more advanced processors such as the Spectrum and CPC464 we can use the index registers, ix, to simplify our code a little.  Because the ix register pair allows us to displace our indirect addressing, we can point ix to the beginning of an alien’s data structure and access all elements within it without the need to change ix again.  Using ix our alien display routine might look like this:

       ld ix,aliens        ; alien data structures.
       ld b,55             ; number of aliens.
loop0  call show           ; show this alien.
       ld de,3             ; size of each alien table entry.
       add ix,de           ; point to next alien.
       djnz loop0          ; repeat for all aliens.
       ret
show   ld a,(ix)           ; fetch alien status.
       cp 255              ; is alien switched off?
       ret z               ; yes, so don't display him.
       ld d,(ix+1)         ; get coord.
       ld e,(ix+2)         ; get coord.
       jp disply           ; display alien at (d,e).

Using ix means we only ever need to point to the beginning of an alien’s data structure, so ix will always return the status for the current invader, ix+1 the x coordinate, and so on.  This method enables the programmer to use complex data structures for his aliens of up to 128 bytes long, without getting confused as to which bit of the structure our registers are pointing at any given time as with the hl example earlier.  Unfortunately, using ix is a little slower than hl, so we shouldn’t use it for the more intensive processing tasks such as manipulating graphics.

Let us apply this method to our Centipede game.  Firstly, we need to decide how many segments are needed, and what data to store about each segment.  In our game the segments will need to move left or right until they hit a mushroom, then move down and go back the other way.  So it seems we will need a flag to indicate the particular direction a segment is travelling in, plus an x or y coordinate.  Our flag can also be used to indicate that a particular segment has been destroyed.  With this in mind we can set up a data structure of three bytes:

centf  defb 0              ; flag, 0=left, 1=right, 255=dead.
centx  defb 0              ; segment x coordinate.
centy  defb 0              ; segment y coordinate.

If we choose to have ten segments in our centipede, we need to reserve a table space of thirty bytes. Each segment needs to be initialised at the beginning, then deleted, moved and redisplayed during the game.

Initialising our segments is probably the simplest task, so we can use a simple loop incrementing the hl register pair for each byte before setting it.  Something like this will usually do the trick:

       ld b,10             ; number of segments to initialise.
       ld hl,segmnt        ; segment table.
segint ld (hl),1           ; start off moving right.
       inc hl
       ld (hl),0           ; start at top.
       inc hl
       ld (hl),b           ; use B register as y coordinate.
       inc hl
       djnz segint         ; repeat until all initialised.

Processing and displaying each segment is going to be slightly more complicated, so for that we will use the ix registers.  We need to write a simple algorithm which manipulates a single segment left or right until it hits a mushroom, then moves down and switches direction.  We’ll call this routine proseg (for “process segment”), and set up a loop which points to each segment in turn and calls proseg.  Providing we get the movement algorithm correct we should then see a centipede snaking its way through the mushrooms.  Applying this to our code is straightforward – we check the flag byte for each segment (ix) to see which way the segment is moving, increment or decrement the horizontal coordinate (ix+2) accordingly, then check the attribute at that character cell.  If it’s green and black we increment the vertical coordinate (ix+1), and switch the direction flag (ix).

Okay, there are one or two other things to consider, such as hitting the sides or bottom of the screen, but that’s just a case of checking the segment’s coordinates and switching direction or moving to the top of the screen when we need to.  The segments also need to be deleted from their old positions prior to being moved, the redisplayed at their new positions, but we have already covered the steps required to perform those tasks.

Our new code looks like this:

; We want a black screen.

       ld a,71             ; white ink (7) on black paper (0),
                           ; bright (64).
       ld (23693),a        ; set our screen colours.
       xor a               ; quick way to load accumulator with zero.
       call 8859           ; set permanent border colours.

; Set up the graphics.

       ld hl,blocks        ; address of user-defined graphics data.
       ld (23675),hl       ; make UDGs point to it.

; Okay, let's start the game.

       call 3503           ; ROM routine - clears screen, opens chan 2.

; Initialise coordinates.

       ld hl,21+15*256     ; load hl pair with starting coords.
       ld (plx),hl         ; set player coords.

       ld b,10             ; number of segments to initialise.
       ld hl,segmnt        ; segment table.
segint ld (hl),1           ; start off moving right.
       inc hl
       ld (hl),0           ; start at top.
       inc hl
       ld (hl),b           ; use B register as y coordinate.
       inc hl
       djnz segint         ; repeat until all initialised.

       call basexy         ; set the x and y positions of the player.
       call splayr         ; show player base symbol.

; Now we want to fill the play area with mushrooms.

       ld a,68             ; green ink (4) on black paper (0),
                           ; bright (64).
       ld (23695),a        ; set our temporary colours.
       ld b,50             ; start with a few.
mushlp ld a,22             ; control code for AT character.
       rst 16
       call random         ; get a 'random' number.
       and 15              ; want vertical in range 0 to 15.
       rst 16
       call random         ; want another pseudo-random number.
       and 31              ; want horizontal in range 0 to 31.
       rst 16
       ld a,145            ; UDG 'B' is the mushroom graphic.
       rst 16              ; put mushroom on screen.
       djnz mushlp         ; loop back until all mushrooms displayed.


; This is the main loop.

mloop  equ $

; Delete the player.

       call basexy         ; set the x and y positions of the player.
       call wspace         ; display space over player.

; Now we've deleted the player we can move him before redisplaying him
; at his new coordinates.

       ld bc,63486         ; keyboard row 1-5/joystick port 2.
       in a,(c)            ; see what keys are pressed.
       rra                 ; outermost bit = key 1.
       push af             ; remember the value.
       call nc,mpl         ; it's being pressed, move left.
       pop af              ; restore accumulator.
       rra                 ; next bit along (value 2) = key 2.
       push af             ; remember the value.
       call nc,mpr         ; being pressed, so move right.
       pop af              ; restore accumulator.
       rra                 ; next bit (value 4) = key 3.
       push af             ; remember the value.
       call nc,mpd         ; being pressed, so move down.
       pop af              ; restore accumulator.
       rra                 ; next bit (value 8) reads key 4.
       call nc,mpu         ; it's being pressed, move up.


; Now he's moved we can redisplay the player.

       call basexy         ; set the x and y positions of the player.
       call splayr         ; show player.

; Now for the centipede segments.

       ld ix,segmnt        ; table of segment data.
       ld b,10             ; number of segments in table.
censeg push bc
       ld a,(ix)           ; is segment switched on?
       inc a               ; 255=off, increments to zero.
       call nz,proseg      ; it's active, process segment.
       pop bc
       ld de,3             ; 3 bytes per segment.
       add ix,de           ; get next segment in ix registers.
       djnz censeg         ; repeat for all segments.

       halt                ; delay.

; Jump back to beginning of main loop.

       jp mloop

; Move player left.

mpl    ld hl,ply           ; remember, y is the horizontal coord!
       ld a,(hl)           ; what's the current value?
       and a               ; is it zero?
       ret z               ; yes - we can't go any further left.

; now check that there isn't a mushroom in the way.

       ld bc,(plx)         ; current coords.
       dec b               ; look 1 square to the left.
       call atadd          ; get address of attribute at this position.
       cp 68               ; mushrooms are bright (64) + green (4).
       ret z               ; there's a mushroom - we can't move there.

       dec (hl)            ; subtract 1 from y coordinate.
       ret

; Move player right.

mpr    ld hl,ply           ; remember, y is the horizontal coord!
       ld a,(hl)           ; what's the current value?
       cp 31               ; is it at the right edge (31)?
       ret z               ; yes - we can't go any further left.

; now check that there isn't a mushroom in the way.

       ld bc,(plx)         ; current coords.
       inc b               ; look 1 square to the right.
       call atadd          ; get address of attribute at this position.
       cp 68               ; mushrooms are bright (64) + green (4).
       ret z               ; there's a mushroom - we can't move there.

       inc (hl)            ; add 1 to y coordinate.
       ret

; Move player up.

mpu    ld hl,plx           ; remember, x is the vertical coord!
       ld a,(hl)           ; what's the current value?
       cp 4                ; is it at upper limit (4)?
       ret z               ; yes - we can go no further then.

; now check that there isn't a mushroom in the way.

       ld bc,(plx)         ; current coords.
       dec c               ; look 1 square up.
       call atadd          ; get address of attribute at this position.
       cp 68               ; mushrooms are bright (64) + green (4).
       ret z               ; there's a mushroom - we can't move there.

       dec (hl)            ; subtract 1 from x coordinate.
       ret

; Move player down.

mpd    ld hl,plx           ; remember, x is the vertical coord!
       ld a,(hl)           ; what's the current value?
       cp 21               ; is it already at the bottom (21)?
       ret z               ; yes - we can't go down any more.

; now check that there isn't a mushroom in the way.

       ld bc,(plx)         ; current coords.
       inc c               ; look 1 square down.
       call atadd          ; get address of attribute at this position.
       cp 68               ; mushrooms are bright (64) + green (4).
       ret z               ; there's a mushroom - we can't move there.

       inc (hl)            ; add 1 to x coordinate.
       ret

; Set up the x and y coordinates for the player's gunbase position,
; this routine is called prior to display and deletion of gunbase.

basexy ld a,22             ; AT code.
       rst 16
       ld a,(plx)          ; player vertical coord.
       rst 16              ; set vertical position of player.
       ld a,(ply)          ; player's horizontal position.
       rst 16              ; set the horizontal coord.
       ret

; Show player at current print position.

splayr ld a,69             ; cyan ink (5) on black paper (0),
                           ; bright (64).
       ld (23695),a        ; set our temporary screen colours.
       ld a,144            ; ASCII code for User Defined Graphic 'A'.
       rst 16              ; draw player.
       ret

wspace ld a,71             ; white ink (7) on black paper (0),
                           ; bright (64).
       ld (23695),a        ; set our temporary screen colours.
       ld a,32             ; SPACE character.
       rst 16              ; display space.
       ret

segxy  ld a,22             ; ASCII code for AT character.
       rst 16              ; display AT code.
       ld a,(ix+1)         ; get segment x coordinate.
       rst 16              ; display coordinate code.
       ld a,(ix+2)         ; get segment y coordinate.
       rst 16              ; display coordinate code.
       ret

proseg ld a,(ix)           ; check if segment was switched off
       inc a               ; by collision detection routine.
       ret z               ; it was, so this segment is now dead.
       call segxy          ; set up segment coordinates.
       call wspace         ; display a space, white ink on black.
       call segmov         ; move segment.
       ld a,(ix)           ; check if segment was switched off
       inc a               ; by collision detection routine.
       ret z               ; it was, so this segment is now dead.
       call segxy          ; set up segment coordinates.
       ld a,2              ; attribute code 2 = red segment.
       ld (23695),a        ; set temporary attributes.
       ld a,146            ; UDG 'C' to display segment.
       rst 16
       ret
segmov ld a,(ix+1)         ; x coord.
       ld c,a              ; GP x area.
       ld a,(ix+2)         ; y coord.
       ld b,a              ; GP y area.
       ld a,(ix)           ; status flag.
       and a               ; is the segment heading left?
       jr z,segml          ; going left - jump to that bit of code.

; so segment is going right then!

segmr  ld a,(ix+2)         ; y coord.
       cp 31               ; already at right edge of screen?
       jr z,segmd          ; yes - move segment down.
       inc a               ; look right.
       ld b,a              ; set up GP y coord.
       call atadd          ; find attribute address.
       cp 68               ; mushrooms are bright (64) + green (4).
       jr z,segmd          ; mushroom to right, move down instead.
       inc (ix+2)          ; no obstacles, so move right.
       ret

; so segment is going left then!

segml  ld a,(ix+2)         ; y coord.
       and a               ; already at left edge of screen?
       jr z,segmd          ; yes - move segment down.
       dec a               ; look right.
       ld b,a              ; set up GP y coord.
       call atadd          ; find attribute address at (dispx,dispy).
       cp 68               ; mushrooms are bright (64) + green (4).
       jr z,segmd          ; mushroom to left, move down instead.
       dec (ix+2)          ; no obstacles, so move left.
       ret

; so segment is going down then!

segmd  ld a,(ix)           ; segment direction.
       xor 1               ; reverse it.
       ld (ix),a           ; store new direction.
       ld a,(ix+1)         ; y coord.
       cp 21               ; already at bottom of screen?
       jr z,segmt          ; yes - move segment to the top.

; At this point we're moving down regardless of any mushrooms that 
; may block the segment's path.  Anything in the segment's way will
; be obliterated.

       inc (ix+1)          ; haven't reached the bottom, move down.
       ret

; moving segment to the top of the screen.

segmt  xor a               ; same as ld a,0 but saves 1 byte.
       ld (ix+1),a         ; new x coordinate = top of screen.
       ret

; Simple pseudo-random number generator.
; Steps a pointer through the ROM (held in seed), returning
; the contents of the byte at that location.

random ld hl,(seed)        ; Pointer
       ld a,h
       and 31              ; keep it within first 8k of ROM.
       ld h,a
       ld a,(hl)           ; Get "random" number from location.
       inc hl              ; Increment pointer.
       ld (seed),hl
       ret
seed   defw 0


; Calculate address of attribute for character at (dispx, dispy).

atadd  ld a,c              ; vertical coordinate.
       rrca                ; multiply by 32.
       rrca                ; Shifting right with carry 3 times is
       rrca                ; quicker than shifting left 5 times.
       ld e,a
       and 3
       add a,88            ; 88x256=address of attributes.
       ld d,a
       ld a,e
       and 224
       ld e,a
       ld a,b              ; horizontal position.
       add a,e
       ld e,a              ; de=address of attributes.
       ld a,(de)           ; return with attribute in accumulator.
       ret

plx    defb 0              ; player's x coordinate.
ply    defb 0              ; player's y coordinate.

; UDG graphics.

blocks defb 16,16,56,56,124,124,254,254    ; player base.
       defb 24,126,255,255,60,60,60,60     ; mushroom.
       defb 24,126,126,255,255,126,126,24  ; segment.


; Table of segments.
; Format: 3 bytes per entry, 10 segments.
; byte 1: 255=segment off, 0=left, 1=right.
; byte 2 = x (vertical) coordinate.
; byte 3 = y (horizontal) coordinate.

segmnt defb 0,0,0          ; segment 1.
       defb 0,0,0          ; segment 2.
       defb 0,0,0          ; segment 3.
       defb 0,0,0          ; segment 4.
       defb 0,0,0          ; segment 5.
       defb 0,0,0          ; segment 6.
       defb 0,0,0          ; segment 7.
       defb 0,0,0          ; segment 8.
       defb 0,0,0          ; segment 9.
       defb 0,0,0          ; segment 10.
%d bloggers like this: