← Back to Courses
module
24

When things touch

Why this matters

The bullet sails through the asteroid. The rocket parks inside it. Nothing in the machine objects, because nothing in the machine knows they are things: the VDP draws rectangles of pixels where it is told, every frame, and overlap is not an error - it is Tuesday. c137 put one sprite in front of another on purpose; this is the same event wearing a different meaning.

Touching is a meaning games add. A pickup, a wall, a kill - all of them begin as the same question: do these two rectangles overlap right now? This section builds the routine that answers it, and answers it exactly.

What the chip offers, and why it is not enough

The VDP does keep watch: one bit in its status register goes up when any two sprites' drawn pixels meet anywhere on screen. One bit. It does not say which two sprites, or where - and reading it wipes it, along with flags the machine's timing depends on, under rules S25 will spell out. As a game's collision system, "somewhere, something touched" answers almost nothing: the pair that matters - bullet and enemy? player and pickup? - is exactly what it cannot name.

So games do collision themselves, from the coordinates they already own. Your objects' positions live in RAM - the sprites are just where those numbers get published - which means the question "do they overlap?" never needs the chip at all. It is arithmetic on your own variables.

Four ways to miss

Two 8-pixel boxes overlap unless something separates them. There are only four somethings: entirely left, entirely right, entirely above, entirely below. Rule out all four and they are touching - there is nowhere else to be.

A box at X occupies columns X through X+7 - eight of them, counting the first. So "A is entirely left of B" is precisely:

        A's last column  <  B's first
              AX + 7     <     BX
        LD A,D          ; asteroid X
        ADD A,7         ; its LAST column
        CP B            ; against the body's first
        JR C,miss       ; strictly left - a separating edge

Four of those - two horizontal, two vertical - and falling through them all IS the collision. No overlap arithmetic, no widths multiplied: just "is there daylight on any side?"

Why 7 and not 8? Because X+7 is the last pixel and X+8 is the fence past it - and the difference is not pedantry, it is two real bugs:

  • With +8 and a carry-only test, boxes that merely touch - last column 159 against first column 160 - fail to be rejected and count as a hit. Your rocket dies one pixel before contact, every time, and no one can tell you why the game feels cruel.
  • +8 can wrap. A byte holds 255; an X of 248 plus 8 is 0, and the compare answers a question about the wrong side of the screen. Plus 7, at every position the movement clamps allow, stays inside the byte.

The hitbox is a choice

The rocket is sixteen pixels tall and its box in this section is eight - the body. Flying the flame through the asteroid costs nothing, and that is not a compromise: exhaust is not ship. What can be hit is a design decision that happens to be expressed in geometry, and most good games lie about it in the player's favour - hitboxes a little smaller than the art, so every near miss feels earned. The picture is for the eyes; the box is for the rules. They answer to different masters.

A live verdict

The test needs somewhere to say what it found. The backdrop is ideal: one register write, visible everywhere, and gone next pass if the answer changes:

        LD A,246        ; hit: dark red backdrop
        JR tset
miss:   LD A,244        ; miss: the machine's blue
tset:   OUT (9),A
        LD A,135        ; register 7
        OUT (9),A

Note it repaints every pass, both ways. Set it only on hit and the first touch stains the screen forever - a latch, when what you want while building a game is an instrument.

The code

        ORG 256

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

        ; --- the asteroid sits still: publish it once, then the terminator
        LD A,12
        OUT (9),A
        LD A,123
        OUT (9),A
        LD A,(astY)
        OUT (8),A
        LD A,(astX)
        OUT (8),A
        LD A,4
        OUT (8),A
        LD A,3          ; light green
        OUT (8),A
        LD A,208        ; close the list at sprite 4
        OUT (8),A

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

; --- the box test: any separating edge means no collision
touch:  LD A,(rockX)
        LD B,A
        LD A,(rockY)
        LD C,A
        LD A,(astX)
        LD D,A
        LD A,(astY)
        LD E,A

        LD A,D
        ADD A,7         ; asteroid's last column
        CP B
        JR C,miss       ; entirely left of the body
        LD A,B
        ADD A,7         ; body's last column
        CP D
        JR C,miss       ; entirely left of the asteroid
        LD A,E
        ADD A,7         ; asteroid's last row
        CP C
        JR C,miss       ; entirely above the body
        LD A,C
        ADD A,7         ; body's last row
        CP E
        JR C,miss       ; entirely above the asteroid

        LD A,246        ; hit: dark red backdrop
        JR tset
