← Back to Courses
module
23

Opening fire

Why this matters

Everything on screen so far has been the player. A game needs things that act on their own: a bullet that keeps going after the key is released, an enemy that patrols, a bonus that drifts past. The machinery for all of them is the same, and a bullet is its simplest case - it is born, it moves by itself, it dies at the edge, and the player's fingers have nothing to do with any of it after the first moment.

The pattern this section builds - variables, an update routine, a publish, and an active flag - is what "an object" means in a game. Learn it once with one bullet and you have learned enemies, pickups and explosions too.

An object is its variables

The bullet gets exactly what the rocket got in S21: state in RAM, and nothing else is the truth.

bullX:  DEFB 0
bullY:  DEFB 224
bullA:  DEFB 0

Two coordinates and a flag. bullA is the object's pulse: 1 means it is in flight, 0 means there is no bullet right now - the sprite still exists, the variables still exist, but the object does not. Games rarely create or destroy anything; they flip flags on things that were always there.

While there is no bullet, it parks at Y = 224. That number is doing quiet work: an 8x8 sprite is completely off screen for any Y from 192 to 247 - below the last visible line, short of the wrap that would bring it back in at the top - except 208, which is not a position at all but the list-terminator from S19, and would switch off every sprite after it. Pick parking spots away from the trap, not next to it.

Firing is copying

fire:   LD A,(bullA)
        OR A
        RET NZ          ; one is already flying
        LD A,1
        LD (bullA),A
        LD A,(rockY)
        LD (bullY),A
        LD A,(rockX)
        LD (bullX),A
        RET

Raise the flag, copy the shooter's position, done. The guard at the top is the one-bullet rule: while a bullet is in flight, space does nothing. Reload happens by flying off the screen.

input gains one line to reach it - CP 32 - because the space bar arrives from the key call as code 32, sitting in the character set right where S3's appendix put it, before the digits and letters.

The update, gated

bullet: LD A,(bullA)
        OR A
        JR Z,bpub       ; not flying - publish it parked
        LD A,(bullY)
        SUB 3           ; bullets outrun rockets
        CP 8
        JR C,bstop      ; reached the top - done
        LD (bullY),A
        JR bpub
bstop:  LD A,0
        LD (bullA),A
        LD A,224        ; park off the bottom, clear of 208
        LD (bullY),A
bpub:   ...             ; publish sprite 2 from the variables

Read the first two lines again, because they are the section. An inactive object's update does nothing but publish. Skip that gate and the parked bullet keeps moving - invisibly at first, round and round through the coordinate space, until its path crosses the screen and a phantom shot sails up it with nobody firing. The flag is not bookkeeping; it is what stops the dead from walking.

Two more choices worth their sentence:

  • The bullet moves 3 pixels a pass; the rocket moves 1. Objects have their own speeds - that is most of what makes them feel like different things. A bullet at walking pace is not a bullet.
  • The edge check happens before the store. SUB 3 / CP 8 / JR C decides on the candidate position, so a wrapped value is never published - the same wrap-avoidance the clamps did in S21, pointed the other way.

The code

        ORG 256

        ; --- four patterns: body, flame A, flame B, bullet
        LD A,0
        OUT (9),A
        LD A,88
        OUT (9),A
        LD HL,rocket
        LD B,32
pat:    LD A,(HL)
        OUT (8),A
        INC HL
        DJNZ pat

        ; --- close the sprite list after our three
        LD A,12
        OUT (9),A
        LD A,123
        OUT (9),A
        LD A,208
        OUT (8),A

main:   CALL place
        CALL pause
        CALL input
        CALL animate
        CALL bullet
        JR main

; --- draw the 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
        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

; --- the bullet: move it only while it is flying, publish it always
bullet: LD A,(bullA)
        OR A
        JR Z,bpub       ; not flying - publish it parked
        LD A,(bullY)
        SUB 3           ; bullets outrun rockets
        CP 8
        JR C,bstop      ; reached the top - done
        LD (bullY),A
        JR bpub
