Author Topic: Z80 ASM game 30 minute tutorial  (Read 18451 times)

0 Members and 1 Guest are viewing this topic.

Offline bj

  • ZX 81
  • *
  • Posts: 20
  • Karma: 10
    • View Profile
Z80 ASM game 30 minute tutorial
« on: November 06, 2009 »
If any of you fancy learning Z80 assembly, I've written a short tutorial on machine-code game programming for the ZX Spectrum. Your comments and suggestions welcome.

Code: [Select]
ZX SPECTRUM MACHINE CODE GAME 30 MINUTE TUTORIAL
By bigjon (Jon Kingsman)
Roadrace game by bigjon, incorporating suggestions from Dr Beep, skoolkid and Matt B


Hi folks, I'm a machine code novice who coded a very small roadrace game to help me learn.
I reckon you can learn the basics of machine code in half an hour by coding this game step by step.
This tutorial assumes you have a working knowledge of ZX Spectrum basic, and the ZX Spin emulator.
Make yourself a large cup of tea - by the time you've drunk it, you be able to program in machine code!


CHAPTER 1 - Create a machine code function that returns the score to BASIC

Machine code programs are a series of bytes in the Spectrum's memory.
In this chapter we will
- use the Spin assembler to write a few bytes into the memory generating a score for our game.
- write a BASIC program to run the machine code and print the score we have obtained from it.

Open ZX Spin. Select Tools -> Z80 Assembler.
To run our roadrace game, we need to execute the following steps

MAIN ;label for main section of program as opposed to graphics data etc
org 33000
;org 33000 = arrange to put our machine code at free, fast-running (over 32768) and memorable address in RAM
;initialise score
;initialise road, car
PRINCIPALLOOP ;label for the loop in the game that will execute over and over
;read keyboard
;set new carposition
;crash? if so, go to GAMEOVER.
;print car
;scroll road
;random road left or right
;jump back to PRINCIPALLOOP
GAMEOVER ; label for the cleaning up that needs to be done before returning to BASIC
;return score to BASIC

Copy and paste the paragraph above into the Spin Assembler.It will appear as 15 lines of mainly grey text.
The text is grey because text after a ; is a comment. The assembler ignores it but it's there for our benefit.
You can TAB comments over towards the right-hand side of the assembler page to make your code more readable.
The labels are in pink.The assembler won't put anything in RAM for them but will use them as entry points to jump to.
In the assembler, do File -> Save as and type something like mc30mintut.asm into the save box.

We'll do the first and last of these steps in this chapter, starting with the last one.
The assembly language instruction for 'return to calling program' (in our case a BASIC routine) is 'ret'.
Click on the end of line 15, press enter to create line 16 and type ret
The word 'ret' appears in blue. This is Spin's colour code for an instruction.
When the Spin assembler gets to the instruction 'ret' it writes the byte 201 into the memory at an address we choose.
The computer knows that the first byte it meets will be an instruction byte.
It does something different for each byte from 0 to 256. There's a list in Appendix A of the Spectrum Manual.
Some instruction bytes, like 201 for 'ret', are complete as is - no further info is needed to complete the action.
Some instruction bytes need one or two bytes of data afterwards for the computer to know what to do.
Some instruction bytes need a further instruction byte to clarify the action required.

Now we'll initialise the score, ready for the mc program to report it back to BASIC at the end of the game.
The computer does most of its work using 7 temporary byte-sized addresses called 'registers'.
The first register deals with single bytes only (numbers 0 to 255), the other six are in pairs to deal with 0 to 65355.
The first (single-byte) register is called the A register, sometimes also referred to as the accumulator.
The other three register pairs are called BC, DE, and HL (H is for High byte, L is for Low byte)
Any machine code function called from basic will return the value from 0 to 65355 in the BC register.
We will write the value 0 into the BC register, ready to increase it by 1 each time the game goes round its principal loop.
At the beginning of line 4, type 'ld bc,0'. ld is the instruction for load a value into a register or register pair.
The instruction byte for ld is different for each register or register pair that is loaded.
The instruction byte for ld bc is 1. The computer then expects 2 data bytes to give it a number from 0 to 65355.
In our case the two data bytes will be 0,0. So the assembler will write 1,0,0,201 at address 33000 in RAM
We'll assemble this code now. Do File -> Save, then File -> Assemble. Type 33000 into the Start Address box and click OK.
At the bottom window of the assembler you should see a report that says "No errors in 16 lines. 4 bytes generated"
You can see the four bytes are now at memory address 33000 by clicking in the main Spin display window on Tools -> Debugger

