Deterministic Game Loop
What you'll learn:
- Build an assembly-driven update/render loop with predictable timing.
- Integrate a frame timer using KERNAL or raster checks to guard budget.
- Structure scheduler tables for tasks that run each frame.
Introduction
Objects are falling. The player can move. Now let’s make them interact.
By the end of this lesson, Skyfall will be a playable (if simple) game!
What is Collision?
A collision happens when the player and object occupy the same screen position. We need to check:
- Is the object on the player’s row? (row 23)
- Is the object on the player’s column? (same column as player)
If BOTH are true = collision = catch!
Checking for Collision
Add this to the game loop, right after updating the object:
game_loop:
jsr wait_for_raster
; Player movement (every 3 frames)
dec MOVE_COUNTER
bne skip_player_movement
lda #3
sta MOVE_COUNTER
jsr check_keys
skip_player_movement:
; Object falling (every 5 frames)
dec FALL_COUNTER
bne skip_object_fall
lda #5
sta FALL_COUNTER
jsr update_object
jsr check_collision ; Add this!
skip_object_fall:
jmp game_loop
Implement collision detection:
check_collision:
lda OBJ_ACTIVE
beq no_collision ; Skip if object not active
; Check if object is on player row
lda OBJ_ROW
cmp #PLAYER_ROW
bne no_collision ; Different row = no collision
; Check if object is on player column
lda OBJ_COL
cmp PLAYER_COL
bne no_collision ; Different column = no collision
; Both match = COLLISION!
jsr handle_catch
no_collision:
rts
handle_catch:
; Erase the object
jsr erase_object
; Deactivate it
lda #$00
sta OBJ_ACTIVE
; TODO: Add score, spawn new object
; For now, just make it disappear
rts
Build and run. Move the player to column 20 and let the object fall to you. It should disappear when it touches you!
Adding Score
Let’s track how many objects the player has caught:
; Add to variables
SCORE = $08 ; Current score (0-255)
; Initialize in main
lda #$00
sta SCORE
Update handle_catch
:
handle_catch:
jsr erase_object
lda #$00
sta OBJ_ACTIVE
; Increase score
inc SCORE
; Display score
jsr display_score
rts
Displaying the Score
Let’s show the score in the top-left corner:
display_score:
; For now, just show the number directly
; Score at row 0, column 0
lda SCORE
clc
adc #$30 ; Convert to PETSCII digit (0-9 = $30-$39)
sta SCREEN_RAM ; Top-left corner
lda #WHITE
sta COLOR_RAM
rts
Respawning Objects
When an object is caught, spawn a new one immediately:
handle_catch:
jsr erase_object
lda #$00
sta OBJ_ACTIVE
inc SCORE
jsr display_score
; Spawn new object
jsr spawn_object
rts
Now catching one object immediately spawns another at the top!
Handling Misses
What if the object reaches the bottom without being caught? That should be a “miss.” Let’s track misses:
; Add to variables
MISSES = $09 ; Missed objects (0-255)
; Initialize in main
lda #$00
sta MISSES
Update the update_object
routine:
update_object:
lda OBJ_ACTIVE
beq done_update
jsr erase_object
inc OBJ_ROW
lda OBJ_ROW
cmp #24
bne still_falling
; Hit bottom = MISS
lda #$00
sta OBJ_ACTIVE
inc MISSES ; Count the miss
jsr display_misses ; Show it
jsr spawn_object ; Spawn new object
rts
still_falling:
jsr draw_object
done_update:
rts
Display misses next to the score:
display_misses:
lda MISSES
clc
adc #$30 ; Convert to PETSCII digit
sta SCREEN_RAM + 5 ; Row 0, column 5
lda #WHITE
sta COLOR_RAM + 5
rts
Complete Lesson 10 Code
; SKYFALL - Lesson 10
; Collision detection
* = $0801
; BASIC stub: SYS 2061
!byte $0c,$08,$0a,$00,$9e
!byte $32,$30,$36,$31 ; "2061" in ASCII
!byte $00,$00,$00
* = $080d
; ===================================
; Constants
; ===================================
SCREEN_RAM = $0400
COLOR_RAM = $d800
VIC_BORDER = $d020
VIC_BACKGROUND = $d021
CIA1_PORT_A = $dc01
CIA1_PORT_B = $dc00
VIC_RASTER = $d012
BLACK = $00
WHITE = $01
YELLOW = $07
DARK_GREY = $0b
PLAYER_CHAR = $1e
PLAYER_COLOR = WHITE
PLAYER_ROW = 23
PLAYER_START_COL = 20
OBJECT_CHAR = $2a
OBJECT_COLOR = YELLOW
; Variables
PLAYER_COL = $02
MOVE_COUNTER = $03
OBJ_COL = $04
OBJ_ROW = $05
OBJ_ACTIVE = $06
FALL_COUNTER = $07
SCORE = $08
MISSES = $09
; ===================================
; Main Program
; ===================================
main:
jsr init_screen
lda #PLAYER_START_COL
sta PLAYER_COL
lda #3
sta MOVE_COUNTER
lda #5
sta FALL_COUNTER
lda #$00
sta SCORE
sta MISSES
jsr draw_player
jsr spawn_object
jsr display_score
jsr display_misses
game_loop:
jsr wait_for_raster
; Player movement (every 3 frames)
dec MOVE_COUNTER
bne skip_player_movement
lda #3
sta MOVE_COUNTER
jsr check_keys
skip_player_movement:
; Object falling (every 5 frames)
dec FALL_COUNTER
bne skip_object_fall
lda #5
sta FALL_COUNTER
jsr update_object
jsr check_collision
jsr draw_player ; Redraw player (in case object erased it)
skip_object_fall:
jmp game_loop
; ===================================
; Subroutines
; ===================================
wait_for_raster:
lda VIC_RASTER
cmp #250
bne wait_for_raster
rts
init_screen:
jsr wait_for_raster
lda #BLACK
sta VIC_BACKGROUND
lda #DARK_GREY
sta VIC_BORDER
jsr clear_screen
rts
clear_screen:
lda #$20
ldx #$00
clear_screen_loop:
sta SCREEN_RAM,x
sta SCREEN_RAM + $100,x
sta SCREEN_RAM + $200,x
sta SCREEN_RAM + $300,x
inx
bne clear_screen_loop
lda #WHITE
ldx #$00
clear_color_loop:
sta COLOR_RAM,x
sta COLOR_RAM + $100,x
sta COLOR_RAM + $200,x
sta COLOR_RAM + $300,x
inx
bne clear_color_loop
rts
check_keys:
; Check D key (column 2, row 2)
lda #%11111011
sta CIA1_PORT_B
lda CIA1_PORT_A
and #%00000100
beq move_right
; Check A key (column 1, row 2)
lda #%11111101
sta CIA1_PORT_B
lda CIA1_PORT_A
and #%00000100
beq move_left
rts
move_left:
lda PLAYER_COL
beq skip_left
jsr erase_player
dec PLAYER_COL
jsr draw_player
skip_left:
rts
move_right:
lda PLAYER_COL
cmp #39
beq skip_right
jsr erase_player
inc PLAYER_COL
jsr draw_player
skip_right:
rts
erase_player:
ldx PLAYER_COL
lda #$20
sta SCREEN_RAM + (PLAYER_ROW * 40),x
rts
draw_player:
ldx PLAYER_COL
lda #PLAYER_CHAR
sta SCREEN_RAM + (PLAYER_ROW * 40),x
lda #PLAYER_COLOR
sta COLOR_RAM + (PLAYER_ROW * 40),x
rts
spawn_object:
lda #$01
sta OBJ_ACTIVE
lda #$00
sta OBJ_ROW
lda #20
sta OBJ_COL
jsr draw_object
rts
update_object:
lda OBJ_ACTIVE
beq done_update
jsr erase_object
inc OBJ_ROW
lda OBJ_ROW
cmp #24
bne still_falling
; Hit bottom = MISS
lda #$00
sta OBJ_ACTIVE
inc MISSES
jsr display_misses
jsr spawn_object
rts
still_falling:
jsr draw_object
done_update:
rts
check_collision:
lda OBJ_ACTIVE
beq no_collision
; Check row
lda OBJ_ROW
cmp #PLAYER_ROW
bne no_collision
; Check column
lda OBJ_COL
cmp PLAYER_COL
bne no_collision
; COLLISION!
jsr handle_catch
no_collision:
rts
handle_catch:
jsr erase_object
lda #$00
sta OBJ_ACTIVE
inc SCORE
jsr display_score
jsr spawn_object
rts
; Row offset lookup table (low bytes)
row_offset_lo:
!byte <(0*40), <(1*40), <(2*40), <(3*40), <(4*40)
!byte <(5*40), <(6*40), <(7*40), <(8*40), <(9*40)
!byte <(10*40), <(11*40), <(12*40), <(13*40), <(14*40)
!byte <(15*40), <(16*40), <(17*40), <(18*40), <(19*40)
!byte <(20*40), <(21*40), <(22*40), <(23*40), <(24*40)
; Row offset lookup table (high bytes)
row_offset_hi:
!byte >(0*40), >(1*40), >(2*40), >(3*40), >(4*40)
!byte >(5*40), >(6*40), >(7*40), >(8*40), >(9*40)
!byte >(10*40), >(11*40), >(12*40), >(13*40), >(14*40)
!byte >(15*40), >(16*40), >(17*40), >(18*40), >(19*40)
!byte >(20*40), >(21*40), >(22*40), >(23*40), >(24*40)
draw_object:
; Get row offset from table
ldx OBJ_ROW
lda row_offset_lo,x
sta $fb
lda row_offset_hi,x
sta $fc
; Add column (with 16-bit carry handling)
lda $fb
clc
adc OBJ_COL
sta $fb
bcc +
inc $fc
+
; Add SCREEN_RAM base address
lda $fb
clc
adc #<SCREEN_RAM
sta $fb
lda $fc
adc #>SCREEN_RAM
sta $fc
; Draw character
ldy #0
lda #OBJECT_CHAR
sta ($fb),y
; Calculate COLOR_RAM address
lda $fb
clc
adc #<(COLOR_RAM-SCREEN_RAM)
sta $fb
lda $fc
adc #>(COLOR_RAM-SCREEN_RAM)
sta $fc
; Draw color
lda #OBJECT_COLOR
sta ($fb),y
rts
erase_object:
; Get row offset from table
ldx OBJ_ROW
lda row_offset_lo,x
sta $fb
lda row_offset_hi,x
sta $fc
; Add column
lda $fb
clc
adc OBJ_COL
sta $fb
bcc +
inc $fc
+
; Add SCREEN_RAM base
lda $fb
clc
adc #<SCREEN_RAM
sta $fb
lda $fc
adc #>SCREEN_RAM
sta $fc
; Erase with space
ldy #0
lda #$20
sta ($fb),y
rts
display_score:
lda SCORE
clc
adc #$30
sta SCREEN_RAM
lda #WHITE
sta COLOR_RAM
rts
display_misses:
lda MISSES
clc
adc #$30
sta SCREEN_RAM + 5
lda #WHITE
sta COLOR_RAM + 5
rts
Build and run. Catch objects to increase your score. Miss them and your miss count increases. New objects spawn automatically!
New Concepts
Collision Detection Philosophy
There are many ways to detect collisions:
- Perfect pixel collision - Check every pixel (overkill for simple games)
- Bounding box collision - Check if rectangles overlap (common)
- Position matching - Check if exact positions match (what we use)
Skyfall uses position matching because our “characters” are single screen positions. This is the simplest and fastest method.
Why Check After Movement?
We check collisions AFTER update_object
because:
- The object has moved to its new position
- The player might have moved too
- We check the current frame’s state
Checking before movement would miss collisions or detect false positives.
Score Display Limitation
Our score display only works for 0-9. At score 10, it will show :
(PETSCII $3A). We’ll fix this in Lesson 14 when we properly format numbers.
For now, it’s good enough to prove the game works!
Game Loop Structure
Notice the pattern:
- Wait for frame
- Update player
- Update objects
- Check collisions
- Repeat
This is the structure of every real-time game. Input → Update → Collision → Repeat.
No New Opcodes
Still using only the opcodes from previous lessons. You’re building complex behavior from simple building blocks!
- Build and run - Play Skyfall! Try to catch 5 objects
- Change spawn column - Make objects fall at different columns
- Adjust speeds - Make player or objects faster/slower
- Break it - Try to catch an object at the wrong position - verify it doesn’t work
Next Steps
Congratulations! Skyfall is now a playable game! You can catch objects, track score, and count misses.
In Lesson 11, we’ll add random spawning so objects don’t always fall in the same place!