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

Moving the Player

Add controller input — move your sprite with the D-pad.

3% of Neon Nexus

What You’re Building

The sprite now moves. Press the D-pad and watch it respond.

Moving Player

This is the moment it becomes a game.

The Code

; =============================================================================
; NEON NEXUS - Unit 2: Moving the Player
; =============================================================================
; The player sprite now moves with the D-pad.
; Press Up, Down, Left, Right to move around the screen.
; =============================================================================

; -----------------------------------------------------------------------------
; NES Hardware Addresses
; -----------------------------------------------------------------------------
PPUCTRL   = $2000
PPUMASK   = $2001
PPUSTATUS = $2002
OAMADDR   = $2003
PPUADDR   = $2006
PPUDATA   = $2007
OAMDMA    = $4014

JOYPAD1   = $4016     ; Controller 1
JOYPAD2   = $4017     ; Controller 2

; Controller button masks
BTN_A      = %10000000
BTN_B      = %01000000
BTN_SELECT = %00100000
BTN_START  = %00010000
BTN_UP     = %00001000
BTN_DOWN   = %00000100
BTN_LEFT   = %00000010
BTN_RIGHT  = %00000001

; -----------------------------------------------------------------------------
; Game Constants
; -----------------------------------------------------------------------------
PLAYER_START_X = 124
PLAYER_START_Y = 116
PLAYER_TILE    = 1
BG_COLOUR      = $12

PLAYER_SPEED   = 2    ; Pixels per frame (try 1, 2, or 3)

; Screen boundaries
SCREEN_LEFT   = 0
SCREEN_RIGHT  = 248   ; 256 - 8 (sprite width)
SCREEN_TOP    = 0
SCREEN_BOTTOM = 224   ; 240 - 16 (sprite height + overscan)

; -----------------------------------------------------------------------------
; Memory Layout
; -----------------------------------------------------------------------------
.segment "ZEROPAGE"
player_x:    .res 1
player_y:    .res 1
buttons:     .res 1   ; Current button state

.segment "OAM"
oam_buffer:  .res 256

.segment "BSS"

; -----------------------------------------------------------------------------
; iNES Header
; -----------------------------------------------------------------------------
.segment "HEADER"
    .byte "NES", $1A
    .byte 2           ; 32KB PRG-ROM
    .byte 1           ; 8KB CHR-ROM
    .byte $01         ; Mapper 0, vertical mirroring
    .byte $00
    .byte 0,0,0,0,0,0,0,0

; -----------------------------------------------------------------------------
; Code
; -----------------------------------------------------------------------------
.segment "CODE"

reset:
    sei
    cld
    ldx #$40
    stx $4017
    ldx #$FF
    txs
    inx
    stx PPUCTRL
    stx PPUMASK
    stx $4010

@vblank1:
    bit PPUSTATUS
    bpl @vblank1

    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

@vblank2:
    bit PPUSTATUS
    bpl @vblank2

    ; Load palette
    bit PPUSTATUS
    lda #$3F
    sta PPUADDR
    lda #$00
    sta PPUADDR

    ldx #0
@load_palette:
    lda palette_data, x
    sta PPUDATA
    inx
    cpx #32
    bne @load_palette

    ; Set up player
    lda #PLAYER_START_X
    sta player_x
    lda #PLAYER_START_Y
    sta player_y

    ; Initialise player sprite
    lda player_y
    sta oam_buffer+0
    lda #PLAYER_TILE
    sta oam_buffer+1
    lda #0
    sta oam_buffer+2
    lda player_x
    sta oam_buffer+3

    ; Hide other sprites
    lda #$FF
    ldx #4
@hide_sprites:
    sta oam_buffer, x
    inx
    bne @hide_sprites

    ; Enable rendering
    lda #%10010000
    sta PPUCTRL
    lda #%00011110
    sta PPUMASK

; === Main Loop ===
main_loop:
    ; Read controller
    jsr read_controller

    ; Move player based on input
    jsr move_player

    jmp main_loop

; -----------------------------------------------------------------------------
; Read Controller
; -----------------------------------------------------------------------------
read_controller:
    ; Strobe the controller
    lda #1
    sta JOYPAD1
    lda #0
    sta JOYPAD1

    ; Read 8 buttons into 'buttons' variable
    ldx #8
@read_loop:
    lda JOYPAD1
    lsr a           ; Bit 0 -> Carry
    rol buttons     ; Carry -> buttons
    dex
    bne @read_loop

    rts

