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.
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
Sound Effect for Catching
A happy, rising bleep:
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:
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 - 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
- Short sounds for quick events (catches)
- Longer sounds for important events (misses)
- Higher pitch = positive feedback
- Lower pitch = negative feedback
- Different waveforms = distinct sounds
- Build and run - Enjoy the audio feedback!
- Change frequencies - Try different notes for catch/miss sounds
- Modify envelopes - Make sounds shorter/longer
- 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?