.png)
Everything so far ran once and stopped. A game does not: it reads the controls, moves what should move, draws what changed, and does it all again, forever. That structure - the game loop - is the skeleton every game hangs off, and this section builds one around the rocket from S20.
It is also where a structural habit starts to matter more than any single instruction. A loop that runs thousands of times a second amplifies the smallest leak until it kills the program - and the failure arrives minutes after the demo looked perfect.
Three jobs, round and round:
main: CALL place ; draw the rocket where the variables say
CALL pause ; burn a little time
CALL input ; read a key, maybe change the variables
JR main
The sprite's position lives in two bytes of RAM - rockX and rockY -
and nothing else. The attribute table is just where the numbers get copied
to; the variables are the truth. input changes the variables, place
publishes them, and the two never need to know about each other.
And one rule holds it together: every routine that is CALLed leaves by
RET. Every path through it. S7 said this when subroutines were new; here is
what it protects you from. Each CALL pushes a return address; only RET
pops one. A routine that exits with JR main instead skips the pop, and the
loop pushes another address next pass, and another - two bytes of stack per
loop, forever. Nothing looks wrong on screen. The stack quietly eats its way
down through memory until it reaches something that matters, and the program
dies minutes after it started, in a part of the code that was never at
fault. If a routine wants the loop to continue, it returns to the loop and
lets the loop continue itself.
S8's call 181 is built for this: it comes back immediately, carrying the
key's code in A, or zero if nothing is pressed. Poll it once per loop.
Two things from S8 worth re-saying, because getting either wrong produces a
rocket that ignores you: letters arrive uppercase whatever is typed - 87
is W, and CP 119 never matches - and the call destroys BC, DE and HL,
so nothing the loop cares about can live in registers across it. It also
cannot tell you about two keys at once - one byte, one key - which is why
diagonals wait for a later section on reading the keyboard properly.
The move routines do not just INC and DEC. Each checks a limit first:
Limits in a game are never decoration. Each one is a hardware fact wearing a number.
ORG 256
; --- the rocket's two patterns
LD A,0
OUT (9),A
LD A,88
OUT (9),A
LD HL,rocket
LD B,16
pat: LD A,(HL)
OUT (8),A
INC HL
DJNZ pat
; --- close the sprite list after our two
LD A,8
OUT (9),A
LD A,123
OUT (9),A
LD A,208
OUT (8),A
main: CALL place
CALL pause
CALL input
JR main
; --- draw the whole rocket from (rockX, rockY)
place: LD A,0
OUT (9),A
LD A,123
OUT (9),A
LD A,(rockY)
OUT (8),A
LD A,(rockX)
OUT (8),A
LD A,0 ; body pattern
OUT (8),A
LD A,15 ; white
OUT (8),A
LD A,(rockY)
ADD A,8 ; the flame sits under the body
OUT (8),A
LD A,(rockX)
OUT (8),A
LD A,1 ; flame pattern
OUT (8),A
LD A,11 ; light yellow
OUT (8),A
RET
; --- one key, four directions - and every path leaves by RET
input: RST 8
DEFB 181 ; ZKSCAN
OR A
RET Z ; nothing pressed
CP 87 ; W
JR Z,up
CP 83 ; S
JR Z,down
CP 65 ; A
JR Z,left
CP 68 ; D
JR Z,right
RET ; some other key
up: LD A,(rockY)
OR A
RET Z ; already at the top
DEC A
LD (rockY),A
RET
down: LD A,(rockY)
CP 175 ; the flame's last safe row
RET Z
INC A
LD (rockY),A
RET
left: LD A,(rockX)
OR A
RET Z
DEC A
LD (rockX),A
RET
right: LD A,(rockX)
CP 248
RET Z
INC A
LD (rockX),A
RET
; --- the crude brake: burn time so steps are visible
pause: LD BC,3000
dloop: DEC BC
LD A,B
OR C
JR NZ,dloop
RET
rockX: DEFB 124
rockY: DEFB 88
rocket: ; body
DEFB 00011000b
DEFB 00111100b
DEFB 00111100b
DEFB 01111110b
DEFB 01111110b
DEFB 11111111b
DEFB 11011011b
DEFB 10011001b
; flame
DEFB 01011010b
DEFB 01111110b
DEFB 01111110b
DEFB 00111100b
DEFB 00111100b
DEFB 00011000b
DEFB 00010000b
DEFB 00001000b
Type W, A, S, D to fly it.
What you should see
The rocket from S20, white over yellow, near the middle of the screen - and under your control. Each press steps it; hold a key and the steps accumulate. It stops at every edge, and at the bottom it stops with the flame still showing - that is the 175 doing its job.
The pace is the pause loop. BC = 3000 burns about twenty milliseconds
a pass; the rocket steps as fast as the loop comes round. There is no
connection to the screen's own rhythm yet - this brake is honest but crude,
and making movement smooth rather than merely visible is the next
section's whole subject.
LD BC,3000 to 300, then to 30000. What exactly is being traded?CALL pause entirely. Now hold D. Explain what you see using the
numbers you know: polls per second against pixels of screen.CP 87 to CP 119. Which key was that supposed to be, and why
does the rocket now ignore it?input's RET Z - the nothing-pressed exit - to JP Z,main. It
still flies perfectly. Leave it running, hands off, for a few minutes, and
then explain what you find to yourself slowly.| Symptom | Cause |
|---|---|
| Flies for a while, then the machine goes strange or resets | A CALLed routine leaves with JR instead of RET. Two bytes of stack leak per loop; the crash arrives long after the cause. Check every exit path. |
| The rocket ignores its keys | Lowercase comparisons. Letters arrive as 87, 65, 83, 68 - uppercase - whatever you type. |
| Ignores keys, but only sometimes | The loop keeps something in BC, DE or HL across the key call, which destroys all three. |
| The flame vanishes near the bottom of the screen | The body's Y reached 200, so the flame's computed Y hit 208 - the list terminator. Clamp lower. |
| Movement is instant teleporting | No pause in the loop. The Z80 polls thousands of times a second. |
S22. Bringing it to life. The rocket moves, but its flame is a photograph of fire. A sprite's whole appearance is one byte in the attribute table - and rewriting that byte on a schedule is animation.