; -----------------------------------------------------------------------------
; Move Player
; -----------------------------------------------------------------------------
move_player:
    ; Check UP
    lda buttons
    and #BTN_UP
    beq @check_down

    lda player_y
    sec
    sbc #PLAYER_SPEED
    cmp #SCREEN_TOP
    bcc @check_down      ; Don't go above top
    sta player_y

@check_down:
    lda buttons
    and #BTN_DOWN
    beq @check_left

    lda player_y
    clc
    adc #PLAYER_SPEED
    cmp #SCREEN_BOTTOM
    bcs @check_left      ; Don't go below bottom
    sta player_y

@check_left:
    lda buttons
    and #BTN_LEFT
    beq @check_right

    lda player_x
    sec
    sbc #PLAYER_SPEED
    cmp #SCREEN_LEFT
    bcc @check_right     ; Don't go past left edge
    sta player_x

@check_right:
    lda buttons
    and #BTN_RIGHT
    beq @done

    lda player_x
    clc
    adc #PLAYER_SPEED
    cmp #SCREEN_RIGHT
    bcs @done            ; Don't go past right edge
    sta player_x

@done:
    rts

; === NMI ===
nmi:
    pha
    txa
    pha
    tya
    pha

    ; DMA sprites
    lda #0
    sta OAMADDR
    lda #>oam_buffer
    sta OAMDMA

    ; Update sprite from player position
    lda player_y
    sta oam_buffer+0
    lda player_x
    sta oam_buffer+3

    pla
    tay
    pla
    tax
    pla
    rti

irq:
    rti

; -----------------------------------------------------------------------------
; Data
; -----------------------------------------------------------------------------
palette_data:
    .byte BG_COLOUR, $00, $10, $20
    .byte BG_COLOUR, $00, $10, $20
    .byte BG_COLOUR, $00, $10, $20
    .byte BG_COLOUR, $00, $10, $20
    .byte BG_COLOUR, $30, $20, $0F
    .byte BG_COLOUR, $30, $16, $0F
    .byte BG_COLOUR, $30, $12, $0F
    .byte BG_COLOUR, $30, $14, $0F

; -----------------------------------------------------------------------------
; Vectors
; -----------------------------------------------------------------------------
.segment "VECTORS"
    .word nmi
    .word reset
    .word irq

; -----------------------------------------------------------------------------
; CHR-ROM
; -----------------------------------------------------------------------------
.segment "CHARS"

; Tile 0: Empty
.byte $00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00

; Tile 1: Player sprite
.byte %00011000
.byte %00111100
.byte %01111110
.byte %11111111
.byte %00111100
.byte %00111100
.byte %00111100
.byte %00111100
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000

.res 8192 - 32, $00

Build and Run

ca65 nexus.asm -o nexus.o
ld65 -C nes.cfg nexus.o -o nexus.nes

Use the D-pad (arrow keys in most emulators) to move the sprite around.

What’s New

Two things changed from Unit 1:

1. Reading the Controller

read_controller:
    lda #1
    sta JOYPAD1       ; Strobe controller
    lda #0
    sta JOYPAD1

    ldx #8
@read_loop:
    lda JOYPAD1       ; Read one bit
    lsr a             ; Shift into Carry
    rol buttons       ; Rotate into buttons
    dex
    bne @read_loop
    rts

The NES controller sends button states one bit at a time. Strobe it, then read 8 times.

2. Moving Based on Input

move_player:
    lda buttons
    and #BTN_UP       ; Is UP pressed?
    beq @check_down   ; No? Skip

    lda player_y
    sec
    sbc #PLAYER_SPEED ; Move up
    sta player_y

@check_down:
    ; ... same pattern for other directions

Check each button. If pressed, adjust position. Boundary checks prevent going off screen.

Try This

  1. Change speed — Find PLAYER_SPEED = 2 and try 1 (slow) or 4 (fast)

  2. Remove boundaries — Delete the cmp and bcc/bcs lines. Now you can wrap around the screen.

  3. Invert controls — Swap BTN_UP with BTN_DOWN in the movement code.

The Main Loop

Notice the structure:

main_loop:
    jsr read_controller
    jsr move_player
    jmp main_loop

Read input, update game state, repeat. This is a game loop. Every NES game has one.

Next

Unit 3 explains how all this works — the PPU, VBlank, and why the code is structured this way.

What Changed

