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

Collision & Physics Engine

What you'll learn:

  • Implement tile and sprite collision tests entirely in 6502.
  • Handle velocity and clamp logic using cycle-aware arithmetic.
  • Log collision outcomes for debugging without dropping frames.
13% Complete

Introduction

Silent games feel dead. Let’s bring Skyfall to life with sound effects!

By the end of this lesson, catching objects will make a happy “bleep” and missing them will make a sad “bloop”.

The SID Chip - Again!

We used SID voice 3 for random numbers. Now let’s use voice 1 for actual sound:

SID Voice 1 Registers:

$D400-$D401: Frequency (low/high bytes)
$D402-$D403: Pulse width (if using pulse wave)
$D404: Control register (waveform and gate)
$D405-$D406: Attack/Decay and Sustain/Release (ADSR envelope)

Understanding ADSR

ADSR controls how a sound changes over time:

  • Attack: How fast the sound reaches full volume
  • Decay: How fast it drops to sustain level
  • Sustain: The held volume level
  • Release: How fast it fades when stopped

🎵 Think of a Piano
Pressing a key quickly (fast attack), holding it (sustain), then releasing (slow release).

Sound Effect for Catching

A happy, rising bleep:

Catch Sound Effect
play_catch_sound:
    ; Set frequency for a high note (C5)
    lda #$3e
    sta $d400                  ; Voice 1 frequency low
    lda #$11
    sta $d401                  ; Voice 1 frequency high

    ; Set ADSR envelope
    lda #$09                   ; Fast attack, fast decay
    sta $d405                  ; Attack/Decay
    lda #$00                   ; No sustain, fast release
    sta $d406                  ; Sustain/Release

    ; Start the sound (triangle wave + gate)
    lda #$11                   ; Triangle waveform, gate on
    sta $d404                  ; Voice 1 control

    ; We'll turn it off later
    rts

Sound Effect for Missing

A sad, descending bloop:

Miss Sound Effect
play_miss_sound:
    ; Set frequency for a low note (C3)
    lda #$f7
    sta $d400                  ; Voice 1 frequency low
    lda #$04
    sta $d401                  ; Voice 1 frequency high

    ; Different ADSR for a "bloop" sound
    lda #$00                   ; Instant attack
    sta $d405                  ; Attack/Decay
    lda #$a8                   ; Medium sustain, slow release
    sta $d406                  ; Sustain/Release

    ; Start the sound (sawtooth wave + gate)
    lda #$21                   ; Sawtooth waveform, gate on
    sta $d404                  ; Voice 1 control

    rts

Stopping Sounds

Sounds keep playing until we turn off the gate:

stop_sound:
    lda $d404                  ; Get current control
    and #$fe                   ; Clear gate bit (bit 0)
    sta $d404                  ; Sound will now release
    rts

Sound Timer

We need to stop sounds after a short time. Add a counter:

; Add to variables
SOUND_TIMER = $0a              ; Frames until sound stops

; In game loop, add:
    lda SOUND_TIMER
    beq skip_sound_timer
    dec SOUND_TIMER
    bne skip_sound_timer
    jsr stop_sound             ; Timer hit zero, stop sound
skip_sound_timer:

Initialize SID

Set volume at startup:

init_sound:
    lda #$0f                   ; Maximum volume
    sta $d418                  ; SID volume register
    rts

Call from main:

main:
    jsr init_screen
    jsr init_random
    jsr init_sound             ; Add this
    ; etc...

Integrate with Game Events

Update handle_catch:

handle_catch:
    jsr erase_object

    lda #$00
    sta OBJ_ACTIVE,x

    inc SCORE
    jsr display_score

    jsr play_catch_sound       ; Add sound!
    lda #10                    ; Play for 10 frames
    sta SOUND_TIMER

    jsr spawn_object
    rts

