Skip to content
Game 1 Unit 5 of 64 1 hr learning time

Hit Detection

Detect when keypresses match notes in the hit zone. The core rhythm game mechanic.

8% of SID Symphony

Now it’s a real game. Press the right key when a note reaches the hit zone.

Previously, pressing Z/X/C always played sounds regardless of timing. This unit adds the core rhythm game mechanic: hit detection. Press the key when a note is in the hit zone - you hear the sound and the note disappears. Miss the timing - nothing happens.

Run It

Assemble and run:

acme -f cbm -o symphony.prg symphony.asm

Unit 5 Screenshot

Watch notes scroll toward the hit zone. Press Z when a note on track Z reaches the yellow bar. If timed correctly, the note vanishes and the SID plays. Press too early or late - nothing.

Hit detection — notes must be in the hit zone when you press the key

The Hit Zone

Notes must be within a range of columns to count as “hittable”:

; Hit zone boundaries (column positions)
HIT_ZONE_MIN = 2                ; Left edge of hit zone
HIT_ZONE_MAX = 5                ; Right edge of hit zone

The hit zone spans columns 2-5. A note at column 2 is about to leave the zone. A note at column 5 is just entering. This gives the player a window of 4 columns to hit each note.

How Hit Detection Works

When a key is pressed, the game searches through all active notes looking for one that:

  1. Is on the correct track (Z key = track 1, X = track 2, C = track 3)
  2. Has an X position within the hit zone
; ----------------------------------------------------------------------------
; Check Hit - Find a note in the hit zone for the pressed track
; ----------------------------------------------------------------------------
; Input: key_pressed = track number (1-3)
; Output: Carry set if hit found, X = note index
;         Carry clear if no hit
; Side effect: Removes hit note from play

check_hit:
            ldx #0

check_hit_loop:
            ; Check if this note slot is active
            lda note_track,x
            beq check_hit_next  ; Empty slot - skip

            ; Check if note is on the pressed track
            cmp key_pressed
            bne check_hit_next  ; Wrong track - skip

            ; Check if note is in the hit zone
            lda note_col,x
            cmp #HIT_ZONE_MIN
            bcc check_hit_next  ; Too far left - skip
            cmp #HIT_ZONE_MAX+1
            bcs check_hit_next  ; Too far right - skip

            ; HIT! Note is in zone on correct track
            ; Erase the note from screen
            jsr erase_note

            ; Deactivate the note
            lda #0
            sta note_track,x

            ; Return with carry set (hit found)
            sec
            rts

check_hit_next:
            inx
            cpx #MAX_NOTES
            bne check_hit_loop

            ; No hit found - return with carry clear
            clc
            rts

The routine uses the carry flag to signal success or failure:

  • Carry set (SEC) - Hit found, note removed
  • Carry clear (CLC) - No hit, nothing changed

This is a common 6502 pattern. The branch instructions BCS (branch if carry set) and BCC (branch if carry clear) let the caller respond to the result.

Updating Key Handling

The key checking code now calls check_hit and only plays sound on success:

; ----------------------------------------------------------------------------
; Check Keys - Now with hit detection
; ----------------------------------------------------------------------------
; Checks if a key is pressed and if there's a matching note in the hit zone.
; Only plays sound and removes note on a successful hit.

check_keys:
            ; Check Z key (track 1)
            lda #$FD
            sta CIA1_PRA
            lda CIA1_PRB
            and #$10
            bne check_x_key

            ; Z pressed - check for hit on track 1
            lda #1
            sta key_pressed
            jsr check_hit
            bcc check_x_key     ; No hit - don't play sound
            jsr play_voice1
            jsr flash_track1_hit

check_x_key:
            ; Check X key (track 2)
            lda #$FB
            sta CIA1_PRA
            lda CIA1_PRB
            and #$80
            bne check_c_key

            ; X pressed - check for hit on track 2
            lda #2
            sta key_pressed
            jsr check_hit
            bcc check_c_key     ; No hit - don't play sound
            jsr play_voice2
            jsr flash_track2_hit

check_c_key:
            ; Check C key (track 3)
            lda #$FB
            sta CIA1_PRA
            lda CIA1_PRB
            and #$10
            bne check_keys_done

            ; C pressed - check for hit on track 3
            lda #3
            sta key_pressed
            jsr check_hit
            bcc check_keys_done ; No hit - don't play sound
            jsr play_voice3
            jsr flash_track3_hit

check_keys_done:
            lda #$FF
            sta CIA1_PRA
            rts

Compare this to Unit 4 where every keypress triggered a sound. Now BCC skips the sound if no hit was found.

The Complete Code

; ============================================================================
; SID SYMPHONY - Unit 5: Hit Detection
; ============================================================================
; Detect when keypresses match notes in the hit zone. Press the right key at
; the right time to hit notes and hear the SID play. Wrong timing does nothing.
;
; Controls: Z = Track 1 (high), X = Track 2 (mid), C = Track 3 (low)
; ============================================================================

; ============================================================================
; CUSTOMISATION SECTION
; ============================================================================

; SID Voice Settings
VOICE1_WAVE = $21               ; Sawtooth
VOICE2_WAVE = $41               ; Pulse
VOICE3_WAVE = $11               ; Triangle

VOICE1_FREQ = $1C               ; High pitch
VOICE2_FREQ = $0E               ; Mid pitch
VOICE3_FREQ = $07               ; Low pitch

VOICE_AD    = $09               ; Attack=0, Decay=9
VOICE_SR    = $00               ; Sustain=0, Release=0
PULSE_WIDTH = $08               ; 50% duty cycle

; Visual Settings
BORDER_COL  = 0                 ; Black border
BG_COL      = 0                 ; Black background

