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

The Background

Draw a bordered arena using the nametable.

6% of Neon Nexus

What You’re Building

A bordered play area. The player is now confined to an arena.

The Arena

This is the game world taking shape.

The Nametable

The NES background is a 32×30 grid of tile indices. Each byte in the nametable references a tile from the pattern table.

  • Nametable starts at $2000
  • 960 bytes total (32 × 30)
  • Each byte = one 8×8 tile

To draw the arena, we fill this grid with border and floor tiles.

Drawing the Arena

draw_arena:
    ; Set PPU address to nametable start
    lda #$20
    sta PPUADDR
    lda #$00
    sta PPUADDR

    ; Draw 30 rows
    ldy #0

@draw_row:
    ; First/last 2 rows are all border
    cpy #0
    beq @border_row
    cpy #1
    beq @border_row
    cpy #28
    beq @border_row
    cpy #29
    beq @border_row

    ; Middle rows: border + floor + border
    jmp @middle_row

The PPU address auto-increments after each write to PPUDATA. Write 960 tiles and you’ve filled the screen.

Tile Layout

We define three tiles in CHR-ROM:

TilePurposeAppearance
0EmptyTransparent
1BorderSolid cyan
2FloorBlack
3PlayerArrow sprite

The Code

; =============================================================================
; NEON NEXUS - Unit 4: The Background
; =============================================================================
; Add a bordered arena using the nametable.
; =============================================================================

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

JOYPAD1   = $4016
JOYPAD2   = $4017

; Controller buttons
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_SPEED   = 2

; Tile indices
TILE_EMPTY  = 0
TILE_BORDER = 1
TILE_FLOOR  = 2
TILE_PLAYER = 3

; Arena boundaries (in pixels, accounting for border)
ARENA_LEFT   = 16    ; 2 border tiles × 8
ARENA_RIGHT  = 232   ; 256 - 16 - 8 (sprite width)
ARENA_TOP    = 16
ARENA_BOTTOM = 208   ; 240 - 16 - 16 (overscan + border)

; Palette
BG_COLOUR = $0F      ; Black background

; -----------------------------------------------------------------------------
; Memory Layout
; -----------------------------------------------------------------------------
.segment "ZEROPAGE"
player_x:    .res 1
player_y:    .res 1
buttons:     .res 1
temp:        .res 1

.segment "OAM"
oam_buffer:  .res 256

.segment "BSS"

; -----------------------------------------------------------------------------
; iNES Header
; -----------------------------------------------------------------------------
.segment "HEADER"
    .byte "NES", $1A
    .byte 2
    .byte 1
    .byte $01
    .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
    jsr load_palette

    ; Draw the arena
    jsr draw_arena

    ; 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 #TILE_PLAYER
    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

    ; Reset scroll
    lda #0
    sta PPUSCROLL
    sta PPUSCROLL

    ; Enable rendering
    lda #%10000000    ; NMI on, background and sprites from pattern table 0
    sta PPUCTRL
    lda #%00011110    ; Show sprites and background
    sta PPUMASK

; === Main Loop ===
main_loop:
    jsr read_controller
    jsr move_player
    jmp main_loop

; -----------------------------------------------------------------------------
; Load Palette
; -----------------------------------------------------------------------------
load_palette:
    bit PPUSTATUS
    lda #$3F
    sta PPUADDR
    lda #$00
    sta PPUADDR

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

; -----------------------------------------------------------------------------
; Draw Arena
; -----------------------------------------------------------------------------
draw_arena:
    ; Set PPU address to start of nametable ($2000)
    bit PPUSTATUS
    lda #$20
    sta PPUADDR
    lda #$00
    sta PPUADDR

    ; Draw 30 rows
    ldy #0              ; Row counter

@draw_row:
    ; Check if first row, last row, or middle
    cpy #0
    beq @border_row
    cpy #1
    beq @border_row
    cpy #28
    beq @border_row
    cpy #29
    beq @border_row

    ; Middle row: border + floor + border
    jmp @middle_row

@border_row:
    ; All border tiles
    ldx #32
@border_loop:
    lda #TILE_BORDER
    sta PPUDATA
    dex
    bne @border_loop
    jmp @next_row

@middle_row:
    ; Left border (2 tiles)
    lda #TILE_BORDER
    sta PPUDATA
    sta PPUDATA

    ; Floor (28 tiles)
    ldx #28
@floor_loop:
    lda #TILE_FLOOR
    sta PPUDATA
    dex
    bne @floor_loop

    ; Right border (2 tiles)
    lda #TILE_BORDER
    sta PPUDATA
    sta PPUDATA

@next_row:
    iny
    cpy #30
    bne @draw_row

    rts

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

    ldx #8
@read_loop:
    lda JOYPAD1
    lsr a
    rol buttons
    dex
    bne @read_loop
    rts