To run these four bytes of machine code, enter this one-line program in the main Spin display window:
10 PRINT AT 0,0; "Your score was "; USR 33000
Now RUN the program. Did you get "Your score was 0"? Congratulations - you have coded your first machine code program!
Do File -> Save in the main Spin display window and save as something like mc30mintut.sna. Here ends Chapter 1!


CHAPTER 2 - Display material on the screen.

There are two areas of the Spectrum's memory which have a direct effect on the screen display.
The complicated way is the Display file, from addresses 16384 to 22527, which stores a dash of 8 pixels per byte.
Try POKE-ing 255 into bytes within this range to see the funny order in which this memory area is mapped onto the screen.
The simple way is the Attribute file, from 22528 to 23296, which affects an 8x8 pixel block per byte, in logical order.
In this chapter we will
- draw our 'car' by changing the paper colour of one character square to blue.
- draw our 'road' by using a loop to create two vertical stripes of black paper colour down the screen.

In the spin assembler line 5, delete the word 'road' in the comments.
At the beginning of line 5, type ld hl,23278. This points HL to the middle of the bottom row in the display file.
Insert line 6, ld a,8. This puts a blue PAPER colour into the A register. Why 8? See the BASIC manual chapter 16.
Insert line 7, ld (hl),a. The brackets round hl mean the load will be to the address in RAM that hl is pointing to.
Insert line 8, ld (32900),hl ;save car posn. We'll store the attribute file address of the 'car' in some free bytes in RAM.
Now for the road. Insert line 4, ld hl,22537 ;initialise road. This points to a third of the way along the top line.
To save the road position, which we'll need frequently, we'll let the computer choose where to store it, on its 'stack'
Chapter 24 of the manual has a diagram showing where the machine stack is in the RAM.
To write to the stack we use 'push'. To write from the stack we use 'pop'. What goes on the stack first will come off last.
Insert line 5, push hl  ;save road posn. Insert line 21, pop hl  ;empty stack
To print a black road we need 0 in the accumulator (ch16 of the BASIC manual).
We could do 'ld a, 0' but this takes 2 bytes whereas 'xor a' takes only one. Insert line 6, 'xor a'
xor compares the chosen register to the A register and puts a 1 in the A register for each bit that is different.
We'll print the top line of the road. Two double squares of black with a 9-square gap between them.
Insert line 7, then copy and paste the following code:
ld (hl),a
inc hl ;inc increases the register by one, dec decreases it by one.
ld (hl),a
ld de,9 ;for a 9-space gap in the road.
add hl,de ;add adds the registers together, so hl points to the right hand side of the road.
ld (hl),a
inc hl
ld (hl),a

To get hl to point to the left hand verge on the next line, we need to move 21 bytes further in the attribute file.
Insert line 15, 'ld de, 21 ;point to left verge on next line'
Insert line 16, 'add hl,de'
To fill the screen with the road we will use machine code's equivalent of a FOR-NEXT loop, djnz.
djnz stands for Decrement then Jump if Not Zero. We load the b register with the number of times we want to loop.
Insert line 7, 'ld b,24 ;print road verge on 24 lines'
Insert line 8 'fillscreen' - this is the label for our loop to jump back to.
Insert line 19 'djnz fillscreen'
Because our routine will continue from the loop when b=0, we no longer need to initialise b as well as c to 0 in the next line.
Change line 20 'ld bc, 0' to 'ld c,b'. This is one byte shorter.
Assemble and save. If you want to see the blue 'car', you'll need to add something like 20 PAUSE 0 to your basic program.


CHAPTER 3 - move the car, test for collision.

Time to start playing the game! First we need to erase the car ready to move it if the player wants to.
Insert line 26, then copy and paste the following code
ld hl,(32900) ;retrieve car posn
ld a,56  ;erase car
ld (hl),a

Before we read the keyboard we will lock the keyboard for most of the game, and unlock it only when we want to read the keys.
The instruction to lock the keyboard is 'di' = 'disable interrupts. Its opposite is 'ei' = 'enable interrupts'
Replace line 3 with 'di'. Insert line 29, ei. Insert line 31, di. Insert line 41, ei
To read the keys we use the IN ports - see ch23 of the BASIC manual - to read the left and right half of the bottom row.
We load bc with the port number and use the instruction cp (= compare) to see if the number has dropped to show a keypress
Delete line30 and replace with the following code
ld bc,65278 ;read keyboard caps to v
in a,(c)
cp 191
jr nz, moveright
inc l
moveright
ld bc,32766 ;read keyboard space to b
in a,(c)
cp 191
jr nz, dontmove
dec l
dontmove

'jr nz' stands for jump relative if not zero. It skips over the instruction to increment / decrement the car position.

