Archive

Posts Tagged ‘Z80 Assembly’

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.

64 Column print

January 8, 2010 Leave a comment

The source code for 64 column printing was originally provided by Andrew Owen in a thread on WoSF. Reproduced with permission.

BASIC normally provides only 32 columns for printing text. This machine code routine, which can be called from BASIC, effectively doubles the number of columns horizontally available for printing.

In BASIC, do a CLEAR 49999 so that BASIC won’t overrun the machine code in memory.

Assemble or load the following routine at address 50000. Do a RANDOMIZE USR 50000, to run the routine and perform the magic for 64 column printing.

To print in 64 columns, use stream 4, for eg: PRINT #4;AT 0,53;”Hello World”

; ---------------
; 4x8 font driver, (c) 2007 Andrew Owen
; ---------------

	org	50000		;

; --------------------------------
; CREATE CHANNEL AND ATTACH STREAM
; --------------------------------
;
; Based on code by Ian Beardsmore from Your Spectrum issue 7, September 1984.

c_chan:	ld	hl,($5c53)	; a channel must be created below basic
				; so look at the system variable PROG
	dec	hl		; move hl down one address

	ld	bc,$0005	; the new channel takes 5 bytes
	call	$1655		; call the MAKE_ROOM routine
	inc	hl		; move HL up one address

	ld	bc,chan_4	; could write the bytes directly but
				; then code would be non-relocatable

	ld	(hl),c		; low byte of the output routine
	inc	hl		; move HL up one address
	push	hl		; save this address for later

	ld	(hl),b		; high byte of the output routine
	inc	hl		; move HL up one address

	ld	bc,$15c4	; address of input routine

	ld	(hl),c		; low byte of the input routine
	inc	hl		; move HL up one address

	ld	(hl),b		; high byte of the input routine
	inc	hl		; move HL up one address 

	ld	(hl),'P'	; channel type; 'K', 'S', 'R' or 'P'

; attach stream

	pop	hl		; the first address plus one of the
				; extra space stored earlier
	ld	de,($5c4f)	; store the contents of CHANS in DE
	and	a		; clear the carry flag before
				; calculation
	sbc	hl,de		; the difference between the start of
				; the channels area and the start of the
				; extra space becomes the offset, stored
				; in HL
	ex	de,hl		; store the offset in DE

	ld	hl,$5c10	; store the contents of STRMS in HL
	ld	a,$04		; stream number 4
	add	a,$03		; take account of streams -3 to -1
	add	a,a		; each of the seven default streams has
				; two bytes of offset data
				; the total number of bytes occupied,
				; held in a, forms the offset for the
				; new stream
	ld	b,$00		; set b to hold $00
	ld	c,a		; set the low byte of the offset
	add	hl,bc		; the offset is added to the base
				; address to give the correct location
				; in the streams table to store the
				; offset
	ld	(hl),e		; the low byte of the offset
	inc	hl		; move HL up one address
	ld	(hl),d		; the high byte of the offset
	ret			; all done

; -----------------
; CHANNEL #4 OUTPUT
; -----------------
;
; Based on code by Tony Samuels from Your Spectrum issue 13, April 1985.
; A channel wrapper for the 64-column display driver.

chan_4:	ld	b,a		; save character
	ld	a,(atflg)	; value of AT flag
	and	a		; test against zero
	jr	nz,getrow	; jump if not
	ld	a,b		; restore character

atchk:	cp	$16		; test for AT
	jr	nz,crchk	; if not test for CR
	ld	a,$ff		; set the AT flag
	ld	(atflg),a	; next character will be row
	ret			; return

getrow:	cp	$fe		; test AT flag
	jr	z,getcol	; jump if setting col
	ld	a,b		; restore character
	cp	$18		; greater than 23?
	jr	nc,err_b	; error if so

	ld	(row),a		; store it in row
	ld	hl,atflg	; AT flag
	dec	(hl)		; indicates next character is col
	ret			; return

getcol:	ld	a,b		; restore character
	cp	$40		; greater than 63?
	jr	nc,err_b	; error if so
	ld	(col),a		; store it in col
	xor	a		; set a to zero
	ld	(atflg),a	; store in AT flag
	ret			; return

