Skip to content

Move, collect and respond on the NES

Combine input, bounded movement, box collision, drawing and sound in a small program you can take apart.

Move onto the star. The count changes from 0 to 1, and you hear a short sound. Stay there: the star has gone, so it cannot score again. Press restart and it returns.

Those few seconds bring several useful routines together. Here is the NES version, with each part kept small enough to lift into a project of your own.

The NES example at its starting position, with the player, collectible and zero count.
Captured from the running Emu198x example.

Run it first

Target: NTSC NES, NROM cartridge. Controls: controller 1 D-pad; A restarts. Reset the machine to leave.

With Asm198x available, keep the code-sample folders together and build:

Shell
make -C nintendo-entertainment-system/patterns/assembly/framework/collect-and-respond

Start the generated NROM cartridge. The sample folder contains the complete program, build recipe and integration notes. The comparison explains the choices across all five versions.

The player moves one cell per six accepted updates. Hold a direction to keep moving. Restart reacts to a new press, so holding it will not trap the player at the starting position.

Connect the pieces

Read the controls once. Calculate the new and released bits from that sample. Update the world, then draw its new state. Call service_sound before the new update so an effect gets its full first frame. That order is visible in the caller:

The jobs in one update

demo.asm, excerpt
main_loop:
    jsr wait_frame
    jsr read_controls
    jsr map_controls
    jsr detect_edges
    jsr service_sound
    jsr update_player
    jsr draw_screen
    inc updates

Startup owns the machine-specific work. It establishes the memory and hardware assumptions that the smaller routines rely on:

Initialise this caller

demo.asm, excerpt
reset:
    sei
    cld
    ldx #$ff
    txs
    inx
    stx $2000
    stx $2001
    stx $4010
    stx $4015
    lda #$40
    sta $4017
    lda #0
clear_ram:
    sta $0000,x
    sta $0100,x
    sta $0200,x
    sta $0300,x
    sta $0400,x
    sta $0500,x
    sta $0600,x
    sta $0700,x
    inx
    bne clear_ram
warmup_1:
    bit $2002
    bpl warmup_1
warmup_2:
    bit $2002
    bpl warmup_2

    ; Rendering is off: initialise nametable, attributes and palettes.
    bit $2002
    lda #$20
    sta $2006
    lda #0
    sta $2006
    ldy #4
    ldx #0
clear_picture:
    sta $2007
    inx
    bne clear_picture
    dey
    bne clear_picture
    lda #$3f
    sta $2006
    lda #0
    sta $2006
    lda #$0f
    sta $2007
    lda #$30
    sta $2007
    jsr reset_game
    jsr initialise_clock
    lda #$80
    sta $2000
    lda #$0a
    sta $2001

This program owns the nametable, CHR tiles 0–4, background palette, pulse channel 1, $4015 and controller 1. DMC stays disabled. NMI only increments the frame clock. The routines together change A/X/Y and flags. Keep competing writers out of those resources; adding a music player or IRQ handler requires an explicit agreement about who writes what.

Move, test and consume

The playfield is 16 × 10 cells. The player begins at (2,5); the star occupies (10,5). The rules keep X between 0 and 15 and Y between 0 and 9. Diagonals move on both axes. Opposing directions are processed left/right, then up/down; choose cancellation or last-direction priority if that better suits your game.

previous, pressed and released belong to input sampling. player_x/player_y, move_delay, item_active and score belong to the rules. Reset preserves input history: clearing it while a button is held would invent another press.

The collision uses half-open boxes. A cell at (x,y) has edges (x,y,x+1,y+1), so merely touching the star’s edge leaves it alone. The routine can also compare larger rectangles or pixel coordinates, provided both boxes use the same units and edge arithmetic does not wrap.

Reusable box overlap

collision.inc
.segment "CODE"
; Half-open boxes [left,top,right,bottom]; all edges unsigned bytes.
; Reject empty/reversed boxes and touching edges. Returns C=1 on overlap.
; Changes A/flags; preserves X/Y. No arithmetic inside the test.
boxes_overlap:
    lda rect_a
    cmp rect_a+2
    bcs boxes_apart
    lda rect_a+1
    cmp rect_a+3
    bcs boxes_apart
    lda rect_b
    cmp rect_b+2
    bcs boxes_apart
    lda rect_b+1
    cmp rect_b+3
    bcs boxes_apart
    lda rect_a
    cmp rect_b+2
    bcs boxes_apart
    lda rect_b
    cmp rect_a+2
    bcs boxes_apart
    lda rect_a+1
    cmp rect_b+3
    bcs boxes_apart
    lda rect_b+1
    cmp rect_a+3
    bcs boxes_apart
    sec
    rts
boxes_apart:
    clc
    rts
rect_b: .byte 10,5,11,6
.segment "BSS"
rect_a: .res 4
.segment "CODE"

The update consumes the star before starting feedback. That single state change is what prevents the next frame from collecting it again.

Input edges, movement and collection

rules.inc
; Writable byte state is allocated by demo.asm. Bounds: x=0..15, y=0..9.
; read_controls supplies controls; up/down/left/right/action = 1/2/4/8/16.
; All routines change A/X/Y/flags; no IRQ may write this state.
; Call edges -> service_sound -> update_player -> draw_screen once per frame.
detect_edges:
    lda previous
    eor #$ff
    and controls
    sta pressed
    lda controls
    eor #$ff
    and previous
    sta released
    lda controls
    sta previous
    rts