TRACK1_NOTE_COL = 10            ; Light red
TRACK2_NOTE_COL = 13            ; Light green
TRACK3_NOTE_COL = 14            ; Light blue

TRACK_LINE_COL = 11             ; Dark grey
HIT_ZONE_COL = 7                ; Yellow

FLASH1_COL  = 2                 ; Red
FLASH2_COL  = 5                 ; Green
FLASH3_COL  = 6                 ; Blue

HIT_COL     = 1                 ; White - flash on successful hit

; ============================================================================
; HIT DETECTION SETTINGS
; ============================================================================

; Hit zone boundaries (column positions)
HIT_ZONE_MIN = 2                ; Left edge of hit zone
HIT_ZONE_MAX = 5                ; Right edge of hit zone
HIT_ZONE_CENTRE = 3             ; Perfect hit position

; ============================================================================
; MEMORY MAP
; ============================================================================

SCREEN      = $0400             ; Screen memory
COLRAM      = $D800             ; Colour RAM
BORDER      = $D020             ; Border colour
BGCOL       = $D021             ; Background colour
CHARPTR     = $D018             ; Character memory pointer

CHARSET     = $3000             ; Custom character set location

; SID registers
SID         = $D400
SID_V1_FREQ_LO = $D400
SID_V1_FREQ_HI = $D401
SID_V1_PWHI = $D403
SID_V1_CTRL = $D404
SID_V1_AD   = $D405
SID_V1_SR   = $D406

SID_V2_FREQ_LO = $D407
SID_V2_FREQ_HI = $D408
SID_V2_PWHI = $D40A
SID_V2_CTRL = $D40B
SID_V2_AD   = $D40C
SID_V2_SR   = $D40D

SID_V3_FREQ_LO = $D40E
SID_V3_FREQ_HI = $D40F
SID_V3_PWHI = $D411
SID_V3_CTRL = $D412
SID_V3_AD   = $D413
SID_V3_SR   = $D414

SID_VOLUME  = $D418

; CIA keyboard
CIA1_PRA    = $DC00
CIA1_PRB    = $DC01

; Track positions
TRACK1_ROW  = 8
TRACK2_ROW  = 12
TRACK3_ROW  = 16

; Hit zone
HIT_ZONE_COLUMN = 3

; Custom character codes
CHAR_NOTE   = 128               ; Filled arrow/circle for notes
CHAR_TRACK  = 129               ; Thin horizontal line for tracks
CHAR_HITZONE = 130              ; Vertical bar for hit zone
CHAR_SPACE  = 32                ; Space

; Note settings
MAX_NOTES   = 8
NOTE_SPAWN_COL = 37

; Timing
FRAMES_PER_BEAT = 25

; Zero page
ZP_PTR      = $FB
ZP_PTR_HI   = $FC

; Variables
frame_count = $02
beat_count  = $03
song_pos    = $04
song_pos_hi = $05
temp_track  = $06
key_pressed = $07               ; Which track key was pressed (0=none)

; ----------------------------------------------------------------------------
; BASIC Stub
; ----------------------------------------------------------------------------

            * = $0801

            !byte $0C, $08
            !byte $0A, $00
            !byte $9E
            !text "2064"
            !byte $00
            !byte $00, $00

; ----------------------------------------------------------------------------
; Main Program
; ----------------------------------------------------------------------------

            * = $0810

start:
            jsr copy_charset    ; Copy ROM charset and add custom chars
            jsr init_screen
            jsr init_sid
            jsr init_notes

            lda #<song_data
            sta song_pos
            lda #>song_data
            sta song_pos_hi

            lda #0
            sta frame_count
            sta beat_count

main_loop:
            lda #$FF
wait_raster:
            cmp $D012
            bne wait_raster

            inc frame_count
            lda frame_count
            cmp #FRAMES_PER_BEAT
            bcc no_new_beat

            lda #0
            sta frame_count
            jsr check_spawn_note
            inc beat_count

no_new_beat:
            jsr update_notes
            jsr reset_track_colours
            jsr check_keys

            jmp main_loop

; ----------------------------------------------------------------------------
; Copy Character Set from ROM to RAM
; ----------------------------------------------------------------------------

copy_charset:
            sei

            lda $01
            pha
            and #$FB
            sta $01

            ldx #0
copy_loop:
            lda $D000,x
            sta CHARSET,x
            lda $D100,x
            sta CHARSET+$100,x
            lda $D200,x
            sta CHARSET+$200,x
            lda $D300,x
            sta CHARSET+$300,x
            lda $D400,x
            sta CHARSET+$400,x
            lda $D500,x
            sta CHARSET+$500,x
            lda $D600,x
            sta CHARSET+$600,x
            lda $D700,x
            sta CHARSET+$700,x
            inx
            bne copy_loop

            pla
            sta $01

            cli

            jsr define_custom_chars

            lda #$1C
            sta CHARPTR

            rts

; ----------------------------------------------------------------------------
; Define Custom Characters
; ----------------------------------------------------------------------------