bstop:  LD A,0
        LD (bullA),A
        LD A,224        ; park off the bottom, clear of 208
        LD (bullY),A
bpub:   LD A,8          ; sprite 2's slot in the attribute table
        OUT (9),A
        LD A,123
        OUT (9),A
        LD A,(bullY)
        OUT (8),A
        LD A,(bullX)
        OUT (8),A
        LD A,3          ; the streak pattern
        OUT (8),A
        LD A,15
        OUT (8),A
        RET

; --- fire: one bullet at a time
fire:   LD A,(bullA)
        OR A
        RET NZ          ; one is already flying
        LD A,1
        LD (bullA),A
        LD A,(rockY)
        LD (bullY),A
        LD A,(rockX)
        LD (bullX),A
        RET

; --- the animation clock, as S22
animate:
        LD A,(flick)
        DEC A
        LD (flick),A
        RET NZ
        LD A,8
        LD (flick),A
        LD A,(flameP)
        XOR 3
        LD (flameP),A
        RET

; --- one key per poll - every path leaves by RET
input:  RST 8
        DEFB 181        ; ZKSCAN
        OR A
        RET Z
        CP 32           ; space
        JR Z,fire
        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

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
bullX:  DEFB 0
bullY:  DEFB 224
bullA:  DEFB 0

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
        ; the bullet - a two-pixel streak
        DEFB 00011000b
        DEFB 00011000b
        DEFB 00011000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b

W, A, S, D to fly, space to fire.

What you should see

The rocket, alive as in S22 - and on space, a white streak leaves it and climbs, fast, gone in under half a second off the top. Space during a flight does nothing; space after it fires again. Fly around while a bullet is up: the two move independently, because the loop updates each object from its own variables and neither knows the other exists.

A bullet streak in flight above the rocket

Change one thing

  • Delete the OR A / JR Z,bpub gate at the top of bullet and watch the screen for a while - moving nothing, pressing nothing. Explain the interval between hauntings.
  • Change the parked Y from 224 to 100. Where does the "no bullet" live now?
  • Change SUB 3 to SUB 1, fly upward, and fire while moving. What is wrong with a bullet that matches your speed?
  • Change the fire guard RET NZ to nothing - delete those two lines - and hold space. What is the flame-thrower you accidentally built, and why does it never show more than one bullet?
  • In fire, copy rockY but not rockX. Fire from a few places. What is the bullet remembering?

When it goes wrong

Symptom Cause
Phantom bullets cross the screen with nobody firing The update is not gated on the active flag: the parked object is still being moved, and its path crosses the visible screen. Inactive means update does nothing.
The bullet fires once and never again It never deactivates - the edge check is wrong or missing, the flag stays up, and the one-bullet guard blocks every later press for ever.
A bullet hangs frozen on screen Parked somewhere visible. Park in 192-247 - and not 208.
Every sprite after the bullet vanished Parked AT 208. That is the list terminator, not a position.
Fires, then the machine dies minutes later The leak again. Every CALLed routine, every path, RET - S21.

Summary

  • An object is variables + a gated update + a publish. The flag is the object's existence; everything else is furniture that was always there.
  • Firing is copying: the shooter's position into the object's variables, and the flag up. One guard makes the one-at-a-time rule.
  • An inactive object's update does nothing. The alternative is phantoms.
  • Park hidden sprites at Y 192-247, never 208. Check edges before storing, so a wrapped coordinate is never published.
  • Objects own their speeds. The bullet's 3-per-pass against the rocket's 1 is what makes it read as a bullet.
  • Space is key code 32.

Next

S24. When things touch. The bullet sails through everything, because nothing in the machine knows sprites are things. Touching is a meaning games add - four comparisons' worth - and it has to be exact to the pixel.

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.