Replace line 43 with the following to see if we bump into the oncoming road 32 bytes (1 screen) down the attribute file.
ld (32900),hl ;store car posn
ld de, 32 ;new carposn
xor a  ;set carry flag to 0
sbc hl,de
ld a,(hl) ;crash?
or a
jr z,gameover
ld a,8  ;print car
ld (hl),a

We'd like to 'sub hl,de' but there's no such instruction so we use sbc, subtract with carry, and set the carry flag to zero.
'or' compares the register to the a register bit by bit and leaves a 1 in the a register for each bit that is 1 in either.
If all the digits are zero, then the zero flag will be set, so we can use 'or a' to test for a black paper colour.
Delete line 53. Delete line 53 again!

To clean up the score at GAMEOVER insert line21, 'push bc; save score'. Replace line 57 with 'pop bc;retrieve score'
To cycle round the game before GAMEOVER change line 55 to 'jp PRINCIPALLOOP'.
Assemble, save, and run. You'll need to deliberately crash to get out!


CHAPTER 4 - scroll and move the road, keep score, adjust speed.

To scroll the road down the screen we copy the screen attribute bytes to the line beneath 736 times.
We use the instruction lddr, which stand for LoaD ((hl) to (de)),Decrement (hl and de) and Repeat (until bc is zero).
Replace line 53 with the following
ld hl,23263 ;scroll road
ld de,23295
ld bc,736
lddr
pop bc  ;retrieve score

To add 1 to the score and save it ready for GAMEOVER, insert the following into line 59
inc bc  ;add 1 to score
push bc  ;save score

To move the road randomly left or right on the top line we use the following algorithm -
Choose a location in ROM where the are 256 random looking bytes and add the low byte of the score in bc to it.
If it is odd, lower the road position in hl by one. If it is even, increase by one.
(To test the last bit for odd and even we use 'and 1' which "masks" the last bit and sets the zero flag if it is 0)
Check to see if the road has reached the edge of the screen and bump it away if it has.
Print the new road top line like we did in chapter 2.

Replace line 58 with the following hefty chunk of code

pop hl  ;retrieve road posn
push hl  ;save road posn
ld a,56  ;delete old road
ld (hl),a
inc hl
ld (hl),a
ld de,9
add hl,de
ld (hl),a
inc hl
ld (hl),a
 ;random road left or right
ld hl,14000 ;source of random bytes in ROM
ld d,0
ld e,c
add hl, de
ld a,(hl)
pop hl  ;retrieve road posn
dec hl  ;move road posn 1 left
and 1
jr z, roadleft
inc hl
inc hl
roadleft
ld a,l  ;check left
cp 255
jr nz, checkright
inc hl
inc hl
checkright
ld a,l
cp 21
jr nz, newroadposn
dec hl
dec hl
newroadposn
push hl  ;save road posn
xor a  ;print new road
ld (hl),a
inc hl
ld (hl),a
ld de,9
add hl,de
ld (hl),a
inc hl
ld (hl),a

The last thing we need to do to have a playable game is slow down our blindingly fast machine code.
Insert the following into line 106 (as extension material, you could adjust the figure in bc with a keypress to 'brake')

 ;wait routine
ld bc,$1fff ;max waiting time
wait
dec bc
ld a,b
or c
jr nz, wait

Save, assemble, and run - and that's it! Has your tea gone cold yet?
A full listing follows, with my email address at the end for your comments and suggestions.

main
org 33000
di
ld hl, 22537 ;initialise road
push hl  ;save road posn
xor a
ld b,24
fillscreen
ld (hl),a
inc hl
ld (hl),a
ld de,9
add hl,de
ld (hl),a
inc hl
ld (hl),a
ld de,21
add hl,de
djnz fillscreen
ld c,b  ;initialise score
push bc  ;save score
ld hl,23278 ;initialise car
ld a,8
ld (hl),a
ld (32900),hl ;save car posn
principalloop
ld hl,(32900) ;retrieve car posn
ld a,56  ;erase car
ld (hl),a
ei
ld bc,65278 ;read keyboard caps to v
in a,(c)
cp 191
jr nz, moveright
inc l
moveright
ld bc,32766 ;read keyboard space to b
in a,(c)
cp 191
jr nz, dontmove
dec l
dontmove
di
ld (32900),hl ;store car posn
ld de, 32 ;new carposn
xor a  ;set carry flag to 0
sbc hl,de
ld a,(hl) ;crash?
or a
jr z,gameover
ld a,8  ;print car
ld (hl),a
ld hl,23263 ;scroll road
ld de,23295
ld bc,736
lddr
pop bc  ;retrieve score
pop hl  ;retrieve road posn
push hl  ;save road posn
ld a,56  ;delete old road
ld (hl),a
inc hl
ld (hl),a
ld de,9
add hl,de
ld (hl),a
inc hl
ld (hl),a
 ;random road left or right