define_custom_chars:
            ; Character 128: Note (filled chevron pointing left)
            lda #%00000110
            sta CHARSET + (CHAR_NOTE * 8) + 0
            lda #%00011110
            sta CHARSET + (CHAR_NOTE * 8) + 1
            lda #%01111110
            sta CHARSET + (CHAR_NOTE * 8) + 2
            lda #%11111110
            sta CHARSET + (CHAR_NOTE * 8) + 3
            lda #%11111110
            sta CHARSET + (CHAR_NOTE * 8) + 4
            lda #%01111110
            sta CHARSET + (CHAR_NOTE * 8) + 5
            lda #%00011110
            sta CHARSET + (CHAR_NOTE * 8) + 6
            lda #%00000110
            sta CHARSET + (CHAR_NOTE * 8) + 7

            ; Character 129: Track line (centered horizontal line)
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 0
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 1
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 2
            lda #%11111111
            sta CHARSET + (CHAR_TRACK * 8) + 3
            lda #%11111111
            sta CHARSET + (CHAR_TRACK * 8) + 4
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 5
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 6
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 7

            ; Character 130: Hit zone (vertical bars)
            lda #%01100110
            sta CHARSET + (CHAR_HITZONE * 8) + 0
            lda #%01100110
            sta CHARSET + (CHAR_HITZONE * 8) + 1
            lda #%01100110
            sta CHARSET + (CHAR_HITZONE * 8) + 2
            lda #%01100110
            sta CHARSET + (CHAR_HITZONE * 8) + 3
            lda #%01100110
            sta CHARSET + (CHAR_HITZONE * 8) + 4
            lda #%01100110
            sta CHARSET + (CHAR_HITZONE * 8) + 5
            lda #%01100110
            sta CHARSET + (CHAR_HITZONE * 8) + 6
            lda #%01100110
            sta CHARSET + (CHAR_HITZONE * 8) + 7

            rts

; ----------------------------------------------------------------------------
; Initialize Notes
; ----------------------------------------------------------------------------

init_notes:
            ldx #MAX_NOTES-1
            lda #0
init_notes_loop:
            sta note_track,x
            sta note_col,x
            dex
            bpl init_notes_loop
            rts

; ----------------------------------------------------------------------------
; Check Spawn Note
; ----------------------------------------------------------------------------

check_spawn_note:
            ldy #0

spawn_check_loop:
            lda (song_pos),y
            cmp #$FF
            beq spawn_restart_song

            cmp beat_count
            beq spawn_match
            bcs spawn_done

            jmp spawn_advance

spawn_match:
            iny
            lda (song_pos),y
            jsr spawn_note
            dey

spawn_advance:
            lda song_pos
            clc
            adc #2
            sta song_pos
            lda song_pos_hi
            adc #0
            sta song_pos_hi
            jmp spawn_check_loop

spawn_done:
            rts

spawn_restart_song:
            lda #<song_data
            sta song_pos
            lda #>song_data
            sta song_pos_hi
            lda #0
            sta beat_count
            rts

; ----------------------------------------------------------------------------
; Spawn Note
; ----------------------------------------------------------------------------

spawn_note:
            sta temp_track

            ldx #0
spawn_find_slot:
            lda note_track,x
            beq spawn_found_slot
            inx
            cpx #MAX_NOTES
            bne spawn_find_slot
            rts

spawn_found_slot:
            lda temp_track
            sta note_track,x
            lda #NOTE_SPAWN_COL
            sta note_col,x
            jsr draw_note
            rts

; ----------------------------------------------------------------------------
; Update Notes
; ----------------------------------------------------------------------------

update_notes:
            ldx #0

update_loop:
            lda note_track,x
            beq update_next

            jsr erase_note

            dec note_col,x
            lda note_col,x
            cmp #1
            bcc update_deactivate

            jsr draw_note
            jmp update_next

update_deactivate:
            lda #0
            sta note_track,x

update_next:
            inx
            cpx #MAX_NOTES
            bne update_loop
            rts

; ----------------------------------------------------------------------------
; Draw Note
; ----------------------------------------------------------------------------

draw_note:
            lda note_track,x
            cmp #1
            beq draw_note_t1
            cmp #2
            beq draw_note_t2
            cmp #3
            beq draw_note_t3
            rts

draw_note_t1:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK1_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK1_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #CHAR_NOTE
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK1_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK1_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #TRACK1_NOTE_COL
            sta (ZP_PTR),y
            rts

draw_note_t2:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK2_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK2_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #CHAR_NOTE
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK2_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK2_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #TRACK2_NOTE_COL
            sta (ZP_PTR),y
            rts

draw_note_t3:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK3_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK3_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #CHAR_NOTE
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK3_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK3_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #TRACK3_NOTE_COL
            sta (ZP_PTR),y
            rts

; ----------------------------------------------------------------------------
; Erase Note - Restores track line character
; ----------------------------------------------------------------------------

erase_note:
            lda note_track,x
            cmp #1
            beq erase_note_t1
            cmp #2
            beq erase_note_t2
            cmp #3
            beq erase_note_t3
            rts

erase_note_t1:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK1_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK1_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #CHAR_TRACK
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK1_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK1_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #TRACK_LINE_COL
            sta (ZP_PTR),y
            rts

erase_note_t2:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK2_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK2_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #CHAR_TRACK
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK2_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK2_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #TRACK_LINE_COL
            sta (ZP_PTR),y
            rts

erase_note_t3:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK3_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK3_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #CHAR_TRACK
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK3_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK3_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #TRACK_LINE_COL
            sta (ZP_PTR),y
            rts

; ----------------------------------------------------------------------------
; Initialize Screen
; ----------------------------------------------------------------------------

init_screen:
            lda #BORDER_COL
            sta BORDER
            lda #BG_COL
            sta BGCOL

            ldx #0
            lda #CHAR_SPACE
clr_screen:
            sta SCREEN,x
            sta SCREEN+$100,x
            sta SCREEN+$200,x
            sta SCREEN+$2E8,x
            inx
            bne clr_screen

            ldx #0
            lda #TRACK_LINE_COL
clr_colour:
            sta COLRAM,x
            sta COLRAM+$100,x
            sta COLRAM+$200,x
            sta COLRAM+$2E8,x
            inx
            bne clr_colour

            jsr draw_tracks
            jsr draw_hit_zones
            jsr draw_labels

            rts

; ----------------------------------------------------------------------------
; Draw Tracks
; ----------------------------------------------------------------------------

