← Back to Courses
module
22

Bringing it to life

Why this matters

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.

Frames are just patterns

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.

A clock from a counter

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.

The code

        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.

Two clocks, two feels

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.

Change one thing

  • Change LD A,8 in animate to 2, then to 40. What is the flame doing at each, and which looks most like fire?
  • Change 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?
  • Move the 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?
  • Add a third flame frame at pattern 3 and try to cycle all three with XOR. When you give up, do it with INC and a compare instead - and work out why XOR could never do three.

When it goes wrong

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.

Summary

  • A sprite's appearance is one byte - the pattern number - and animation is rewriting that byte on a schedule. Frames are ordinary patterns at known numbers.
  • The current frame lives in RAM like the position does; the loop publishes both. State in variables, publication in one place.
  • A counter divides the main loop into an animation clock for things that move on their own; tying the flip to steps suits things that reflect motion.
  • XOR n flips exactly the bits set in n: the cheapest two-frame toggle there is. Longer cycles use INC and a mask.
  • The whole system cost two variables and ten instructions.

Next

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.

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.