.png)
Every program since S21 has kept time the same way: a pause loop burning a guessed number of counts. It worked, but look at what it cost. The rocket's speed is really "whatever is left over after the code runs" - add an asteroid and everything slows; trim a routine and everything lurches forward. The flame flickers at "about" some rate. Nothing in the game moves at a speed you could write down.
The screen itself keeps better time than any of this. Fifty times a second, every second, it finishes drawing a frame - and the VDP will tell you each time it happens. Lock the loop to that, and every rate in the game becomes a number: fifty pixels a second, a flip every eighth of a... this section is where the game stops guessing.
The VDP has a status register, read from port 9 - the same port the addresses go out on, but reading instead of writing. Its top bit is the one that matters today:
Bit 7 rises when the VDP finishes drawing a frame, fifty times a second - and reading the register puts it back down.
That second half is not a nuisance, it is the design: seeing the flag and
consuming it are the same act, so two reads never count one frame twice. One
IN, one frame, armed again for the next.
waitframe:
IN A,(9)
BIT 7,A
JR Z,waitframe ; not yet - the read that finally sees it clears it
RET
Three lines, and they replace the pause loop for good. Call it at the top of
the main loop and the loop runs exactly once per frame: never faster,
because the flag has not risen; never slower, as long as the pass fits
inside a frame's worth of time. The spins around waitframe are the loop
absorbing however long the rest of the pass took - fast passes wait
longer, slow passes wait less, and the tick lands on the frame either way.
That is the property no pause loop can offer: the pace belongs to the clock,
not to the code.
One more thing the machine will NOT do for you: its own interrupt - the signal that S12's HALT waits for - ticks about once per second. A wall clock, not a game clock. Frame timing on this machine is polled.
With one pass per frame, read the game's constants again and notice they have become facts:
And new rates are now designed, not tuned: want an enemy at half the rocket's speed? Move it every second frame. A one-second countdown? Fifty frames. The frame is the game's unit of time, and everything else is counting.
ORG 256
; --- three patterns: body, flame A, flame B
LD A,0
OUT (9),A
LD A,88
OUT (9),A
LD HL,rocket
LD B,24
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
; --- AY: port A output, port B input
LD A,7
OUT (2),A
LD A,127
OUT (3),A
main: CALL waitframe
CALL input
CALL animate
CALL place
JR main
; --- the heartbeat: return when the next frame begins
waitframe:
IN A,(9)
BIT 7,A
JR Z,waitframe ; not yet - the read that finally sees it clears it
RET
; --- matrix input, as S25
input: LD C,223
CALL readrow
BIT 5,A
CALL Z,up
LD C,191
CALL readrow
LD D,A
BIT 6,D
CALL Z,left
BIT 5,D
CALL Z,down
BIT 4,D
CALL Z,right
RET
readrow:
LD A,14
OUT (2),A
LD A,C
OUT (3),A
LD A,15
OUT (2),A
IN A,(2)
RET
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
OUT (8),A
LD A,15
OUT (8),A
LD A,(rockY)
ADD A,8
OUT (8),A
LD A,(rockX)
OUT (8),A
LD A,(flameP)
OUT (8),A
LD A,11
OUT (8),A
RET
animate:
LD A,(flick)
DEC A
LD (flick),A
RET NZ
LD A,8 ; every 8 frames: the flame flips at 6.25 a second
LD (flick),A
LD A,(flameP)
XOR 3
LD (flameP),A
RET
up: LD A,(rockY)
OR A
RET Z
DEC A
LD (rockY),A
RET
down: LD A,(rockY)
CP 175
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
rockX: DEFB 60
rockY: DEFB 88
flameP: DEFB 1
flick: DEFB 8
rocket: ; body
DEFB 00011000b
DEFB 00111100b
DEFB 00111100b
DEFB 01111110b
DEFB 01111110b
DEFB 11111111b
DEFB 11011011b
DEFB 10011001b
; flame frame A
DEFB 01011010b
DEFB 01111110b
DEFB 01111110b
DEFB 00111100b
DEFB 00111100b
DEFB 00011000b
DEFB 00010000b
DEFB 00001000b
; flame frame B
DEFB 00111100b
DEFB 01111110b
DEFB 00111100b
DEFB 00111100b
DEFB 00011000b
DEFB 00000000b
DEFB 00000000b
DEFB 00000000b
W, A, S, D to fly.
What you should see
The same rocket - and the difference is felt rather than seen. Movement is
perfectly even: no surge when the loop has little to do, no drag when it has
much. Hold D and the rocket crosses the screen in almost exactly five
seconds, every time. The pause routine is gone; nothing guesses.
CALL pause back IN ADDITION to waitframe, with BC = 30000.
What happens to the rocket's speed, and every how many frames does the
loop now actually run?CALL waitframe with HALT. Fly. Measure the rocket's new speed
in pixels per second by counting along with it, and say what clock the
game is now wearing.waitframe twice. What is the game's frame rate now, and what game
design would want exactly this?| Symptom | Cause |
|---|---|
| The game runs wildly fast | waitframe tests the wrong bit, or tests with the wrong sense - a poll that never waits returns instantly. Bit 7, wait for NOT zero. |
| Movement is even, but slower than the arithmetic says | Something else also reads port 9 in the loop, consuming flags this pass then waiting out the next. One reader: waitframe. |
| Speeds still wander | A pause loop survives somewhere, adding dead time after the frame lands. The frame wait replaces the pause; it does not join it. |
| Everything is glacial - one step a second | The wait was built on HALT or on the machine's interrupt. That tick is a wall clock. Poll the flag. |
S27. Giving the game a voice. Every part of a game is on screen now, and the machine has stayed silent through all of it - the same chip that reads the keyboard also makes sound, and it has not yet been asked to.