miss:   LD A,244        ; miss: the machine's blue
tset:   OUT (9),A
        LD A,135
        OUT (9),A
        RET

; --- rocket, bullet, fire, animate, input: as S23
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

bullet: LD A,(bullA)
        OR A
        JR Z,bpub
        LD A,(bullY)
        SUB 3
        CP 8
        JR C,bstop
        LD (bullY),A
        JR bpub
bstop:  LD A,0
        LD (bullA),A
        LD A,224
        LD (bullY),A
bpub:   LD A,8
        OUT (9),A
        LD A,123
        OUT (9),A
        LD A,(bullY)
        OUT (8),A
        LD A,(bullX)
        OUT (8),A
        LD A,3
        OUT (8),A
        LD A,15
        OUT (8),A
        RET

fire:   LD A,(bullA)
        OR A
        RET NZ
        LD A,1
        LD (bullA),A
        LD A,(rockY)
        LD (bullY),A
        LD A,(rockX)
        LD (bullX),A
        RET

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

input:  RST 8
        DEFB 181        ; ZKSCAN
        OR A
        RET Z
        CP 32
        JR Z,fire
        CP 87
        JR Z,up
        CP 83
        JR Z,down
        CP 65
        JR Z,left
        CP 68
        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
astX:   DEFB 156
astY:   DEFB 88

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
        DEFB 00011000b
        DEFB 00011000b
        DEFB 00011000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        ; the asteroid
        DEFB 00111100b
        DEFB 01111110b
        DEFB 11111111b
        DEFB 11101111b
        DEFB 11111101b
        DEFB 11111111b
        DEFB 01111110b
        DEFB 00111100b

W, A, S, D to fly, space still fires.

What you should see

The rocket, the flame, the bullet - and a green asteroid to the right. Fly at it. The instant the body's pixels reach it the whole backdrop and border snap dark red; back off one pixel and the blue returns. Skim past above or below; drag the flame straight through it - nothing, and the nothing is correct: the flame is not in the box.

The rocket near a green asteroid, backdrop still blue The instant the rocket's body reaches the asteroid, the backdrop and border snap dark red

The bullet still flies straight through the rock. It has no box yet - that is the next thing this game grows, and by then you will know the test by heart.

Change one thing

  • Set rockX to 152 and take your hands off the keyboard. Then 153. One of these is red. Get the other one red by pressing D exactly once.
  • Change all four ADD A,7 to ADD A,8. Repeat the 152 experiment. Where does the light come on now, and what would a player call that?
  • Make the vertical test cover the whole rocket: the body's last row is ADD A,15 from rockY. Now the flame kills. Fly under the asteroid and decide which version your game wants.
  • Delete the miss: repaint - set the backdrop only on hit, like the old machines' burglar alarms. What is the difference between a latch and an instrument, and when would you want each?
  • Move the asteroid to (156, 96) - overlapping the flame's rows only. Fly the flame through it. Explain the silence.

When it goes wrong

Symptom Cause
Hits register one pixel before contact +8 with a carry-only compare: edge-touching boxes are not rejected. The last column is X+7.
Phantom hits or misses at the right or bottom edge The +8 sums wrapped past 255. +7 within the movement clamps cannot.
The light comes on and never goes off The verdict is only written on hit. Repaint both ways, every pass.
The flame hits things / the flame never hits things Neither is a bug. The hitbox is whatever you defined - decide what the object IS, then write its box.
Fine for minutes, then chaos Still S21's leak. Every CALLed routine, every path, RET.

Summary

  • Collision is your arithmetic on your variables. The chip's coincidence bit says only that something touched somewhere - games need which, so games compare coordinates.
  • Two boxes overlap unless one of four separating edges exists. Test each with last-column arithmetic: ADD A,7, compare, reject on carry.
  • +7, not +8: the last pixel, not the fence past it. +8 hits on mere adjacency and wraps at the screen's far edges.
  • The hitbox is a design choice wearing geometry. Body yes, flame no - and a kind game shaves its boxes smaller than its art.
  • Repaint verdicts every pass. A latch remembers; an instrument tells the truth now.

Next

S25. Every key at once. Try to fly diagonally and the machine shrugs: the ROM's key call hands back one byte, one key. Underneath it, the keyboard is a grid that will happily report every key you are holding - read it yourself and the shrug goes away.

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.