Update miss handling in update_object:

    ; When object hits bottom:
    inc MISSES
    jsr display_misses

    jsr play_miss_sound        ; Add sound!
    lda #20                    ; Play for 20 frames
    sta SOUND_TIMER

    jsr spawn_object

Complete Lesson 13 Code

skyfall.asm - Complete
; SKYFALL - Lesson 13
; Sound effects

* = $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

SID_V1_FREQ_LO = $d400
SID_V1_FREQ_HI = $d401
SID_V1_CONTROL = $d404
SID_V1_AD = $d405
SID_V1_SR = $d406
SID_V3_FREQ_LO = $d40e
SID_V3_FREQ_HI = $d40f
SID_V3_CONTROL = $d412
SID_V3_OSC = $d41b
SID_VOLUME = $d418

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

MAX_OBJECTS = 3

; Variables
PLAYER_COL = $02
MOVE_COUNTER = $03
FALL_COUNTER = $07
SCORE = $08
MISSES = $09
SOUND_TIMER = $0a

; Object arrays
OBJ_COL = $10                  ; 3 bytes
OBJ_ROW = $13                  ; 3 bytes
OBJ_ACTIVE = $16               ; 3 bytes

; ===================================
; Main Program
; ===================================
main:
    jsr init_screen
    jsr init_random
    jsr init_sound

    lda #PLAYER_START_COL
    sta PLAYER_COL

    lda #3
    sta MOVE_COUNTER

    lda #5
    sta FALL_COUNTER

    lda #$00
    sta SCORE
    sta MISSES
    sta SOUND_TIMER

    jsr clear_objects
    jsr draw_player
    jsr spawn_object
    jsr display_score
    jsr display_misses

game_loop:
    jsr wait_for_raster

    ; Sound timer
    lda SOUND_TIMER
    beq skip_sound_timer
    dec SOUND_TIMER
    bne skip_sound_timer
    jsr stop_sound
skip_sound_timer:

    ; 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_all_objects
    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

init_random:
    lda #$ff
    sta SID_V3_FREQ_LO
    sta SID_V3_FREQ_HI

    lda #$80
    sta SID_V3_CONTROL
    rts

init_sound:
    lda #$0f                   ; Maximum volume
    sta SID_VOLUME
    rts

get_random:
    lda SID_V3_OSC
    rts

get_random_column:
try_again:
    jsr get_random
    and #%00111111
    cmp #40
    bcs try_again
    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

clear_objects:
    ldx #$00
clear_obj_loop:
    lda #$00
    sta OBJ_ACTIVE,x
    inx
    cpx #MAX_OBJECTS
    bne clear_obj_loop
    rts

find_free_object:
    ldx #$00
find_loop:
    lda OBJ_ACTIVE,x
    beq found_free
    inx
    cpx #MAX_OBJECTS
    bne find_loop
    ldx #$ff                   ; No free slot
found_free:
    rts

spawn_object:
    jsr find_free_object
    cpx #$ff
    beq spawn_done

    lda #$01
    sta OBJ_ACTIVE,x

    lda #$00
    sta OBJ_ROW,x

    jsr get_random_column
    sta OBJ_COL,x

    jsr draw_object

spawn_done:
    rts

update_all_objects:
    ldx #$00
update_loop:
    lda OBJ_ACTIVE,x
    beq next_object

    jsr update_object
    jsr check_collision

next_object:
    inx
    cpx #MAX_OBJECTS
    bne update_loop
    rts

update_object:
    jsr erase_object

    inc OBJ_ROW,x

    lda OBJ_ROW,x
    cmp #24
    bne still_falling

    lda #$00
    sta OBJ_ACTIVE,x

    inc MISSES
    jsr display_misses

    jsr play_miss_sound        ; Sound effect!
    lda #20
    sta SOUND_TIMER

    jsr spawn_object
    rts

still_falling:
    jsr draw_object
    rts

check_collision:
    lda OBJ_ROW,x
    cmp #PLAYER_ROW
    bne no_collision

    lda OBJ_COL,x
    cmp PLAYER_COL
    bne no_collision

    jsr handle_catch

