In the previous lesson, we created projectile sprites that could move across the screen independently.
This introduced the idea of separate game objects, each with their own position and behaviour.
Now it’s time to make those objects interact with one another.
In this lesson, we’ll introduce collision detection — the method a game uses to decide when two sprites have touched.
We’ll start with a simple system that detects when the player sprite overlaps with another sprite on the screen. When that happens, the background colour will change to yellow as visual proof that a collision occurred.
This lesson lays the groundwork for more advanced interactions, such as projectiles hitting targets, enemies colliding with the player, and item pickups.
Every sprite on the Tatung Einstein has an X and Y position in video memory. These values represent the top-left corner of its visible 8×8 pixel area.
If you imagine drawing a small invisible box around each sprite, then a collision occurs when the two boxes overlap.
This method is called bounding box collision detection, and it’s widely used because it’s both simple and fast.
Instead of checking each individual pixel, the program just compares the positions of the boxes.
For two sprites — for example, the player and a barrier — a collision happens if:
Player’s right edge > Barrier’s left edge
AND
Player’s left edge < Barrier’s right edge
AND
Player’s bottom edge > Barrier’s top edge
AND
Player’s top edge < Barrier’s bottom edge
If all four of those conditions are true at the same time, the two boxes overlap.
We already store each sprite’s position in memory using simple byte variables:
PlayerX: DEFB 80
PlayerY: DEFB 80
BarrierX: DEFB 160
BarrierY: DEFB 80
These hold the screen positions for both sprites.
The update routines copy these values to the video chip (the VDP) each frame, so the visible sprites always match the positions stored in RAM.
By using these memory values for collision testing, we can perform all the game logic safely in the CPU without interfering with the VDP.
The routine called CheckCollision does four key things:
LD A,(PlayerX)
LD B,A ; B = Player X
LD A,(PlayerY)
LD C,A ; C = Player Y
LD A,(BarrierX)
LD D,A ; D = Barrier X
LD A,(BarrierY)
LD E,A ; E = Barrier Y
LD A,D ; Barrier X
ADD A,8 ; Barrier right edge
CP B ; Compare with Player X
JP C,NoCollision ; If Barrier is completely left of Player → no overlap
LD A,B ; Player X
ADD A,8 ; Player right edge
CP D ; Compare with Barrier X
JP C,NoCollision ; If Player is completely left of Barrier → no overlap
LD A,E ; Barrier Y
ADD A,8 ; Barrier bottom edge
CP C ; Compare with Player Y
JP C,NoCollision ; Barrier is above Player → no overlap
LD A,C ; Player Y
ADD A,8 ; Player bottom edge
CP E ; Compare with Barrier Y
JP C,NoCollision ; Player is above Barrier → no overlap
CollisionDetected:
CALL ChangeScreen
RET
If any of the earlier comparisons find no overlap, the routine jumps to NoCollision and simply returns without doing anything.
The subroutine ChangeScreen modifies the colours in VDP Register 7, which controls the overall screen ink and paper colours.
ChangeScreen:
LD A,11 ; yellow background, black ink
OUT (9),A
LD A,135 ; 128 + 7 selects Register 7
OUT (9),A
RET
This is a quick and easy way to show that a collision has taken place.
When the player sprite touches the barrier sprite, the background instantly turns yellow.
You can later replace this effect with anything you like — for example:
Here’s the complete working example that ties everything together.
Move the player sprite using the W, A, S, and D keys.
When the player collides with the barrier, the background colour changes to yellow.
ORG 256
; -----------------------
; Enable Graphics Mode II
; ----------------------
; Set Register 0 values
LD A,2
OUT (9),A
LD A,128
OUT (9),A
; Set Register 1 values
LD A,224
OUT (9),A
LD A,129
OUT (9),A
; ---------------------------
; Set Foreground / Background
; ---------------------------
LD A, 240 ; 11110000 (white foreground, black background)
OUT (9), A ; Send to Port 9
LD A, 135 ; 128 (bit 7 set) + 7 (register 7)
OUT (9), A ; Send to Port 9
; -----------------
; Write Blank Tiles
; -----------------
; Top Third Blank Tile
LD A, 0 ; Load zero into A register
OUT (9), A ; Send to Port 9
LD A, 64 ; Load 64 into A register
OUT (9), A ; Send to Port 9
LD A, 0 ; Load 170 into A
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
; Middle Third Blank Tile
LD A, 0 ; Load zero into A register
OUT (9), A ; Send to Port 9
LD A, 72 ; Load 72 into A register
OUT (9), A ; Send to Port 9
LD A, 0 ; Load 170 into A
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
; Bottom Third Blank Tile
LD A, 0 ; Load zero into A register
OUT (9), A ; Send to Port 9
LD A, 80 ; Load 80 into A register
OUT (9), A ; Send to Port 9
LD A, 0 ; Load 170 into A
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
OUT (8), A ; Send to Port 8
; --------------------------------
; Clear Name Table with Blank Tile
; --------------------------------
LD A, 0 ; Low Byte
OUT (9), A ; Send to Port 9
LD A, 120 ; High Byte
OUT (9), A ; Send to Port 9
LD BC, 768 ; Set Counter for table address space
ClearLoop:
LD A, 0 ; Load zero into A register
OUT (8), A ; Send to Port 8
DEC BC ; Decrement BC Counter
LD A, B ; Load B into A register
OR C ; Bitwise OR to compare
JP NZ, ClearLoop ; Jump to start unless BC is zero
; ---------------------
; Define Sprite Pattern
; ---------------------
; Player Sprite 0
LD A,0
OUT (9),A
LD A,88
OUT (9),A
LD A,%00011000 : OUT (8),A
LD A,%00111100 : OUT (8),A
LD A,%01111110 : OUT (8),A
LD A,%11111111 : OUT (8),A
LD A,%11111111 : OUT (8),A
LD A,%01111110 : OUT (8),A
LD A,%00111100 : OUT (8),A
LD A,%00011000 : OUT (8),A
; Barrier Sprite 0
LD A,8
OUT (9),A
LD A,88
OUT (9),A
LD A,%11111111 : OUT (8),A
LD A,%11111111 : OUT (8),A
LD A,%11111111 : OUT (8),A
LD A,%11111111 : OUT (8),A
LD A,%11111111 : OUT (8),A
LD A,%11111111 : OUT (8),A
LD A,%11111111 : OUT (8),A
LD A,%11111111 : OUT (8),A
MainLoop:
CALL UpdatePlayerSprite
CALL UpdateBarrierSprite
CALL CheckCollision
CALL CheckKeys
JP MainLoop
UpdatePlayerSprite:
LD A,0
OUT (9),A
LD A,123
OUT (9),A
LD A,(PlayerY)
OUT (8),A
LD A,(PlayerX)
OUT (8),A
LD A, 0
OUT (8),A
LD A,15
OUT (8),A
RET
UpdateBarrierSprite:
LD A,4
OUT (9),A
LD A,123
OUT (9),A
LD A,(BarrierY)
OUT (8),A
LD A,(BarrierX)
OUT (8),A
LD A, 1
OUT (8),A
LD A,13
OUT (8),A
RET
CheckKeys:
CALL Delay
RST 8
DEFB 181
CP 0 ; No Key Pressed
JP Z, MainLoop
; W Pressed
CP 87
JP Z, MoveUp
; A Pressed
CP 65
JP Z, MoveLeft
; S Pressed
CP 83
JP Z, MoveDown
; D Pressed
CP 68
JP Z, MoveRight
RET
MoveUp:
LD A, (PlayerY)
DEC A
LD (PlayerY), A
JP MainLoop
MoveLeft:
LD A, (PlayerX)
DEC A
LD (PlayerX), A
JP MainLoop
MoveDown:
LD A, (PlayerY)
INC A
LD (PlayerY), A
JP MainLoop
MoveRight:
LD A, (PlayerX)
INC A
LD (PlayerX), A
JP MainLoop
RET
CheckCollision:
; Load Player X and Y
LD A,(PlayerX)
LD B,A ; B = PlayerX
LD A,(PlayerY)
LD C,A ; C = PlayerY
; Load Barrier X and Y
LD A,(BarrierX)
LD D,A ; D = BarrierX
LD A,(BarrierY)
LD E,A ; E = BarrierY
; Check if PlayerX < BarrierX + 8
LD A,D
ADD A,8
CP B
JP C,NoCollision ; PlayerX >= BarrierX + 8 â no overlap (right side missed)
; Check if BarrierX < PlayerX + 8
LD A,B
ADD A,8
CP D
JP C,NoCollision ; BarrierX >= PlayerX + 8 â no overlap (left side missed)
; Check if PlayerY < BarrierY + 8
LD A,E
ADD A,8
CP C
JP C,NoCollision ; Player below Barrier
; Check if BarrierY < PlayerY + 8
LD A,C
ADD A,8
CP E
JP C,NoCollision ; Barrier above Player
; If all checks passed, they overlap
CollisionDetected:
CALL ChangeScreen ; optional feedback
RET
NoCollision:
RET
ChangeScreen:
LD A,11 ; yellow foreground, black background
OUT (9),A
LD A,135
OUT (9),A
RET
Delay:
LD BC,2000 ; adjust this number for smooth speed
DLoop:
DEC BC
LD A,B
OR C
JP NZ,DLoop
RET
PlayerX:
DEFB 80
PlayerY:
DEFB 80
BarrierX:
DEFB 160
BarrierY:
DEFB 80
JP $ ; JP Idefinitely
Try adjusting the coordinates of the barrier in memory:
BarrierX: DEFB 120
BarrierY: DEFB 120
This moves the barrier closer to the player so you can test collision sooner.
You can also increase the “size” of the collision area by changing the value added in the comparisons (for example, ADD A,16 instead of ADD A,8), which makes the boxes larger and easier to hit.
This type of collision system is simple but powerful.
It forms the basis for every type of interaction in a game — collecting items, being hit by enemies, bouncing off walls, and more.
Because it works purely with the values stored in memory, it’s completely safe for the Einstein’s VDP. You can calculate collisions, reactions, and even score updates in CPU space before updating the display.
Later, we’ll extend this same idea to detect when projectiles hit other sprites, allowing your player to shoot at targets.
In the next lesson, we’ll take this a step further by combining projectiles and collision detection — so your fired shots can interact with barriers and enemies, creating the first building block of a complete game.