err_b:	xor	a		; set a to zero
	ld	(atflg),a	; clear AT flag
	rst	08h		;
	defb	$0a		;

crchk:	cp	$0d		; check for return
	jr	z,do_cr		; to carriage return if so
	call	pr_64		; print it

	ld	hl,col		; increment
	inc	(hl)		; the column
	ld	a,(hl)		;

	cp	$40		; column 64?
	ret	nz		;

do_cr:	xor	a		; set A to zero
	ld	(col),a		; reset column
	ld	a,(row)		; get the row
	inc	a		; increment it
	cp	$18		; row 24?
	jr	z,wrap		;

zend:	ld	(row),a		; write it back
	ret

wrap:	xor	a		;
	jr	zend		;

; ------------------------
; 64 COLUMN DISPLAY DRIVER
; ------------------------

pr_64:	rra			; divide by two with remainder in carry flag

	ld	h,$00		; clear H
	ld	l,a		; CHAR to low byte of HL

	ex	af,af'		; save the carry flag

	add	hl,hl		; multiply
	add	hl,hl		; by
	add	hl,hl		; eight
	ld	de,font-$80	; offset to FONT
	add	hl,de		; HL holds address of first byte of
				; character map in FONT
	push	hl		; save font address

; convert the row to the base screen address

	ld	a,(row)		; get the row
	ld	b,a		; save it
	and	$18		; mask off bit 3-4
	ld	d,a		; store high byte of offset in D
	ld	a,b		; retrieve it
	and	$07		; mask off bit 0-2
	rlca			; shift
	rlca			; five
	rlca			; bits
	rlca			; to the
	rlca			; left
	ld	e,a		; store low byte of offset in E

; add the column

	ld	a,(col)		; get the column
	rra			; divide by two with remainder in carry flag
	push	af		; store the carry flag

	ld	h,$40		; base location
	ld	l,a		; plus column offset

	add	hl,de		; add the offset

	ex	de,hl		; put the result back in DE

; HL now points to the location of the first byte of char data in FONT_1
; DE points to the first screen byte in SCREEN_1
; C holds the offset to the routine

	pop	af		; restore column carry flag
	pop	hl		; restore the font address

	jr	nc,odd_col	; jump if odd column

even_col:
	ex	af,af'		; restore char position carry flag
	jr	c,l_on_l	; left char on left col
	jr	r_on_l		; right char on left col

odd_col:
	ex	af,af'		; restore char position carry flag
	jr	nc,r_on_r	; right char on right col
	jr	l_on_r		; left char on right col

; -------------------------------
; WRITE A CHARACTER TO THE SCREEN
; -------------------------------
;
; There are four separate routines

; HL points to the first byte of a character in FONT
; DE points to the first byte of the screen address

; left nibble on left hand side

l_on_l:	ld	c,$08		; 8 bytes to write
ll_lp:	ld	a,(de)		; read byte at destination
	and	$f0		; mask area used by new character
	ld	b,a		; store in b
	ld	a,(hl)		; get byte of font
	and	$0f		; mask off unused half
	or	b		; combine with background
	ld	(de),a		; write it back
	inc	d		; point to next screen location
	inc	hl		; point to next font data
	dec	c		; adjust counter
	jr	nz,ll_lp	; loop 8 times
	ret			; done

; right nibble on right hand side

r_on_r:	ld	c,$08		; 8 bytes to write
rr_lp:	ld	a,(de)		; read byte at destination
	and	$0f		; mask area used by new character
	ld	b,a		; store in b
	ld	a,(hl)		; get byte of font
	and	$f0		; mask off unused half
	or	b		; combine with background
	ld	(de),a		; write it back
	inc	d		; point to next screen location
	inc	hl		; point to next font data
	dec	c		; adjust counter
	jr	nz,rr_lp	; loop 8 times
	ret			; done

; left nibble on right hand side

l_on_r:	ld	c,$08		; 8 bytes to write
lr_lp:	ld	a,(de)		; read byte at destination
	and	$0f		; mask area used by new character
	ld	b,a		; store in b
	ld	a,(hl)		; get byte of font
	rrca			; shift right
	rrca			; four bits
	rrca			; leaving 7-4
	rrca			; empty
	and	$f0		;
	or	b		; combine with background
	ld	(de),a		; write it back
	inc	d		; point to next screen location
	inc	hl		; point to next font data
	dec	c		; adjust counter
	jr	nz,lr_lp	; loop 8 times
	ret			; done