reset_game:
    lda #2
    sta player_x
    lda #5
    sta player_y
    lda #1
    sta item_active
    lda #0
    sta score
    sta move_delay
    rts
update_player:
    lda pressed
    and #16
    beq movement
    jsr reset_game
    rts
movement:
    lda move_delay
    beq move_now
    dec move_delay
    rts
move_now:
    lda #5
    sta move_delay
    lda controls
    and #4
    beq move_right
    lda player_x
    beq move_right
    dec player_x
move_right:
    lda controls
    and #8
    beq move_up
    lda player_x
    cmp #15
    beq move_up
    inc player_x
move_up:
    lda controls
    and #1
    beq move_down
    lda player_y
    beq move_down
    dec player_y
move_down:
    lda controls
    and #2
    beq test_collection
    lda player_y
    cmp #9
    beq test_collection
    inc player_y
test_collection:
    lda item_active
    beq collection_done
    lda player_x
    sta rect_a
    clc
    adc #1
    sta rect_a+2
    lda player_y
    sta rect_a+1
    clc
    adc #1
    sta rect_a+3
    jsr boxes_overlap
    bcc collection_done
    lda #0
    sta item_active           ; consume BEFORE starting feedback
    inc score
    jsr trigger_sound
collection_done:
    rts

For this exact one-cell scene, coordinate equality would do less work. The general box routine earns its extra comparisons when objects have different sizes. Box collision and its alternatives explains when a more precise second test helps. A fast object can skip over a target between samples; that calls for a path or swept test as well.

Show the new state

This version uses background tiles written into a nametable during vblank. old_x/old_y remember which cell to erase. The item is drawn before the player, so the player wins the draw order when they overlap.

Draw, erase and show the count

draw.inc
; A=tile, X=x,Y=y; changes A/X/Y/flags. Call only during vblank.
; Nametable $2000, logical grid inset column 8,row 6. No OAM or sprites used.
put_cell:
    sta draw_tile
    tya
    clc
    adc #6
    sta draw_y
    lsr
    lsr
    lsr
    ora #$20
    sta $2006
    lda draw_y
    and #7
    asl
    asl
    asl
    asl
    asl
    sta draw_y
    txa
    clc
    adc #8
    ora draw_y
    sta $2006
    lda draw_tile
    sta $2007
    rts
draw_screen:
    bit $2002
    lda #0
    ldx old_x
    ldy old_y
    jsr put_cell
    lda #0
    ldx #10
    ldy #5
    jsr put_cell
    lda item_active
    beq draw_player
    lda #2
    ldx #10
    ldy #5
    jsr put_cell
draw_player:
    lda #1
    ldx player_x
    ldy player_y
    jsr put_cell
    lda player_x
    sta old_x
    lda player_y
    sta old_y
    lda score
    clc
    adc #3
    ldx #0
    ldy #0
    jsr put_cell
    lda #0
    sta $2005
    sta $2005
    lda #$80
    sta $2000
    rts

Painting a blank over the old position works because the background is empty. With a map behind the player, restore its tile. With overlapping objects, redraw the affected scene or choose another renderer. Keep that decision in the drawing routine so the rules can stay useful.

Make the event audible

On this machine, pulse channel 1 plays while the loop continues; a frame counter disables it. The count also changes visibly, so sound carries useful feedback without being the only signal.

Collection sound

sound.inc
; Pulse 1 only. $4015 is owned by this caller; DMC remains disabled.
; Non-blocking eight-frame effect. service_sound precedes update_player.
trigger_sound:
    lda #1
    sta $4015
    lda #$d5
    ldx #0
    jsr pulse1_tone
    lda #8
    sta sound_ticks
    rts
service_sound:
    lda sound_ticks
    beq sound_done
    dec sound_ticks
    bne sound_done
    lda #0
    sta $4015
sound_done:
    rts
map_controls:
    lda controls
    sta raw_controls
    lda #0
    sta controls
    ldx #4
map_button:
    lda raw_controls
    and raw_masks,x
    beq map_next
    lda controls
    ora game_masks,x
    sta controls
map_next:
    dex
    bpl map_button
    rts
raw_masks: .byte 8,4,2,1,$80
game_masks: .byte 1,2,4,8,16

Take it into your game

Copy the routine you need, its state and its maintained includes. Read the declaration and caller together: the caller shows initialisation, call frequency and ownership. The sample README lists register effects and resource limits. Preserve the relative include layout, or update those paths deliberately when moving files.

These examples use whole-cell movement and a frame-based repeat counter. Fractional motion, scrolling, multiple items and sound priorities are useful next steps when a game needs them. A 50 Hz and a 60 Hz machine give this counter different wall-clock speeds.

Code status: assembled and emulator-executed for the named configuration. Checks drive real input, exercise all four bounds, reject edge-only contact, collect once, restart while held and capture the picture and PCM. A broken collection caller must fail the same guard. The execution record identifies the exact sources, binary, emulator and firmware; it establishes this caller rather than every feature of the machine.

Read the execution record · Understand the frame wait · Read the controls · Explore the sound routine

For several bullets, enemies or pickups at once, reusable object slots add bounded acquire/release operations and a lifetime pass without shifting live neighbours.

Game states and restart separate a fresh session from resuming a paused one, while preserving input history.