no_collision:
    rts

handle_catch:
    jsr erase_object

    lda #$00
    sta OBJ_ACTIVE,x

    inc SCORE
    jsr display_score

    jsr play_catch_sound       ; Sound effect!
    lda #10
    sta SOUND_TIMER

    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:
    ; Save current X
    stx $fd

    ; Get row offset from table
    ldy OBJ_ROW,x
    lda row_offset_lo,y
    sta $fb
    lda row_offset_hi,y
    sta $fc

    ; Add column
    ldx $fd
    lda $fb
    clc
    adc OBJ_COL,x
    sta $fb
    bcc +
    inc $fc
+
    ; Add SCREEN_RAM base
    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

    ldx $fd
    rts

erase_object:
    ; Save current X
    stx $fd

    ; Get row offset from table
    ldy OBJ_ROW,x
    lda row_offset_lo,y
    sta $fb
    lda row_offset_hi,y
    sta $fc

    ; Add column
    ldx $fd
    lda $fb
    clc
    adc OBJ_COL,x
    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

    ldx $fd
    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

play_catch_sound:
    ; High note (C5)
    lda #$3e
    sta SID_V1_FREQ_LO
    lda #$11
    sta SID_V1_FREQ_HI

    ; Fast attack, fast decay
    lda #$09
    sta SID_V1_AD
    lda #$00
    sta SID_V1_SR

    ; Triangle wave, gate on
    lda #$11
    sta SID_V1_CONTROL
    rts

play_miss_sound:
    ; Low note (C3)
    lda #$f7
    sta SID_V1_FREQ_LO
    lda #$04
    sta SID_V1_FREQ_HI

    ; Different envelope
    lda #$00
    sta SID_V1_AD
    lda #$a8
    sta SID_V1_SR

    ; Sawtooth wave, gate on
    lda #$21
    sta SID_V1_CONTROL
    rts

stop_sound:
    lda SID_V1_CONTROL
    and #$fe                   ; Clear gate bit
    sta SID_V1_CONTROL
    rts

Build and run. Now your game has sound! Every catch makes a happy bleep, every miss a sad bloop.

New Concepts

Opcodes Used in This Lesson

No new opcodes - we’re just writing values to SID registers!

SID Waveforms

The SID can generate four waveforms:

  • $10 - Triangle (smooth, flute-like)
  • $20 - Sawtooth (bright, buzzy)
  • $40 - Pulse (variable width, hollow)
  • $80 - Noise (random, for explosions)

Add $01 to turn the gate on (start sound).

ADSR Envelope Explained

The ADSR values are packed into two registers:

$D405 (Attack/Decay):

  • High nibble (bits 4-7): Attack rate (0=fast, F=slow)
  • Low nibble (bits 0-3): Decay rate (0=fast, F=slow)

$D406 (Sustain/Release):

  • High nibble: Sustain level (0=silent, F=loud)
  • Low nibble: Release rate (0=fast, F=slow)

Why Different Sounds?

  • Catch sound: Triangle wave, fast attack/decay, no sustain = quick “ding!”
  • Miss sound: Sawtooth wave, slow release = descending “waaah”

The frequency difference (high vs low) adds to the happy/sad feeling.

Sound Design Tips

  1. Short sounds for quick events (catches)
  2. Longer sounds for important events (misses)
  3. Higher pitch = positive feedback
  4. Lower pitch = negative feedback
  5. Different waveforms = distinct sounds
🎯 Your Tasks
  1. Build and run - Enjoy the audio feedback!
  2. Change frequencies - Try different notes for catch/miss sounds
  3. Modify envelopes - Make sounds shorter/longer
  4. Try waveforms - Change triangle to pulse, sawtooth to noise

Next Steps

Sound brings the game to life! Players now get instant audio feedback for their actions.

In Lesson 14, we’ll tackle a display problem - what happens when the score goes above 9?