; right nibble on left hand side

r_on_l:	ld	c,$08		; 8 bytes to write
rl_lp:	ld	a,(de)		; read byte at destination
	and	$f0		; mask area used by new character
	ld	b,a		; store in b
	ld	a,(hl)		; get byte of font
	rlca			; shift left
	rlca			; four bits
	rlca			; leaving 3-0
	rlca			; empty
	and	$0f		;
	or	b		; combine with background
	ld	(de),a		; write it back
	inc	d		; point to next screen location
	inc	hl		; point to next font data
	dec	c		; adjust counter
	jr	nz,rl_lp	; loop 8 times
	ret			; done

; --------------
; TEXT VARIABLES
; --------------
;
; Used by the 64 column driver

atflg:	defb	$00		; AT flag
row:	defb	$00		; row
col:	defb	$00		; col

; -------------------
; half width 4x8 font
; -------------------
;
; 384 bytes

font:
	defb	$00,$02,$02,$02,$02,$00,$02,$00,$00,$52,$57,$02,$02,$07,$02,$00;
	defb	$00,$25,$71,$62,$32,$74,$25,$00,$00,$22,$42,$30,$50,$50,$30,$00;
	defb	$00,$14,$22,$41,$41,$41,$22,$14,$00,$20,$70,$22,$57,$02,$00,$00;
	defb	$00,$00,$00,$00,$07,$00,$20,$20,$00,$01,$01,$02,$02,$04,$14,$00;
	defb	$00,$22,$56,$52,$52,$52,$27,$00,$00,$27,$51,$12,$21,$45,$72,$00;
	defb	$00,$57,$54,$56,$71,$15,$12,$00,$00,$17,$21,$61,$52,$52,$22,$00;
	defb	$00,$22,$55,$25,$53,$52,$24,$00,$00,$00,$00,$22,$00,$00,$22,$02;
	defb	$00,$00,$10,$27,$40,$27,$10,$00,$00,$02,$45,$21,$12,$20,$42,$00;
	defb	$00,$23,$55,$75,$77,$45,$35,$00,$00,$63,$54,$64,$54,$54,$63,$00;
	defb	$00,$67,$54,$56,$54,$54,$67,$00,$00,$73,$44,$64,$45,$45,$43,$00;
	defb	$00,$57,$52,$72,$52,$52,$57,$00,$00,$35,$15,$16,$55,$55,$25,$00;
	defb	$00,$45,$47,$45,$45,$45,$75,$00,$00,$62,$55,$55,$55,$55,$52,$00;
	defb	$00,$62,$55,$55,$65,$45,$43,$00,$00,$63,$54,$52,$61,$55,$52,$00;
	defb	$00,$75,$25,$25,$25,$25,$22,$00,$00,$55,$55,$55,$55,$27,$25,$00;
	defb	$00,$55,$55,$25,$22,$52,$52,$00,$00,$73,$12,$22,$22,$42,$72,$03;
	defb	$00,$46,$42,$22,$22,$12,$12,$06,$00,$20,$50,$00,$00,$00,$00,$0F;
	defb	$00,$20,$10,$03,$05,$05,$03,$00,$00,$40,$40,$63,$54,$54,$63,$00;
	defb	$00,$10,$10,$32,$55,$56,$33,$00,$00,$10,$20,$73,$25,$25,$43,$06;
	defb	$00,$42,$40,$66,$52,$52,$57,$00,$00,$14,$04,$35,$16,$15,$55,$20;
	defb	$00,$60,$20,$25,$27,$25,$75,$00,$00,$00,$00,$62,$55,$55,$52,$00;
	defb	$00,$00,$00,$63,$55,$55,$63,$41,$00,$00,$00,$53,$66,$43,$46,$00;
	defb	$00,$00,$20,$75,$25,$25,$12,$00,$00,$00,$00,$55,$55,$27,$25,$00;
	defb	$00,$00,$00,$55,$25,$25,$53,$06,$00,$01,$02,$72,$34,$62,$72,$01;
	defb	$00,$24,$22,$22,$21,$22,$22,$04,$00,$56,$A9,$06,$04,$06,$09,$06;