← Back to Courses
module
21

Taking the controls

Why this matters

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.

The loop

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.

Reading a key without stopping

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.

Clamps, and where their numbers come from

The move routines do not just INC and DEC. Each checks a limit first:

  • Down stops at 175. The flame draws at the body's Y plus 8, and S19's trap is exactly here: if the body ever reached 200, the flame's byte would compute to 208 - and 208 in a Y does not place a sprite, it switches off every sprite from there on. The rocket would fly toward the bottom and lose its own flame on the way. 175 keeps the flame's last row on the screen's last line.
  • Up stops at 0. Y wraps: 255 is not "just above 0", it is flush with the top edge, and climbing further hides the sprite's rows above the screen. A game that wants a sprite to slide off the top uses that range on purpose; a game that does not, clamps.
  • Left at 0 and right at 248 for the same wrap reason, with 248 leaving the body's eight columns exactly on screen.

Limits in a game are never decoration. Each one is a hardware fact wearing a number.

The code

        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.

Change one thing

  • Change LD BC,3000 to 300, then to 30000. What exactly is being traded?
  • Delete CALL pause entirely. Now hold D. Explain what you see using the numbers you know: polls per second against pixels of screen.
  • Change the down clamp from 175 to 200 and fly to the bottom. Watch the flame as you descend, and explain the exact height at which what happens happens.
  • Change CP 87 to CP 119. Which key was that supposed to be, and why does the rocket now ignore it?
  • Change 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.

When it goes wrong

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.

Summary

  • A game is a loop: read input, update variables, redraw. Sprite positions live in RAM as the single truth; the attribute table is just where they get published each pass.
  • Every CALLed routine leaves by RET, on every path. The alternative is a stack leak that kills the program long after the demo worked.
  • Call 181 polls without waiting: zero or an uppercase key code, with BC, DE and HL destroyed. One byte, one key - no diagonals from this call.
  • Clamp positions against the hardware's real numbers: the 208 terminator, the wrap at 0 and 255, the screen's 256 x 192.
  • A counted pause loop sets the pace, crudely. The screen has a rhythm of its own, and hooking the loop to it is next.

Next

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.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.