Game 1 Unit 10 of 16

Timing Windows

Precision matters. Add Perfect, Good, and Late grades with different point values to reward accurate timing.

63% of SID Symphony

Beyond Hit or Miss

Until now, hitting a note anywhere in the hit zone gave the same reward. Press the key when the note is barely visible, or press it at the last possible moment — same points, same feedback.

Real rhythm games don’t work this way. They grade your timing: Perfect, Great, Good, OK, Miss. Better timing means better scores. This precision is what separates casual play from mastery.

This unit divides our hit zone into timing windows, each with its own point value and visual feedback.

Defining the Windows

Our hit zone is 8 columns wide (columns 0-7, where column 0 is the target line). We divide this into three grades:

GradeColumnsPointsColour
Perfect0-215Green
Good3-510Yellow
Late6-75Red
; Timing window boundaries (within HIT_ZONE_X = 8)
ZONE_PERFECT    = 3             ; Columns 0-2 = Perfect
ZONE_GOOD       = 6             ; Columns 3-5 = Good
                                ; Columns 6-7 = Late

; Point values per grade
POINTS_PERFECT  = 15
POINTS_GOOD     = 10
POINTS_LATE     = 5

; Grade identifiers
GRADE_PERFECT   = 0
GRADE_GOOD      = 1
GRADE_LATE      = 2

GRADE_FLASH_TIME = 20           ; Frames to show grade text

The closer to the target line (column 0), the better the grade. Players who wait until the last moment get Perfect; those who press early get Late.

Calculating the Grade

When a hit occurs, we check where the note was in the hit zone:

calculate_grade:
            lda hit_position
            cmp #ZONE_PERFECT       ; < 3 = Perfect
            bcs cg_not_perfect
            ; PERFECT!
            lda #GRADE_PERFECT
            sta current_grade
            lda #POINTS_PERFECT
            jmp cg_add_score

cg_not_perfect:
            cmp #ZONE_GOOD          ; < 6 = Good
            bcs cg_late
            ; GOOD
            lda #GRADE_GOOD
            sta current_grade
            lda #POINTS_GOOD
            jmp cg_add_score

cg_late:
            ; LATE (positions 6-7)
            lda #GRADE_LATE
            sta current_grade
            lda #POINTS_LATE

cg_add_score:
            ; Add points (A contains point value)
            clc
            adc score_lo
            sta score_lo
            bcc cg_done
            inc score_hi
cg_done:
            rts

The bcs (branch if carry set) instruction branches when the comparison result is greater than or equal. So cmp #ZONE_PERFECT followed by bcs branches if the position is 3 or higher — meaning it’s not Perfect.

Showing the Grade

Each grade flashes briefly on screen in its track row. We use indirect addressing with zero-page pointers to write to the correct screen location:

show_grade_text:
            ; Set up screen pointer based on track
            lda check_track
            cmp #TRACK_1
            bne sgt_not_t1
            lda #<(SCREEN + ROW_TRACK1 * 40 + 15)
            sta grade_screen_ptr
            lda #>(SCREEN + ROW_TRACK1 * 40 + 15)
            sta grade_screen_ptr + 1
            lda #GRADE_FLASH_TIME
            sta grade_timer_t1
            jmp sgt_draw
            ; ... similar for tracks 2 and 3

sgt_draw:
            ; Draw grade text based on current_grade
            lda current_grade
            cmp #GRADE_PERFECT
            bne sgt_not_perf
            ; Draw "PERFECT!" in green
            ldx #$00
sgt_perf_loop:
            lda perfect_text,x
            beq sgt_perf_done
            ldy #$00
            sta (grade_screen_ptr),y
            lda #COL_GREEN
            sta (grade_colour_ptr),y
            ; Increment pointers
            inc grade_screen_ptr
            bne +
            inc grade_screen_ptr + 1
+           inc grade_colour_ptr
            bne +
            inc grade_colour_ptr + 1
+           inx
            jmp sgt_perf_loop
sgt_perf_done:
            rts

The pointers live in zero page ($57-$5a) so we can use indirect indexed addressing: sta (grade_screen_ptr),y. This lets us write to calculated screen addresses without hardcoding them.

Timer-Based Clearing

Each track has its own grade timer. Every frame, we check if any timer has expired and clear that track’s grade text:

update_grade_text:
            ; Track 1
            lda grade_timer_t1
            beq ugt_t2              ; Skip if already zero
            dec grade_timer_t1
            bne ugt_t2              ; Skip if not yet zero
            jsr clear_grade_t1      ; Timer just hit zero - clear text

ugt_t2:
            lda grade_timer_t2
            beq ugt_t3
            dec grade_timer_t2
            bne ugt_t3
            jsr clear_grade_t2

ugt_t3:
            lda grade_timer_t3
            beq ugt_done
            dec grade_timer_t3
            bne ugt_done
            jsr clear_grade_t3

ugt_done:
            rts

clear_grade_t1:
            ldx #$00
cgt1_loop:
            lda #TRACK_CHAR         ; Restore track line character
            sta SCREEN + ROW_TRACK1 * 40 + 15,x
            lda #COL_CYAN
            sta COLOUR + ROW_TRACK1 * 40 + 15,x
            inx
            cpx #8                  ; Clear 8 characters
            bne cgt1_loop
            rts

This pattern — countdown timer that triggers an action when it reaches zero — appears constantly in game programming. We’ll use it again for combo displays, power-ups, and animations.

Integration

The grade calculation replaces the old fixed-point scoring. In check_hit_on_track:

check_hit_on_track:
            ; ... find note in hit zone ...

            ; HIT! Save position for grade calculation
            lda note_x,x
            sta hit_position

            jsr erase_note
            lda #NOTE_INACTIVE
            ldx hit_note_idx
            sta note_x,x

            ; Calculate grade and add appropriate score
            jsr calculate_grade

            ; Show grade text on screen
            jsr show_grade_text

            ; Continue with hit flash, crowd update, etc.
            ; ...

And in the main game loop, we add the grade text update:

            ; Update sound effects
            jsr update_filter_sweep
            jsr update_bum_note

            ; Update grade text (Unit 10)
            jsr update_grade_text

            ; Update displays
            jsr update_display

The Result

Play the game now. You’ll see:

  • “PERFECT!” in green when you hit notes right at the target line
  • “GOOD” in yellow for slightly early hits
  • “LATE” in red for hits at the edge of the zone

Watch your score climb faster with precise timing. A song played perfectly scores 50% higher than one played sloppily.

What’s Next

We have grades, but no way to chain them. In Unit 11, we add a combo system — consecutive Perfect hits multiply your score. The pressure builds.