ld hl,14000 ;source of random bytes in ROM
ld d,0
ld e,c
add hl, de
ld a,(hl)
pop hl  ;retrieve road posn
dec hl  ;move road posn 1 left
and 1
jr z, roadleft
inc hl
inc hl
roadleft
ld a,l  ;check left
cp 255
jr nz, checkright
inc hl
inc hl
checkright
ld a,l
cp 21
jr nz, newroadposn
dec hl
dec hl
newroadposn
push hl  ;save road posn
xor a  ;print new road
ld (hl),a
inc hl
ld (hl),a
ld de,9
add hl,de
ld (hl),a
inc hl
ld (hl),a
inc bc  ;add 1 to score
push bc  ;save score
 ;wait routine
ld bc,$1fff ;max waiting time
wait
dec bc
ld a,b
or c
jr nz, wait
jp principalloop
gameover
pop bc  ;retrieve score
pop hl  ;empty stack
ei
ret; game and tutorial written by Jon Kingsman ('bigjon', 'bj').
 ;electronic mail tesco.net - atsign - jon.kingsman (reversed)

Offline Shockwave

  • good/evil
  • Founder Member
  • DBF Aficionado
  • ********
  • Posts: 17369
  • Karma: 497
  • evil/good
    • View Profile
    • My Homepage
Re: Z80 ASM game 30 minute tutorial
« Reply #1 on: November 06, 2009 »
I'll add this to the front page, thanks :)

K++

Any more of these great tutorials?
Shockwave ^ Codigos
Challenge Trophies Won:

Offline ferris

  • Pentium
  • *****
  • Posts: 841
  • Karma: 84
    • View Profile
    • Youth Uprising Home
Re: Z80 ASM game 30 minute tutorial
« Reply #2 on: November 08, 2009 »
K++ for all Speccy tutorials :D

Great one!
http://iamferris.com/
http://youth-uprising.com/

Where the fun's at.
Challenge Trophies Won:

Offline benny!

  • Senior Member
  • DBF Aficionado
  • ********
  • Posts: 4379
  • Karma: 228
  • in this place forever!
    • View Profile
    • bennyschuetz.com - mycroBlog
Re: Z80 ASM game 30 minute tutorial
« Reply #3 on: November 08, 2009 »
Those tutorials definately belong here. Very cool. K++
[ mycroBLOG - POUET :: whatever keeps us longing - for another breath of air - is getting rare ]

Challenge Trophies Won:

Offline RipX

  • ZX 81
  • *
  • Posts: 2
  • Karma: 0
    • View Profile
Re: Z80 ASM game 30 minute tutorial
« Reply #4 on: December 21, 2009 »
Getting errors:

I seem to get an error each time I attempt to run the program from the assembler, the message that I receive is:

"Access violation at address 0060470C in module 'ZXSpin.exe'. Write of address 05603622"

Maybe I am addressing something incorrectly? I'm usure how to load the program once the assembly has been entered into the assembler and assembled?

Any help :\

RipX

Offline Jim

  • Founder Member
  • DBF Aficionado
  • ********
  • Posts: 5301
  • Karma: 402
    • View Profile
Re: Z80 ASM game 30 minute tutorial
« Reply #5 on: December 21, 2009 »
Hi RipX, welcome to the forum,

I think it's unlikely that it's this demo causing Spin to crash.  Can you run anything else on it?

Jim
Challenge Trophies Won:

Offline bj

  • ZX 81
  • *
  • Posts: 20
  • Karma: 10
    • View Profile
Re: Z80 ASM game 30 minute tutorial
« Reply #6 on: December 21, 2009 »
I suggest using the version I did (0.666) to code the assembly language. There's been some kind of spat amongst the spin dev team, the bloke who coded the assembler is no longer involved, and version 0.7 onwards is concentrating on enhanced graphics.

Offline RipX

  • ZX 81
  • *
  • Posts: 2
  • Karma: 0
    • View Profile
Re: Z80 ASM game 30 minute tutorial
« Reply #7 on: December 21, 2009 »
Hey Jim,

Thanks for the greet, Yeah, I just tried it with v0.666 of zx spin also, I'm able to compile the data and I'm setting the start address to 33000 and keeping the Start Page as MAIN upon trying to assemble, but, as soon as I attempt to run the program using the Program->run menu in the assembler, it dies. I'm a bit stumped :\

Any more ideas?


Best regards

RipX