.png)
The rocket moves, and yet it is dead. Its flame is a photograph of fire. What sells motion is not position changing - it is the picture changing: legs swapping, thrust pulsing, wings beating. That is animation, and on this machine it is astonishingly cheap: the sprite's appearance is one byte in the attribute table, the pattern number, and rewriting that byte redresses the sprite completely. Several patterns plus one variable is a whole animation system.
Draw the flame twice: once long and split, once short and tight. They are two ordinary 8-byte patterns - numbers 1 and 2 - sitting in the pattern table like any other. Neither is special; neither is "the" flame. The flame the player sees is whichever number the attribute table holds right now.
So the current frame becomes a variable, exactly as the position did in S21:
flameP: DEFB 1
and place stops writing a constant and publishes the variable instead:
LD A,(flameP) ; whichever flame frame is current
OUT (8),A
That is the entire display side of animation. Position and appearance are now the same kind of thing: bytes in RAM that the loop publishes each pass.
When should the frame change? Not per keypress - a flame burns whether or not the pilot is touching anything. The loop already has a beat (it comes round once per pause), so a counter divides that beat down to an animation rate:
animate:
LD A,(flick)
DEC A
LD (flick),A
RET NZ ; not time yet
LD A,8
LD (flick),A
LD A,(flameP)
XOR 3 ; 1 becomes 2, 2 becomes 1
LD (flameP),A
RET
Seven passes out of eight, this decrements a counter and leaves. On the eighth it resets the counter and swaps the frame.
XOR is new, and it is the animator's favourite instruction. XOR flips
exactly the bits that are 1 in its operand and leaves the rest alone. Our
two frames are patterns 1 and 2 - binary 01 and 10 - and both of those bits
live in the bottom two positions, so XOR 3 (binary 11) flips both at once:
1 becomes 2, 2 becomes 1, forever, with no comparing and no branching. Two
frames numbered 0 and 1 would toggle with XOR 1 the same way.
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
main: CALL place
CALL pause
CALL input
CALL animate
JR main
; --- draw the whole rocket from its variables
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
OUT (8),A
LD A,(rockX)
OUT (8),A
LD A,(flameP) ; whichever flame frame is current
OUT (8),A
LD A,11 ; light yellow
OUT (8),A
RET
; --- the animation clock: every eighth pass, swap the flame frame
animate:
LD A,(flick)
DEC A
LD (flick),A
RET NZ ; not time yet
LD A,8
LD (flick),A
LD A,(flameP)
XOR 3 ; 1 becomes 2, 2 becomes 1
LD (flameP),A
RET
; --- one key, four directions - every path leaves by RET
input: RST 8
DEFB 181 ; ZKSCAN
OR A
RET Z
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
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
; --- the crude brake
pause: LD BC,3000
dloop: DEC BC
LD A,B
OR C
JR NZ,dloop
RET
rockX: DEFB 124
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 - long and split
DEFB 01011010b
DEFB 01111110b
DEFB 01111110b
DEFB 00111100b
DEFB 00111100b
DEFB 00011000b
DEFB 00010000b
DEFB 00001000b
; flame frame B - short and tight
DEFB 00111100b
DEFB 01111110b
DEFB 00111100b
DEFB 00111100b
DEFB 00011000b
DEFB 00000000b
DEFB 00000000b
DEFB 00000000b
Fly it with W, A, S, D, or just watch it.
What you should see
The rocket from S21 - but alive. The flame pulses between its two shapes several times a second, whether the rocket is moving or parked. Steering is unchanged.
Notice what the animation cost: 8 bytes of pattern data for the second frame, two one-byte variables, and a routine of ten instructions. The VDP is doing all the drawing; the program only changes its mind about a number.
The flame runs on the loop clock: it flickers on its own, because fire burns on its own. But tie the frame flip to movement instead - flip once per step taken - and you get the other classic feel: a character who walks when walking and stands still when standing. Same mechanism, different trigger: call the flip from the move routines rather than from a counter. Which clock an animation belongs to is a design decision, not a technical one: things that act on their own (fire, water, beacons) animate on time; things that reflect effort (legs, wheels, oars) animate on motion.
More than two frames is the same idea wearing a bigger number: keep frames
at consecutive pattern numbers, and step through them with INC and a mask -
INC A / AND 3 cycles 0,1,2,3,0 - instead of the XOR toggle.
LD A,8 in animate to 2, then to 40. What is the flame doing at
each, and which looks most like fire?XOR 3 to XOR 1. The flame's numbers are 1 and 2 - what does the
sprite become on alternate flips, and why exactly that?CALL animate inside the right: routine instead of the main
loop. What changed about when the flame burns, and what kind of thing
would that clock suit?| Symptom | Cause |
|---|---|
| The sprite alternates with a copy of the body | The toggle visits 0. XOR 1 on frames numbered 1 and 2 gives 0 and 3 - pattern 0 is the body. Number your frames to fit your toggle. |
| Animation runs only while a key is held | The flip is being called from the input path, not the main loop - that is the movement clock, fine for legs, wrong for fire. |
| The flame flickers too fast to see | The counter reset is too small, or the pause loop got shorter and took the animation clock with it - the two are coupled until S23. |
| It animates, then the machine dies minutes later | Not the animation: an exit path somewhere leaves a CALL with JR. S21's rule, S21's symptom. |
XOR n flips exactly the bits set in n: the cheapest two-frame toggle
there is. Longer cycles use INC and a mask.S23. Opening fire. The rocket moves and burns, but it cannot touch anything. A bullet is the simplest thing in a game that acts on its own - born on a keypress, moving without one, gone at the edge - and the pattern that runs it runs every enemy and pickup you will ever write.