; -----------------------------------------------------------------------------
; Move Player
; -----------------------------------------------------------------------------
move_player:
    ; UP
    lda buttons
    and #BTN_UP
    beq @check_down
    lda player_y
    sec
    sbc #PLAYER_SPEED
    cmp #ARENA_TOP
    bcc @check_down
    sta player_y

@check_down:
    lda buttons
    and #BTN_DOWN
    beq @check_left
    lda player_y
    clc
    adc #PLAYER_SPEED
    cmp #ARENA_BOTTOM
    bcs @check_left
    sta player_y

@check_left:
    lda buttons
    and #BTN_LEFT
    beq @check_right
    lda player_x
    sec
    sbc #PLAYER_SPEED
    cmp #ARENA_LEFT
    bcc @check_right
    sta player_x

@check_right:
    lda buttons
    and #BTN_RIGHT
    beq @done
    lda player_x
    clc
    adc #PLAYER_SPEED
    cmp #ARENA_RIGHT
    bcs @done
    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 position
    lda player_y
    sta oam_buffer+0
    lda player_x
    sta oam_buffer+3

    ; Reset scroll (important after any PPU access)
    lda #0
    sta PPUSCROLL
    sta PPUSCROLL

    pla
    tay
    pla
    tax
    pla
    rti

irq:
    rti

; -----------------------------------------------------------------------------
; Data
; -----------------------------------------------------------------------------
palette_data:
    ; Background palettes
    .byte BG_COLOUR, $12, $21, $30  ; Palette 0: black, blue, cyan (border), white
    .byte BG_COLOUR, $12, $21, $30  ; Palette 1
    .byte BG_COLOUR, $12, $21, $30  ; Palette 2
    .byte BG_COLOUR, $12, $21, $30  ; Palette 3
    ; Sprite palettes
    .byte BG_COLOUR, $30, $20, $16  ; Palette 0: black, white, red, orange
    .byte BG_COLOUR, $30, $20, $16  ; Palette 1
    .byte BG_COLOUR, $30, $20, $16  ; Palette 2
    .byte BG_COLOUR, $30, $20, $16  ; Palette 3

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

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

; Tile 0: Empty (transparent)
.byte $00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00

; Tile 1: Border (solid cyan block)
.byte $00,$00,$00,$00,$00,$00,$00,$00  ; Low plane = 0
.byte $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF  ; High plane = 1 (colour 2 = cyan)

; Tile 2: Floor (empty/dark)
.byte $00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00

; Tile 3: Player sprite (arrow)
.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

; Fill rest of CHR-ROM
.res 8192 - 64, $00

Build It

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

Move with the D-pad. The player now bounces off the arena walls.

Updated Boundaries

The movement code now uses arena boundaries instead of screen edges:

ARENA_LEFT   = 16    ; 2 border tiles × 8 pixels
ARENA_RIGHT  = 232
ARENA_TOP    = 16
ARENA_BOTTOM = 208

The player can’t escape the play area.

Next

The arena works but looks plain. Unit 5 adds custom tile graphics.

What Changed