Unit 1 → Unit 2
+176-89
11 ; =============================================================================
2-; NEON NEXUS - Unit 1: Hello NES
2+; NEON NEXUS - Unit 2: Moving the Player
33 ; =============================================================================
4-; Your first NES program. A sprite on a coloured background.
5-; Run it. Change the colours. Move the sprite. Make it yours.
4+; The player sprite now moves with the D-pad.
5+; Press Up, Down, Left, Right to move around the screen.
66 ; =============================================================================
77
88 ; -----------------------------------------------------------------------------
99 ; NES Hardware Addresses
1010 ; -----------------------------------------------------------------------------
11-PPUCTRL = $2000 ; PPU control register
12-PPUMASK = $2001 ; PPU mask register
13-PPUSTATUS = $2002 ; PPU status register
14-OAMADDR = $2003 ; OAM address
15-PPUADDR = $2006 ; PPU address
16-PPUDATA = $2007 ; PPU data
17-OAMDMA = $4014 ; OAM DMA register
11+PPUCTRL = $2000
12+PPUMASK = $2001
13+PPUSTATUS = $2002
14+OAMADDR = $2003
15+PPUADDR = $2006
16+PPUDATA = $2007
17+OAMDMA = $4014
18+
19+JOYPAD1 = $4016 ; Controller 1
20+JOYPAD2 = $4017 ; Controller 2
21+
22+; Controller button masks
23+BTN_A = %10000000
24+BTN_B = %01000000
25+BTN_SELECT = %00100000
26+BTN_START = %00010000
27+BTN_UP = %00001000
28+BTN_DOWN = %00000100
29+BTN_LEFT = %00000010
30+BTN_RIGHT = %00000001
1831
1932 ; -----------------------------------------------------------------------------
2033 ; Game Constants
2134 ; -----------------------------------------------------------------------------
22-; Try changing these values and see what happens!
23-PLAYER_START_X = 124 ; Player X position (0-247)
24-PLAYER_START_Y = 116 ; Player Y position (0-231)
25-PLAYER_TILE = 1 ; Which tile to use for the player
26-BG_COLOUR = $12 ; Background colour (try $01, $21, $31, $0F)
35+PLAYER_START_X = 124
36+PLAYER_START_Y = 116
37+PLAYER_TILE = 1
38+BG_COLOUR = $12
39+
40+PLAYER_SPEED = 2 ; Pixels per frame (try 1, 2, or 3)
41+
42+; Screen boundaries
43+SCREEN_LEFT = 0
44+SCREEN_RIGHT = 248 ; 256 - 8 (sprite width)
45+SCREEN_TOP = 0
46+SCREEN_BOTTOM = 224 ; 240 - 16 (sprite height + overscan)
2747
2848 ; -----------------------------------------------------------------------------
2949 ; Memory Layout
3050 ; -----------------------------------------------------------------------------
3151 .segment "ZEROPAGE"
32-player_x: .res 1 ; Player X position
33-player_y: .res 1 ; Player Y position
52+player_x: .res 1
53+player_y: .res 1
54+buttons: .res 1 ; Current button state
3455
3556 .segment "OAM"
36-oam_buffer: .res 256 ; Sprite data (copied to PPU each frame)
57+oam_buffer: .res 256
3758
3859 .segment "BSS"
39-; General RAM variables go here
4060
4161 ; -----------------------------------------------------------------------------
4262 ; iNES Header
4363 ; -----------------------------------------------------------------------------
4464 .segment "HEADER"
45- .byte "NES", $1A ; iNES identifier
46- .byte 2 ; 2x 16KB PRG-ROM = 32KB
47- .byte 1 ; 1x 8KB CHR-ROM = 8KB
65+ .byte "NES", $1A
66+ .byte 2 ; 32KB PRG-ROM
67+ .byte 1 ; 8KB CHR-ROM
4868 .byte $01 ; Mapper 0, vertical mirroring
49- .byte $00 ; Mapper 0
50- .byte 0,0,0,0,0,0,0,0 ; Padding
69+ .byte $00
70+ .byte 0,0,0,0,0,0,0,0
5171
5272 ; -----------------------------------------------------------------------------
5373 ; Code
5474 ; -----------------------------------------------------------------------------
5575 .segment "CODE"
5676
57-; === RESET: Called when NES powers on ===
5877 reset:
59- sei ; Disable interrupts
60- cld ; Clear decimal mode
78+ sei
79+ cld
6180 ldx #$40
62- stx $4017 ; Disable APU frame IRQ
81+ stx $4017
6382 ldx #$FF
64- txs ; Set up stack
65- inx ; X = 0
66- stx PPUCTRL ; Disable NMI
67- stx PPUMASK ; Disable rendering
68- stx $4010 ; Disable DMC IRQs
83+ txs
84+ inx
85+ stx PPUCTRL
86+ stx PPUMASK
87+ stx $4010
6988
70- ; Wait for PPU to stabilise (first wait)
7189 @vblank1:
7290 bit PPUSTATUS
7391 bpl @vblank1
7492
75- ; Clear RAM while we wait
7693 lda #0
7794 @clear_ram:
7895 sta $0000, x
...
86103 inx
87104 bne @clear_ram
88105
89- ; Wait for PPU (second wait)
90106 @vblank2:
91107 bit PPUSTATUS
92108 bpl @vblank2
93-
94- ; === PPU is ready - set up graphics ===
95109
96110 ; Load palette
97- bit PPUSTATUS ; Reset PPU address latch
111+ bit PPUSTATUS
98112 lda #$3F
99113 sta PPUADDR
100114 lda #$00
101- sta PPUADDR ; PPU address = $3F00 (palette)
115+ sta PPUADDR
102116
103117 ldx #0
104118 @load_palette:
...
108122 cpx #32
109123 bne @load_palette
110124
111- ; Set up player sprite
125+ ; Set up player
112126 lda #PLAYER_START_X
113127 sta player_x
114128 lda #PLAYER_START_Y
115129 sta player_y
116130
117- ; Initialise sprite in OAM buffer
131+ ; Initialise player sprite
118132 lda player_y
119- sta oam_buffer+0 ; Y position
133+ sta oam_buffer+0
120134 lda #PLAYER_TILE
121- sta oam_buffer+1 ; Tile number
135+ sta oam_buffer+1
122136 lda #0
123- sta oam_buffer+2 ; Attributes (palette 0, no flip)
137+ sta oam_buffer+2
124138 lda player_x
125- sta oam_buffer+3 ; X position
139+ sta oam_buffer+3
126140
127- ; Hide all other sprites (move off screen)
141+ ; Hide other sprites
128142 lda #$FF
129143 ldx #4
130144 @hide_sprites:
...
133147 bne @hide_sprites
134148
135149 ; Enable rendering
136- lda #%10010000 ; Enable NMI, sprites from pattern table 1
150+ lda #%10010000
137151 sta PPUCTRL
138- lda #%00011110 ; Show sprites and background
152+ lda #%00011110
139153 sta PPUMASK
140154
141- ; === Main Loop ===
155+; === Main Loop ===
142156 main_loop:
143- jmp main_loop ; Wait for NMI (nothing to do yet!)
157+ ; Read controller
158+ jsr read_controller
144159
145-; === NMI: Called every frame during VBlank ===
160+ ; Move player based on input
161+ jsr move_player
162+
163+ jmp main_loop
164+
165+; -----------------------------------------------------------------------------
166+; Read Controller
167+; -----------------------------------------------------------------------------
168+read_controller:
169+ ; Strobe the controller
170+ lda #1
171+ sta JOYPAD1
172+ lda #0
173+ sta JOYPAD1
174+
175+ ; Read 8 buttons into 'buttons' variable
176+ ldx #8
177+@read_loop:
178+ lda JOYPAD1
179+ lsr a ; Bit 0 -> Carry
180+ rol buttons ; Carry -> buttons
181+ dex
182+ bne @read_loop
183+
184+ rts
185+
186+; -----------------------------------------------------------------------------
187+; Move Player
188+; -----------------------------------------------------------------------------
189+move_player:
190+ ; Check UP
191+ lda buttons
192+ and #BTN_UP
193+ beq @check_down
194+
195+ lda player_y
196+ sec
197+ sbc #PLAYER_SPEED
198+ cmp #SCREEN_TOP
199+ bcc @check_down ; Don't go above top
200+ sta player_y
201+
202+@check_down:
203+ lda buttons
204+ and #BTN_DOWN
205+ beq @check_left
206+
207+ lda player_y
208+ clc
209+ adc #PLAYER_SPEED
210+ cmp #SCREEN_BOTTOM
211+ bcs @check_left ; Don't go below bottom
212+ sta player_y
213+
214+@check_left:
215+ lda buttons
216+ and #BTN_LEFT
217+ beq @check_right
218+
219+ lda player_x
220+ sec
221+ sbc #PLAYER_SPEED
222+ cmp #SCREEN_LEFT
223+ bcc @check_right ; Don't go past left edge
224+ sta player_x
225+
226+@check_right:
227+ lda buttons
228+ and #BTN_RIGHT
229+ beq @done
230+
231+ lda player_x
232+ clc
233+ adc #PLAYER_SPEED
234+ cmp #SCREEN_RIGHT
235+ bcs @done ; Don't go past right edge
236+ sta player_x
237+
238+@done:
239+ rts
240+
241+; === NMI ===
146242 nmi:
147- pha ; Save registers
243+ pha
148244 txa
149245 pha
150246 tya
151247 pha
152248
153- ; Copy sprite data to PPU
249+ ; DMA sprites
154250 lda #0
155251 sta OAMADDR
156- lda #>oam_buffer ; High byte of $0200
157- sta OAMDMA ; Triggers DMA transfer
252+ lda #>oam_buffer
253+ sta OAMDMA
158254
159- ; Update sprite position from variables
255+ ; Update sprite from player position
160256 lda player_y
161257 sta oam_buffer+0
162258 lda player_x
163259 sta oam_buffer+3
164260
165- pla ; Restore registers
261+ pla
166262 tay
167263 pla
168264 tax
169265 pla
170266 rti
171267
172-; === IRQ: Not used ===
173268 irq:
174269 rti
175270
176271 ; -----------------------------------------------------------------------------
177272 ; Data
178273 ; -----------------------------------------------------------------------------
179-
180-; Palette data: 4 background palettes + 4 sprite palettes
181274 palette_data:
182- ; Background palettes
183- .byte BG_COLOUR, $00, $10, $20 ; Palette 0 (background colour)
184- .byte BG_COLOUR, $00, $10, $20 ; Palette 1
185- .byte BG_COLOUR, $00, $10, $20 ; Palette 2
186- .byte BG_COLOUR, $00, $10, $20 ; Palette 3
187- ; Sprite palettes
188- .byte BG_COLOUR, $30, $20, $0F ; Palette 0 (player: white, red, black)
189- .byte BG_COLOUR, $30, $16, $0F ; Palette 1
190- .byte BG_COLOUR, $30, $12, $0F ; Palette 2
191- .byte BG_COLOUR, $30, $14, $0F ; Palette 3
275+ .byte BG_COLOUR, $00, $10, $20
276+ .byte BG_COLOUR, $00, $10, $20
277+ .byte BG_COLOUR, $00, $10, $20
278+ .byte BG_COLOUR, $00, $10, $20
279+ .byte BG_COLOUR, $30, $20, $0F
280+ .byte BG_COLOUR, $30, $16, $0F
281+ .byte BG_COLOUR, $30, $12, $0F
282+ .byte BG_COLOUR, $30, $14, $0F
192283
193284 ; -----------------------------------------------------------------------------
194285 ; Vectors
195286 ; -----------------------------------------------------------------------------
196287 .segment "VECTORS"
197- .word nmi ; NMI vector
198- .word reset ; Reset vector
199- .word irq ; IRQ vector
288+ .word nmi
289+ .word reset
290+ .word irq
200291
201292 ; -----------------------------------------------------------------------------
202-; CHR-ROM (Graphics)
293+; CHR-ROM
203294 ; -----------------------------------------------------------------------------
204295 .segment "CHARS"
205296
206-; Tile 0: Empty (8x8 pixels, all zeros)
297+; Tile 0: Empty
207298 .byte $00,$00,$00,$00,$00,$00,$00,$00
208299 .byte $00,$00,$00,$00,$00,$00,$00,$00
209300
210-; Tile 1: Player sprite (simple arrow shape)
211-; Each tile is 16 bytes: 8 bytes low plane + 8 bytes high plane
212-; Colours: 00=transparent, 01=colour 1, 10=colour 2, 11=colour 3
213-.byte %00011000 ; Row 0
214-.byte %00111100 ; Row 1
215-.byte %01111110 ; Row 2
216-.byte %11111111 ; Row 3
217-.byte %00111100 ; Row 4
218-.byte %00111100 ; Row 5
219-.byte %00111100 ; Row 6
220-.byte %00111100 ; Row 7
221-; High plane (all zeros = use colour 1 only)
301+; Tile 1: Player sprite
302+.byte %00011000
303+.byte %00111100
304+.byte %01111110
305+.byte %11111111
306+.byte %00111100
307+.byte %00111100
308+.byte %00111100
309+.byte %00111100
222310 .byte %00000000
223311 .byte %00000000
224312 .byte %00000000
...
228316 .byte %00000000
229317 .byte %00000000
230318
231-; Fill rest of CHR-ROM with empty tiles
232319 .res 8192 - 32, $00
233320