draw_tracks:
            lda #CHAR_TRACK
            ldx #0
draw_t1:
            sta SCREEN + (TRACK1_ROW * 40),x
            inx
            cpx #38
            bne draw_t1

            lda #CHAR_TRACK
            ldx #0
draw_t2:
            sta SCREEN + (TRACK2_ROW * 40),x
            inx
            cpx #38
            bne draw_t2

            lda #CHAR_TRACK
            ldx #0
draw_t3:
            sta SCREEN + (TRACK3_ROW * 40),x
            inx
            cpx #38
            bne draw_t3

            rts

; ----------------------------------------------------------------------------
; Draw Hit Zones
; ----------------------------------------------------------------------------

draw_hit_zones:
            lda #CHAR_HITZONE

            sta SCREEN + ((TRACK1_ROW-2) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK1_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + (TRACK1_ROW * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK1_ROW+1) * 40) + HIT_ZONE_COLUMN

            sta SCREEN + ((TRACK2_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + (TRACK2_ROW * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK2_ROW+1) * 40) + HIT_ZONE_COLUMN

            sta SCREEN + ((TRACK3_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + (TRACK3_ROW * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK3_ROW+1) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK3_ROW+2) * 40) + HIT_ZONE_COLUMN

            lda #HIT_ZONE_COL
            sta COLRAM + ((TRACK1_ROW-2) * 40) + HIT_ZONE_COLUMN
            sta COLRAM + ((TRACK1_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK1_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + ((TRACK1_ROW+1) * 40) + HIT_ZONE_COLUMN

            sta COLRAM + ((TRACK2_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK2_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + ((TRACK2_ROW+1) * 40) + HIT_ZONE_COLUMN

            sta COLRAM + ((TRACK3_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK3_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + ((TRACK3_ROW+1) * 40) + HIT_ZONE_COLUMN
            sta COLRAM + ((TRACK3_ROW+2) * 40) + HIT_ZONE_COLUMN

            rts

; ----------------------------------------------------------------------------
; Draw Labels
; ----------------------------------------------------------------------------

draw_labels:
            ldx #0
draw_title:
            lda title_text,x
            beq draw_title_done
            sta SCREEN + 13,x
            lda #1
            sta COLRAM + 13,x
            inx
            bne draw_title
draw_title_done:

            lda #$1A            ; Z
            sta SCREEN + (TRACK1_ROW * 40)
            lda #TRACK1_NOTE_COL
            sta COLRAM + (TRACK1_ROW * 40)

            lda #$18            ; X
            sta SCREEN + (TRACK2_ROW * 40)
            lda #TRACK2_NOTE_COL
            sta COLRAM + (TRACK2_ROW * 40)

            lda #$03            ; C
            sta SCREEN + (TRACK3_ROW * 40)
            lda #TRACK3_NOTE_COL
            sta COLRAM + (TRACK3_ROW * 40)

            ldx #0
draw_instr:
            lda instr_text,x
            beq draw_instr_done
            sta SCREEN + (23 * 40) + 6,x
            lda #TRACK_LINE_COL
            sta COLRAM + (23 * 40) + 6,x
            inx
            bne draw_instr
draw_instr_done:

            rts

title_text:
            !scr "sid symphony"
            !byte 0

instr_text:
            !scr "hit detection active"
            !byte 0

; ----------------------------------------------------------------------------
; Initialize SID
; ----------------------------------------------------------------------------

init_sid:
            ldx #$18
            lda #0
clear_sid:
            sta SID,x
            dex
            bpl clear_sid

            lda #$0F
            sta SID_VOLUME

            lda #$00
            sta SID_V1_FREQ_LO
            lda #VOICE1_FREQ
            sta SID_V1_FREQ_HI
            lda #PULSE_WIDTH
            sta SID_V1_PWHI
            lda #VOICE_AD
            sta SID_V1_AD
            lda #VOICE_SR
            sta SID_V1_SR

            lda #$00
            sta SID_V2_FREQ_LO
            lda #VOICE2_FREQ
            sta SID_V2_FREQ_HI
            lda #PULSE_WIDTH
            sta SID_V2_PWHI
            lda #VOICE_AD
            sta SID_V2_AD
            lda #VOICE_SR
            sta SID_V2_SR

            lda #$00
            sta SID_V3_FREQ_LO
            lda #VOICE3_FREQ
            sta SID_V3_FREQ_HI
            lda #PULSE_WIDTH
            sta SID_V3_PWHI
            lda #VOICE_AD
            sta SID_V3_AD
            lda #VOICE_SR
            sta SID_V3_SR

            rts

; ----------------------------------------------------------------------------
; Reset Track Colours
; ----------------------------------------------------------------------------

reset_track_colours:
            ldx #0
            lda #TRACK_LINE_COL
reset_t1:
            sta COLRAM + (TRACK1_ROW * 40),x
            inx
            cpx #38
            bne reset_t1

            ldx #0
reset_t2:
            sta COLRAM + (TRACK2_ROW * 40),x
            inx
            cpx #38
            bne reset_t2

            ldx #0
reset_t3:
            sta COLRAM + (TRACK3_ROW * 40),x
            inx
            cpx #38
            bne reset_t3

            lda #TRACK1_NOTE_COL
            sta COLRAM + (TRACK1_ROW * 40)
            lda #TRACK2_NOTE_COL
            sta COLRAM + (TRACK2_ROW * 40)
            lda #TRACK3_NOTE_COL
            sta COLRAM + (TRACK3_ROW * 40)

            lda #HIT_ZONE_COL
            sta COLRAM + (TRACK1_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK2_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK3_ROW * 40) + HIT_ZONE_COLUMN

            jsr redraw_all_notes

            rts

; ----------------------------------------------------------------------------
; Redraw All Notes
; ----------------------------------------------------------------------------

redraw_all_notes:
            ldx #0
redraw_loop:
            lda note_track,x
            beq redraw_next
            jsr draw_note
redraw_next:
            inx
            cpx #MAX_NOTES
            bne redraw_loop
            rts

; ----------------------------------------------------------------------------
; Check Keys - Now with hit detection
; ----------------------------------------------------------------------------
; Checks if a key is pressed and if there's a matching note in the hit zone.
; Only plays sound and removes note on a successful hit.

check_keys:
            ; Check Z key (track 1)
            lda #$FD
            sta CIA1_PRA
            lda CIA1_PRB
            and #$10
            bne check_x_key

            ; Z pressed - check for hit on track 1
            lda #1
            sta key_pressed
            jsr check_hit
            bcc check_x_key     ; No hit - don't play sound
            jsr play_voice1
            jsr flash_track1_hit

check_x_key:
            ; Check X key (track 2)
            lda #$FB
            sta CIA1_PRA
            lda CIA1_PRB
            and #$80
            bne check_c_key

            ; X pressed - check for hit on track 2
            lda #2
            sta key_pressed
            jsr check_hit
            bcc check_c_key     ; No hit - don't play sound
            jsr play_voice2
            jsr flash_track2_hit

check_c_key:
            ; Check C key (track 3)
            lda #$FB
            sta CIA1_PRA
            lda CIA1_PRB
            and #$10
            bne check_keys_done

            ; C pressed - check for hit on track 3
            lda #3
            sta key_pressed
            jsr check_hit
            bcc check_keys_done ; No hit - don't play sound
            jsr play_voice3
            jsr flash_track3_hit

check_keys_done:
            lda #$FF
            sta CIA1_PRA
            rts

; ----------------------------------------------------------------------------
; Check Hit - Find a note in the hit zone for the pressed track
; ----------------------------------------------------------------------------
; Input: key_pressed = track number (1-3)
; Output: Carry set if hit found, X = note index
;         Carry clear if no hit
; Side effect: Removes hit note from play

check_hit:
            ldx #0

check_hit_loop:
            ; Check if this note slot is active
            lda note_track,x
            beq check_hit_next  ; Empty slot - skip

            ; Check if note is on the pressed track
            cmp key_pressed
            bne check_hit_next  ; Wrong track - skip

            ; Check if note is in the hit zone
            lda note_col,x
            cmp #HIT_ZONE_MIN
            bcc check_hit_next  ; Too far left - skip
            cmp #HIT_ZONE_MAX+1
            bcs check_hit_next  ; Too far right - skip

            ; HIT! Note is in zone on correct track
            ; Erase the note from screen
            jsr erase_note

            ; Deactivate the note
            lda #0
            sta note_track,x

            ; Return with carry set (hit found)
            sec
            rts

check_hit_next:
            inx
            cpx #MAX_NOTES
            bne check_hit_loop

            ; No hit found - return with carry clear
            clc
            rts

; ----------------------------------------------------------------------------
; Play Voices
; ----------------------------------------------------------------------------

play_voice1:
            lda #VOICE1_WAVE
            ora #$01
            sta SID_V1_CTRL
            rts

play_voice2:
            lda #VOICE2_WAVE
            ora #$01
            sta SID_V2_CTRL
            rts

play_voice3:
            lda #VOICE3_WAVE
            ora #$01
            sta SID_V3_CTRL
            rts

; ----------------------------------------------------------------------------
; Flash Tracks on Hit - White flash for successful hits
; ----------------------------------------------------------------------------

flash_track1_hit:
            ldx #0
            lda #HIT_COL        ; White for hits
flash_t1h_loop:
            sta COLRAM + (TRACK1_ROW * 40),x
            inx
            cpx #38
            bne flash_t1h_loop
            lda #1
            sta COLRAM + (TRACK1_ROW * 40)
            rts

flash_track2_hit:
            ldx #0
            lda #HIT_COL
flash_t2h_loop:
            sta COLRAM + (TRACK2_ROW * 40),x
            inx
            cpx #38
            bne flash_t2h_loop
            lda #1
            sta COLRAM + (TRACK2_ROW * 40)
            rts

flash_track3_hit:
            ldx #0
            lda #HIT_COL
flash_t3h_loop:
            sta COLRAM + (TRACK3_ROW * 40),x
            inx
            cpx #38
            bne flash_t3h_loop
            lda #1
            sta COLRAM + (TRACK3_ROW * 40)
            rts

; ----------------------------------------------------------------------------
; Song Data
; ----------------------------------------------------------------------------

song_data:
            !byte 0, 1
            !byte 2, 2
            !byte 4, 3
            !byte 6, 1

            !byte 8, 2
            !byte 10, 3
            !byte 12, 1
            !byte 14, 2

            !byte 16, 3
            !byte 18, 1
            !byte 20, 2
            !byte 22, 3

            !byte 24, 1
            !byte 25, 2
            !byte 26, 3
            !byte 28, 1
            !byte 29, 2
            !byte 30, 3

            !byte $FF

; ----------------------------------------------------------------------------
; Note Arrays
; ----------------------------------------------------------------------------

note_track:
            !fill MAX_NOTES, 0

note_col:
            !fill MAX_NOTES, 0

Try This: Narrower Hit Zone

Make timing more challenging:

HIT_ZONE_MIN = 3                ; Tighter window
HIT_ZONE_MAX = 4                ; Only 2 columns wide

The game becomes much harder - you need precise timing.

Try This: Wider Hit Zone

Make it more forgiving:

HIT_ZONE_MIN = 1                ; Very wide window
HIT_ZONE_MAX = 6                ; 6 columns to hit

Good for practice or accessibility.

What You’ve Learnt

  • Position-based collision - Check if a value falls within a range
  • Carry flag signalling - Use SEC/CLC and BCS/BCC for function results
  • Note consumption - One keypress removes one note
  • Conditional sound - Only play audio on successful game actions

What’s Next

In Unit 6, we’ll add scoring and visual feedback. Perfect hits will award more points than late hits. The screen will flash to celebrate success.

What Changed

Unit 4 → Unit 5
+140-78
11 ; ============================================================================
2-; SID SYMPHONY - Unit 4: Custom Graphics
2+; SID SYMPHONY - Unit 5: Hit Detection
33 ; ============================================================================
4-; Design custom characters for a polished look. Notes are arrows, tracks are
5-; clean lines, hit zones have distinctive markers. The game looks professional.
4+; Detect when keypresses match notes in the hit zone. Press the right key at
5+; the right time to hit notes and hear the SID play. Wrong timing does nothing.
66 ;
77 ; Controls: Z = Track 1 (high), X = Track 2 (mid), C = Track 3 (low)
88 ; ============================================================================
...
3838 FLASH1_COL = 2 ; Red
3939 FLASH2_COL = 5 ; Green
4040 FLASH3_COL = 6 ; Blue
41+
42+HIT_COL = 1 ; White - flash on successful hit
43+
44+; ============================================================================
45+; HIT DETECTION SETTINGS
46+; ============================================================================
47+
48+; Hit zone boundaries (column positions)
49+HIT_ZONE_MIN = 2 ; Left edge of hit zone
50+HIT_ZONE_MAX = 5 ; Right edge of hit zone
51+HIT_ZONE_CENTRE = 3 ; Perfect hit position
4152
4253 ; ============================================================================
4354 ; MEMORY MAP
...
8899 ; Hit zone
89100 HIT_ZONE_COLUMN = 3
90101
91-; Custom character codes (we'll define these)
102+; Custom character codes
92103 CHAR_NOTE = 128 ; Filled arrow/circle for notes
93104 CHAR_TRACK = 129 ; Thin horizontal line for tracks
94105 CHAR_HITZONE = 130 ; Vertical bar for hit zone
...
111122 song_pos = $04
112123 song_pos_hi = $05
113124 temp_track = $06
125+key_pressed = $07 ; Which track key was pressed (0=none)
114126
115127 ; ----------------------------------------------------------------------------
116128 ; BASIC Stub
...
172184 ; ----------------------------------------------------------------------------
173185 ; Copy Character Set from ROM to RAM
174186 ; ----------------------------------------------------------------------------
175-; Copies the standard character ROM to $3000, then adds custom characters
176187
177188 copy_charset:
178- ; Disable interrupts during ROM access
179189 sei
180190
181- ; Make character ROM visible at $D000
182191 lda $01
183- pha ; Save processor port
184- and #$FB ; Clear bit 2 (CHAREN)
185- sta $01 ; Character ROM now at $D000
192+ pha
193+ and #$FB
194+ sta $01
186195
187- ; Copy 2KB (256 characters * 8 bytes)
188196 ldx #0
189197 copy_loop:
190198 lda $D000,x
...
206214 inx
207215 bne copy_loop
208216
209- ; Restore processor port
210217 pla
211218 sta $01
212219
213- ; Re-enable interrupts
214220 cli
215221
216- ; Define our custom characters
217222 jsr define_custom_chars
218223
219- ; Point VIC-II to our charset at $3000
220- ; Screen at $0400 = %0001 in upper nibble
221- ; Charset at $3000 = %110 in bits 1-3 = $0C
222224 lda #$1C
223225 sta CHARPTR
224226
...
227229 ; ----------------------------------------------------------------------------
228230 ; Define Custom Characters
229231 ; ----------------------------------------------------------------------------
230-; Creates note, track, and hit zone graphics
231232
232233 define_custom_chars:
233234 ; Character 128: Note (filled chevron pointing left)
234- lda #%00000110 ; Row 0
235+ lda #%00000110
235236 sta CHARSET + (CHAR_NOTE * 8) + 0
236- lda #%00011110 ; Row 1
237+ lda #%00011110
237238 sta CHARSET + (CHAR_NOTE * 8) + 1
238- lda #%01111110 ; Row 2
239+ lda #%01111110
239240 sta CHARSET + (CHAR_NOTE * 8) + 2
240- lda #%11111110 ; Row 3
241+ lda #%11111110
241242 sta CHARSET + (CHAR_NOTE * 8) + 3
242- lda #%11111110 ; Row 4
243+ lda #%11111110
243244 sta CHARSET + (CHAR_NOTE * 8) + 4
244- lda #%01111110 ; Row 5
245+ lda #%01111110
245246 sta CHARSET + (CHAR_NOTE * 8) + 5
246- lda #%00011110 ; Row 6
247+ lda #%00011110
247248 sta CHARSET + (CHAR_NOTE * 8) + 6
248- lda #%00000110 ; Row 7
249+ lda #%00000110
249250 sta CHARSET + (CHAR_NOTE * 8) + 7
250251
251252 ; Character 129: Track line (centered horizontal line)
252- lda #%00000000 ; Row 0
253+ lda #%00000000
253254 sta CHARSET + (CHAR_TRACK * 8) + 0
254- lda #%00000000 ; Row 1
255+ lda #%00000000
255256 sta CHARSET + (CHAR_TRACK * 8) + 1
256- lda #%00000000 ; Row 2
257+ lda #%00000000
257258 sta CHARSET + (CHAR_TRACK * 8) + 2
258- lda #%11111111 ; Row 3 - the line
259+ lda #%11111111
259260 sta CHARSET + (CHAR_TRACK * 8) + 3
260- lda #%11111111 ; Row 4 - the line
261+ lda #%11111111
261262 sta CHARSET + (CHAR_TRACK * 8) + 4
262- lda #%00000000 ; Row 5
263+ lda #%00000000
263264 sta CHARSET + (CHAR_TRACK * 8) + 5
264- lda #%00000000 ; Row 6
265+ lda #%00000000
265266 sta CHARSET + (CHAR_TRACK * 8) + 6
266- lda #%00000000 ; Row 7
267+ lda #%00000000
267268 sta CHARSET + (CHAR_TRACK * 8) + 7
268269
269270 ; Character 130: Hit zone (vertical bars)
270- lda #%01100110 ; Row 0
271+ lda #%01100110
271272 sta CHARSET + (CHAR_HITZONE * 8) + 0
272- lda #%01100110 ; Row 1
273+ lda #%01100110
273274 sta CHARSET + (CHAR_HITZONE * 8) + 1
274- lda #%01100110 ; Row 2
275+ lda #%01100110
275276 sta CHARSET + (CHAR_HITZONE * 8) + 2
276- lda #%01100110 ; Row 3
277+ lda #%01100110
277278 sta CHARSET + (CHAR_HITZONE * 8) + 3
278- lda #%01100110 ; Row 4
279+ lda #%01100110
279280 sta CHARSET + (CHAR_HITZONE * 8) + 4
280- lda #%01100110 ; Row 5
281+ lda #%01100110
281282 sta CHARSET + (CHAR_HITZONE * 8) + 5
282- lda #%01100110 ; Row 6
283+ lda #%01100110
283284 sta CHARSET + (CHAR_HITZONE * 8) + 6
284- lda #%01100110 ; Row 7
285+ lda #%01100110
285286 sta CHARSET + (CHAR_HITZONE * 8) + 7
286287
287288 rts
...
402403 rts
403404
404405 ; ----------------------------------------------------------------------------
405-; Draw Note - Uses custom note character
406+; Draw Note
406407 ; ----------------------------------------------------------------------------
407408
408409 draw_note:
...
425426 sta ZP_PTR_HI
426427
427428 ldy #0
428- lda #CHAR_NOTE ; Custom note character
429+ lda #CHAR_NOTE
429430 sta (ZP_PTR),y
430431
431432 lda note_col,x
...
511512 sta ZP_PTR_HI
512513
513514 ldy #0
514- lda #CHAR_TRACK ; Custom track character
515+ lda #CHAR_TRACK
515516 sta (ZP_PTR),y
516517
517518 lda note_col,x
...
583584 lda #BG_COL
584585 sta BGCOL
585586
586- ; Clear screen with spaces
587587 ldx #0
588588 lda #CHAR_SPACE
589589 clr_screen:
...
594594 inx
595595 bne clr_screen
596596
597- ; Set all colours to track line colour
598597 ldx #0
599598 lda #TRACK_LINE_COL
600599 clr_colour:
...
612611 rts
613612
614613 ; ----------------------------------------------------------------------------
615-; Draw Tracks - Uses custom track character
614+; Draw Tracks
616615 ; ----------------------------------------------------------------------------
617616
618617 draw_tracks:
619- ; Track 1
620618 lda #CHAR_TRACK
621619 ldx #0
622620 draw_t1:
...
625623 cpx #38
626624 bne draw_t1
627625
628- ; Track 2
629626 lda #CHAR_TRACK
630627 ldx #0
631628 draw_t2:
...
634631 cpx #38
635632 bne draw_t2
636633
637- ; Track 3
638634 lda #CHAR_TRACK
639635 ldx #0
640636 draw_t3:
...
646642 rts
647643
648644 ; ----------------------------------------------------------------------------
649-; Draw Hit Zones - Uses custom hit zone character
645+; Draw Hit Zones
650646 ; ----------------------------------------------------------------------------
651647
652648 draw_hit_zones:
653- lda #CHAR_HITZONE ; Custom hit zone character
649+ lda #CHAR_HITZONE
654650
655- ; Draw hit zone bars spanning all three tracks
656651 sta SCREEN + ((TRACK1_ROW-2) * 40) + HIT_ZONE_COLUMN
657652 sta SCREEN + ((TRACK1_ROW-1) * 40) + HIT_ZONE_COLUMN
658653 sta SCREEN + (TRACK1_ROW * 40) + HIT_ZONE_COLUMN
...
667662 sta SCREEN + ((TRACK3_ROW+1) * 40) + HIT_ZONE_COLUMN
668663 sta SCREEN + ((TRACK3_ROW+2) * 40) + HIT_ZONE_COLUMN
669664
670- ; Colour the hit zones
671665 lda #HIT_ZONE_COL
672666 sta COLRAM + ((TRACK1_ROW-2) * 40) + HIT_ZONE_COLUMN
673667 sta COLRAM + ((TRACK1_ROW-1) * 40) + HIT_ZONE_COLUMN
...
695689 lda title_text,x
696690 beq draw_title_done
697691 sta SCREEN + 13,x
698- lda #1 ; White
692+ lda #1
699693 sta COLRAM + 13,x
700694 inx
701695 bne draw_title
702696 draw_title_done:
703697
704- ; Track labels - use standard characters for letters
705- lda #$1A ; Z (screen code)
698+ lda #$1A ; Z
706699 sta SCREEN + (TRACK1_ROW * 40)
707700 lda #TRACK1_NOTE_COL
708701 sta COLRAM + (TRACK1_ROW * 40)
...
735728 !byte 0
736729
737730 instr_text:
738- !scr "custom graphics active"
731+ !scr "hit detection active"
739732 !byte 0
740733
741734 ; ----------------------------------------------------------------------------
...
815808 cpx #38
816809 bne reset_t3
817810
818- ; Restore key labels
819811 lda #TRACK1_NOTE_COL
820812 sta COLRAM + (TRACK1_ROW * 40)
821813 lda #TRACK2_NOTE_COL
...
823815 lda #TRACK3_NOTE_COL
824816 sta COLRAM + (TRACK3_ROW * 40)
825817
826- ; Restore hit zone colours
827818 lda #HIT_ZONE_COL
828819 sta COLRAM + (TRACK1_ROW * 40) + HIT_ZONE_COLUMN
829820 sta COLRAM + (TRACK2_ROW * 40) + HIT_ZONE_COLUMN
...
850841 rts
851842
852843 ; ----------------------------------------------------------------------------
853-; Check Keys
844+; Check Keys - Now with hit detection
854845 ; ----------------------------------------------------------------------------
846+; Checks if a key is pressed and if there's a matching note in the hit zone.
847+; Only plays sound and removes note on a successful hit.
855848
856849 check_keys:
850+ ; Check Z key (track 1)
857851 lda #$FD
858852 sta CIA1_PRA
859853 lda CIA1_PRB
860854 and #$10
861855 bne check_x_key
856+
857+ ; Z pressed - check for hit on track 1
858+ lda #1
859+ sta key_pressed
860+ jsr check_hit
861+ bcc check_x_key ; No hit - don't play sound
862862 jsr play_voice1
863- jsr flash_track1
863+ jsr flash_track1_hit
864864
865865 check_x_key:
866+ ; Check X key (track 2)
866867 lda #$FB
867868 sta CIA1_PRA
868869 lda CIA1_PRB
869870 and #$80
870871 bne check_c_key
872+
873+ ; X pressed - check for hit on track 2
874+ lda #2
875+ sta key_pressed
876+ jsr check_hit
877+ bcc check_c_key ; No hit - don't play sound
871878 jsr play_voice2
872- jsr flash_track2
879+ jsr flash_track2_hit
873880
874881 check_c_key:
882+ ; Check C key (track 3)
875883 lda #$FB
876884 sta CIA1_PRA
877885 lda CIA1_PRB
878886 and #$10
879887 bne check_keys_done
888+
889+ ; C pressed - check for hit on track 3
890+ lda #3
891+ sta key_pressed
892+ jsr check_hit
893+ bcc check_keys_done ; No hit - don't play sound
880894 jsr play_voice3
881- jsr flash_track3
895+ jsr flash_track3_hit
882896
883897 check_keys_done:
884898 lda #$FF
885899 sta CIA1_PRA
900+ rts
901+
902+; ----------------------------------------------------------------------------
903+; Check Hit - Find a note in the hit zone for the pressed track
904+; ----------------------------------------------------------------------------
905+; Input: key_pressed = track number (1-3)
906+; Output: Carry set if hit found, X = note index
907+; Carry clear if no hit
908+; Side effect: Removes hit note from play
909+
910+check_hit:
911+ ldx #0
912+
913+check_hit_loop:
914+ ; Check if this note slot is active
915+ lda note_track,x
916+ beq check_hit_next ; Empty slot - skip
917+
918+ ; Check if note is on the pressed track
919+ cmp key_pressed
920+ bne check_hit_next ; Wrong track - skip
921+
922+ ; Check if note is in the hit zone
923+ lda note_col,x
924+ cmp #HIT_ZONE_MIN
925+ bcc check_hit_next ; Too far left - skip
926+ cmp #HIT_ZONE_MAX+1
927+ bcs check_hit_next ; Too far right - skip
928+
929+ ; HIT! Note is in zone on correct track
930+ ; Erase the note from screen
931+ jsr erase_note
932+
933+ ; Deactivate the note
934+ lda #0
935+ sta note_track,x
936+
937+ ; Return with carry set (hit found)
938+ sec
939+ rts
940+
941+check_hit_next:
942+ inx
943+ cpx #MAX_NOTES
944+ bne check_hit_loop
945+
946+ ; No hit found - return with carry clear
947+ clc
886948 rts
887949
888950 ; ----------------------------------------------------------------------------
...
908970 rts
909971
910972 ; ----------------------------------------------------------------------------
911-; Flash Tracks
973+; Flash Tracks on Hit - White flash for successful hits
912974 ; ----------------------------------------------------------------------------
913975
914-flash_track1:
976+flash_track1_hit:
915977 ldx #0
916- lda #FLASH1_COL
917-flash_t1_loop:
978+ lda #HIT_COL ; White for hits
979+flash_t1h_loop:
918980 sta COLRAM + (TRACK1_ROW * 40),x
919981 inx
920982 cpx #38
921- bne flash_t1_loop
983+ bne flash_t1h_loop
922984 lda #1
923985 sta COLRAM + (TRACK1_ROW * 40)
924986 rts
925987
926-flash_track2:
988+flash_track2_hit:
927989 ldx #0
928- lda #FLASH2_COL
929-flash_t2_loop:
990+ lda #HIT_COL
991+flash_t2h_loop:
930992 sta COLRAM + (TRACK2_ROW * 40),x
931993 inx
932994 cpx #38
933- bne flash_t2_loop
995+ bne flash_t2h_loop
934996 lda #1
935997 sta COLRAM + (TRACK2_ROW * 40)
936998 rts
937999
938-flash_track3:
1000+flash_track3_hit:
9391001 ldx #0
940- lda #FLASH3_COL
941-flash_t3_loop:
1002+ lda #HIT_COL
1003+flash_t3h_loop:
9421004 sta COLRAM + (TRACK3_ROW * 40),x
9431005 inx
9441006 cpx #38
945- bne flash_t3_loop
1007+ bne flash_t3h_loop
9461008 lda #1
9471009 sta COLRAM + (TRACK3_ROW * 40)
9481010 rts