Home / Commodore 64 / Assembly / Lesson 1
Lesson 1 of 8

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.
13% Complete

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 Addition
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:

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:

Score Display
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

⚠️ Limitation
This only works for scores 0-9. We’ll improve it in a later lesson when scores get higher.

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:

Handle Misses
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.asm - Complete
; 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:

  1. Wait for frame
  2. Update player
  3. Update objects
  4. Check collisions
  5. 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!

🎯 Your Tasks
  1. Build and run - Play Skyfall! Try to catch 5 objects
  2. Change spawn column - Make objects fall at different columns
  3. Adjust speeds - Make player or objects faster/slower
  4. 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!