Unit 3 → Unit 4
+158-67
11 ; =============================================================================
2-; NEON NEXUS - Unit 2: Moving the Player
2+; NEON NEXUS - Unit 4: The Background
33 ; =============================================================================
4-; The player sprite now moves with the D-pad.
5-; Press Up, Down, Left, Right to move around the screen.
4+; Add a bordered arena using the nametable.
65 ; =============================================================================
76
87 ; -----------------------------------------------------------------------------
...
1211 PPUMASK = $2001
1312 PPUSTATUS = $2002
1413 OAMADDR = $2003
14+PPUSCROLL = $2005
1515 PPUADDR = $2006
1616 PPUDATA = $2007
1717 OAMDMA = $4014
1818
19-JOYPAD1 = $4016 ; Controller 1
20-JOYPAD2 = $4017 ; Controller 2
19+JOYPAD1 = $4016
20+JOYPAD2 = $4017
2121
22-; Controller button masks
22+; Controller buttons
2323 BTN_A = %10000000
2424 BTN_B = %01000000
2525 BTN_SELECT = %00100000
...
3434 ; -----------------------------------------------------------------------------
3535 PLAYER_START_X = 124
3636 PLAYER_START_Y = 116
37-PLAYER_TILE = 1
38-BG_COLOUR = $12
37+PLAYER_SPEED = 2
3938
40-PLAYER_SPEED = 2 ; Pixels per frame (try 1, 2, or 3)
39+; Tile indices
40+TILE_EMPTY = 0
41+TILE_BORDER = 1
42+TILE_FLOOR = 2
43+TILE_PLAYER = 3
4144
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)
45+; Arena boundaries (in pixels, accounting for border)
46+ARENA_LEFT = 16 ; 2 border tiles × 8
47+ARENA_RIGHT = 232 ; 256 - 16 - 8 (sprite width)
48+ARENA_TOP = 16
49+ARENA_BOTTOM = 208 ; 240 - 16 - 16 (overscan + border)
50+
51+; Palette
52+BG_COLOUR = $0F ; Black background
4753
4854 ; -----------------------------------------------------------------------------
4955 ; Memory Layout
...
5157 .segment "ZEROPAGE"
5258 player_x: .res 1
5359 player_y: .res 1
54-buttons: .res 1 ; Current button state
60+buttons: .res 1
61+temp: .res 1
5562
5663 .segment "OAM"
5764 oam_buffer: .res 256
...
6370 ; -----------------------------------------------------------------------------
6471 .segment "HEADER"
6572 .byte "NES", $1A
66- .byte 2 ; 32KB PRG-ROM
67- .byte 1 ; 8KB CHR-ROM
68- .byte $01 ; Mapper 0, vertical mirroring
73+ .byte 2
74+ .byte 1
75+ .byte $01
6976 .byte $00
7077 .byte 0,0,0,0,0,0,0,0
7178
...
108115 bpl @vblank2
109116
110117 ; Load palette
111- bit PPUSTATUS
112- lda #$3F
113- sta PPUADDR
114- lda #$00
115- sta PPUADDR
118+ jsr load_palette
116119
117- ldx #0
118-@load_palette:
119- lda palette_data, x
120- sta PPUDATA
121- inx
122- cpx #32
123- bne @load_palette
120+ ; Draw the arena
121+ jsr draw_arena
124122
125123 ; Set up player
126124 lda #PLAYER_START_X
...
131129 ; Initialise player sprite
132130 lda player_y
133131 sta oam_buffer+0
134- lda #PLAYER_TILE
132+ lda #TILE_PLAYER
135133 sta oam_buffer+1
136134 lda #0
137135 sta oam_buffer+2
...
145143 sta oam_buffer, x
146144 inx
147145 bne @hide_sprites
146+
147+ ; Reset scroll
148+ lda #0
149+ sta PPUSCROLL
150+ sta PPUSCROLL
148151
149152 ; Enable rendering
150- lda #%10010000
153+ lda #%10000000 ; NMI on, background and sprites from pattern table 0
151154 sta PPUCTRL
152- lda #%00011110
155+ lda #%00011110 ; Show sprites and background
153156 sta PPUMASK
154157
155158 ; === Main Loop ===
156159 main_loop:
157- ; Read controller
158160 jsr read_controller
159-
160- ; Move player based on input
161161 jsr move_player
162-
163162 jmp main_loop
163+
164+; -----------------------------------------------------------------------------
165+; Load Palette
166+; -----------------------------------------------------------------------------
167+load_palette:
168+ bit PPUSTATUS
169+ lda #$3F
170+ sta PPUADDR
171+ lda #$00
172+ sta PPUADDR
173+
174+ ldx #0
175+@loop:
176+ lda palette_data, x
177+ sta PPUDATA
178+ inx
179+ cpx #32
180+ bne @loop
181+ rts
182+
183+; -----------------------------------------------------------------------------
184+; Draw Arena
185+; -----------------------------------------------------------------------------
186+draw_arena:
187+ ; Set PPU address to start of nametable ($2000)
188+ bit PPUSTATUS
189+ lda #$20
190+ sta PPUADDR
191+ lda #$00
192+ sta PPUADDR
193+
194+ ; Draw 30 rows
195+ ldy #0 ; Row counter
196+
197+@draw_row:
198+ ; Check if first row, last row, or middle
199+ cpy #0
200+ beq @border_row
201+ cpy #1
202+ beq @border_row
203+ cpy #28
204+ beq @border_row
205+ cpy #29
206+ beq @border_row
207+
208+ ; Middle row: border + floor + border
209+ jmp @middle_row
210+
211+@border_row:
212+ ; All border tiles
213+ ldx #32
214+@border_loop:
215+ lda #TILE_BORDER
216+ sta PPUDATA
217+ dex
218+ bne @border_loop
219+ jmp @next_row
220+
221+@middle_row:
222+ ; Left border (2 tiles)
223+ lda #TILE_BORDER
224+ sta PPUDATA
225+ sta PPUDATA
226+
227+ ; Floor (28 tiles)
228+ ldx #28
229+@floor_loop:
230+ lda #TILE_FLOOR
231+ sta PPUDATA
232+ dex
233+ bne @floor_loop
234+
235+ ; Right border (2 tiles)
236+ lda #TILE_BORDER
237+ sta PPUDATA
238+ sta PPUDATA
239+
240+@next_row:
241+ iny
242+ cpy #30
243+ bne @draw_row
244+
245+ rts
164246
165247 ; -----------------------------------------------------------------------------
166248 ; Read Controller
167249 ; -----------------------------------------------------------------------------
168250 read_controller:
169- ; Strobe the controller
170251 lda #1
171252 sta JOYPAD1
172253 lda #0
173254 sta JOYPAD1
174255
175- ; Read 8 buttons into 'buttons' variable
176256 ldx #8
177257 @read_loop:
178258 lda JOYPAD1
179- lsr a ; Bit 0 -> Carry
180- rol buttons ; Carry -> buttons
259+ lsr a
260+ rol buttons
181261 dex
182262 bne @read_loop
183-
184263 rts
185264
186265 ; -----------------------------------------------------------------------------
187266 ; Move Player
188267 ; -----------------------------------------------------------------------------
189268 move_player:
190- ; Check UP
269+ ; UP
191270 lda buttons
192271 and #BTN_UP
193272 beq @check_down
194-
195273 lda player_y
196274 sec
197275 sbc #PLAYER_SPEED
198- cmp #SCREEN_TOP
199- bcc @check_down ; Don't go above top
276+ cmp #ARENA_TOP
277+ bcc @check_down
200278 sta player_y
201279
202280 @check_down:
203281 lda buttons
204282 and #BTN_DOWN
205283 beq @check_left
206-
207284 lda player_y
208285 clc
209286 adc #PLAYER_SPEED
210- cmp #SCREEN_BOTTOM
211- bcs @check_left ; Don't go below bottom
287+ cmp #ARENA_BOTTOM
288+ bcs @check_left
212289 sta player_y
213290
214291 @check_left:
215292 lda buttons
216293 and #BTN_LEFT
217294 beq @check_right
218-
219295 lda player_x
220296 sec
221297 sbc #PLAYER_SPEED
222- cmp #SCREEN_LEFT
223- bcc @check_right ; Don't go past left edge
298+ cmp #ARENA_LEFT
299+ bcc @check_right
224300 sta player_x
225301
226302 @check_right:
227303 lda buttons
228304 and #BTN_RIGHT
229305 beq @done
230-
231306 lda player_x
232307 clc
233308 adc #PLAYER_SPEED
234- cmp #SCREEN_RIGHT
235- bcs @done ; Don't go past right edge
309+ cmp #ARENA_RIGHT
310+ bcs @done
236311 sta player_x
237312
238313 @done:
...
252327 lda #>oam_buffer
253328 sta OAMDMA
254329
255- ; Update sprite from player position
330+ ; Update sprite position
256331 lda player_y
257332 sta oam_buffer+0
258333 lda player_x
259334 sta oam_buffer+3
335+
336+ ; Reset scroll (important after any PPU access)
337+ lda #0
338+ sta PPUSCROLL
339+ sta PPUSCROLL
260340
261341 pla
262342 tay
...
272352 ; Data
273353 ; -----------------------------------------------------------------------------
274354 palette_data:
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
355+ ; Background palettes
356+ .byte BG_COLOUR, $12, $21, $30 ; Palette 0: black, blue, cyan (border), white
357+ .byte BG_COLOUR, $12, $21, $30 ; Palette 1
358+ .byte BG_COLOUR, $12, $21, $30 ; Palette 2
359+ .byte BG_COLOUR, $12, $21, $30 ; Palette 3
360+ ; Sprite palettes
361+ .byte BG_COLOUR, $30, $20, $16 ; Palette 0: black, white, red, orange
362+ .byte BG_COLOUR, $30, $20, $16 ; Palette 1
363+ .byte BG_COLOUR, $30, $20, $16 ; Palette 2
364+ .byte BG_COLOUR, $30, $20, $16 ; Palette 3
283365
284366 ; -----------------------------------------------------------------------------
285367 ; Vectors
...
294376 ; -----------------------------------------------------------------------------
295377 .segment "CHARS"
296378
297-; Tile 0: Empty
379+; Tile 0: Empty (transparent)
298380 .byte $00,$00,$00,$00,$00,$00,$00,$00
299381 .byte $00,$00,$00,$00,$00,$00,$00,$00
300382
301-; Tile 1: Player sprite
383+; Tile 1: Border (solid cyan block)
384+.byte $00,$00,$00,$00,$00,$00,$00,$00 ; Low plane = 0
385+.byte $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF ; High plane = 1 (colour 2 = cyan)
386+
387+; Tile 2: Floor (empty/dark)
388+.byte $00,$00,$00,$00,$00,$00,$00,$00
389+.byte $00,$00,$00,$00,$00,$00,$00,$00
390+
391+; Tile 3: Player sprite (arrow)
302392 .byte %00011000
303393 .byte %00111100
304394 .byte %01111110
...
316406 .byte %00000000
317407 .byte %00000000
318408
319-.res 8192 - 32, $00
409+; Fill rest of CHR-ROM
410+.res 8192 - 64, $00
320411