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

Colour and Attributes

Use the attribute table to add colour regions to the arena.

9% of Neon Nexus

What You’re Building

A multi-coloured arena using the attribute table.

Colour Regions

The border uses one palette (blue). The floor uses another (purple). Same tiles, different colours.

The Attribute Table

The NES has 4 background palettes, but how does it know which palette to use for each tile? The attribute table.

The attribute table lives at $23C0-$23FF (64 bytes). Each byte controls a 32×32 pixel area (4×4 tiles):

Byte layout:
+----+----+
| TL | TR |  Bits 0-1: Top-left 16×16
+----+----+  Bits 2-3: Top-right 16×16
| BL | BR |  Bits 4-5: Bottom-left 16×16
+----+----+  Bits 6-7: Bottom-right 16×16

Each 2-bit value selects a palette (0-3).

Setting Attributes

set_attributes:
    bit PPUSTATUS
    lda #$23
    sta PPUADDR
    lda #$C0
    sta PPUADDR

    ; Top rows: all palette 0 (border)
    ldx #8
    lda #$00
@attr_top:
    sta PPUDATA
    dex
    bne @attr_top

    ; Floor rows: edges palette 0, middle palette 1
    ldx #6
@attr_floor_rows:
    lda #$00          ; Left edge
    sta PPUDATA
    lda #%01010101    ; Floor: all palette 1
    sta PPUDATA
    ; ... middle bytes ...
    lda #$00          ; Right edge
    sta PPUDATA
    dex
    bne @attr_floor_rows

Multiple Palettes

We now define distinct palettes:

palette_data:
    ; Palette 0: Blue (border)
    .byte BG_COLOUR, $11, $21, $31

    ; Palette 1: Purple (floor)
    .byte BG_COLOUR, $13, $23, $33

    ; Palette 2: Green (unused)
    .byte BG_COLOUR, $19, $29, $39

    ; Palette 3: Red (unused)
    .byte BG_COLOUR, $16, $26, $36

The floor tiles use the same graphics but palette 1 makes them purple instead of blue.

The Limitation

Attribute granularity is 16×16 pixels - you can’t change palette for individual 8×8 tiles. This is why NES games often have blocky colour boundaries.

Creative level design works around this constraint.

The Code

; =============================================================================
; NEON NEXUS - Unit 6: Colour and Attributes
; =============================================================================
; Use the attribute table to add colour regions to the arena.
; =============================================================================

; -----------------------------------------------------------------------------
; 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 (background)
TILE_EMPTY     = 0
TILE_BORDER    = 1
TILE_FLOOR     = 2
TILE_CORNER_TL = 3
TILE_CORNER_TR = 4
TILE_CORNER_BL = 5
TILE_CORNER_BR = 6

; Sprite tiles
SPRITE_PLAYER  = 7

; Arena boundaries
ARENA_LEFT   = 16
ARENA_RIGHT  = 232
ARENA_TOP    = 16
ARENA_BOTTOM = 208

BG_COLOUR = $0F

; -----------------------------------------------------------------------------
; Memory Layout
; -----------------------------------------------------------------------------
.segment "ZEROPAGE"
player_x:    .res 1
player_y:    .res 1
buttons:     .res 1
temp:        .res 1
row_counter: .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

    jsr load_palette
    jsr draw_arena
    jsr set_attributes

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

    lda #0
    sta PPUSCROLL
    sta PPUSCROLL

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

main_loop:
    jsr read_controller
    jsr move_player
    jmp main_loop

; -----------------------------------------------------------------------------
; Load Palette - Four distinct background palettes
; -----------------------------------------------------------------------------
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 (same as Unit 5)
; -----------------------------------------------------------------------------
draw_arena:
    bit PPUSTATUS
    lda #$20
    sta PPUADDR
    lda #$00
    sta PPUADDR

    lda #0
    sta row_counter

@draw_row:
    lda row_counter

    cmp #0
    beq @top_row
    cmp #1
    beq @top_row

    cmp #28
    beq @bottom_row
    cmp #29
    beq @bottom_row

    jmp @middle_row

@top_row:
    lda row_counter
    cmp #0
    bne @top_row_inner

    lda #TILE_CORNER_TL
    sta PPUDATA
    lda #TILE_BORDER
    ldx #30
@top_fill:
    sta PPUDATA
    dex
    bne @top_fill
    lda #TILE_CORNER_TR
    sta PPUDATA
    jmp @next_row

@top_row_inner:
    lda #TILE_BORDER
    ldx #32
@top_inner_fill:
    sta PPUDATA
    dex
    bne @top_inner_fill
    jmp @next_row

@bottom_row:
    lda row_counter
    cmp #29
    bne @bottom_row_inner

    lda #TILE_CORNER_BL
    sta PPUDATA
    lda #TILE_BORDER
    ldx #30
@bottom_fill:
    sta PPUDATA
    dex
    bne @bottom_fill
    lda #TILE_CORNER_BR
    sta PPUDATA
    jmp @next_row

@bottom_row_inner:
    lda #TILE_BORDER
    ldx #32
@bottom_inner_fill:
    sta PPUDATA
    dex
    bne @bottom_inner_fill
    jmp @next_row

@middle_row:
    lda #TILE_BORDER
    sta PPUDATA
    sta PPUDATA

    lda #TILE_FLOOR
    ldx #28
@floor_fill:
    sta PPUDATA
    dex
    bne @floor_fill

    lda #TILE_BORDER
    sta PPUDATA
    sta PPUDATA

@next_row:
    inc row_counter
    lda row_counter
    cmp #30
    beq @done_drawing
    jmp @draw_row

@done_drawing:
    rts

; -----------------------------------------------------------------------------
; Set Attribute Table - Different palettes for different regions
; -----------------------------------------------------------------------------
set_attributes:
    bit PPUSTATUS
    lda #$23
    sta PPUADDR
    lda #$C0
    sta PPUADDR

    ; Attribute table: 8 bytes per row, 8 rows = 64 bytes
    ; Each byte controls 4 16x16 pixel areas (32x32 total)
    ; Bits: 76=BR, 54=BL, 32=TR, 10=TL

    ; Row 0-1: All border (palette 0)
    ldx #8
    lda #$00          ; All palette 0
@attr_top:
    sta PPUDATA
    dex
    bne @attr_top

    ; Rows 2-5: Border edges (palette 0), floor varies
    ; Create a gradient effect: palette 1 in centre
    ldx #6            ; 6 rows of attributes for floor area
@attr_floor_rows:
    ; Byte 0: Left edge - palette 0
    lda #$00
    sta PPUDATA

    ; Bytes 1-6: Floor area with palette 1
    lda #%01010101    ; All palette 1
    sta PPUDATA
    sta PPUDATA
    sta PPUDATA
    sta PPUDATA
    sta PPUDATA
    sta PPUDATA

    ; Byte 7: Right edge - palette 0
    lda #$00
    sta PPUDATA

    dex
    bne @attr_floor_rows

    ; Row 7: Bottom border (palette 0)
    ldx #8
    lda #$00
@attr_bottom:
    sta PPUDATA
    dex
    bne @attr_bottom

    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:
    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

    lda #0
    sta OAMADDR
    lda #>oam_buffer
    sta OAMDMA

    lda player_y
    sta oam_buffer+0
    lda player_x
    sta oam_buffer+3

    lda #0
    sta PPUSCROLL
    sta PPUSCROLL

    pla
    tay
    pla
    tax
    pla
    rti

irq:
    rti

; -----------------------------------------------------------------------------
; Data
; -----------------------------------------------------------------------------
palette_data:
    ; Background palette 0: Blue border
    .byte BG_COLOUR, $11, $21, $31  ; Black, dark blue, light blue, cyan
    ; Background palette 1: Purple floor
    .byte BG_COLOUR, $13, $23, $33  ; Black, dark purple, purple, light purple
    ; Background palette 2: Green (unused for now)
    .byte BG_COLOUR, $19, $29, $39  ; Black, dark green, green, light green
    ; Background palette 3: Red (unused for now)
    .byte BG_COLOUR, $16, $26, $36  ; Black, dark red, red, light red
    ; Sprite palettes
    .byte BG_COLOUR, $30, $27, $17  ; White, orange, brown
    .byte BG_COLOUR, $30, $27, $17
    .byte BG_COLOUR, $30, $27, $17
    .byte BG_COLOUR, $30, $27, $17

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

; -----------------------------------------------------------------------------
; CHR-ROM - Same tiles as Unit 5
; -----------------------------------------------------------------------------
.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: Border (brick pattern)
.byte %11111111
.byte %10000001
.byte %10000001
.byte %11111111
.byte %11111111
.byte %00010001
.byte %00010001
.byte %11111111
.byte %00000000
.byte %01111110
.byte %01111110
.byte %00000000
.byte %00000000
.byte %11101110
.byte %11101110
.byte %00000000

; Tile 2: Floor (subtle grid)
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %10000001
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000

; Tile 3: Corner TL
.byte %11111111
.byte %11000000
.byte %10100000
.byte %10010000
.byte %10001000
.byte %10000100
.byte %10000010
.byte %10000001
.byte %00000000
.byte %00111111
.byte %01011111
.byte %01101111
.byte %01110111
.byte %01111011
.byte %01111101
.byte %01111110

; Tile 4: Corner TR
.byte %11111111
.byte %00000011
.byte %00000101
.byte %00001001
.byte %00010001
.byte %00100001
.byte %01000001
.byte %10000001
.byte %00000000
.byte %11111100
.byte %11111010
.byte %11110110
.byte %11101110
.byte %11011110
.byte %10111110
.byte %01111110

; Tile 5: Corner BL
.byte %10000001
.byte %10000010
.byte %10000100
.byte %10001000
.byte %10010000
.byte %10100000
.byte %11000000
.byte %11111111
.byte %01111110
.byte %01111101
.byte %01111011
.byte %01110111
.byte %01101111
.byte %01011111
.byte %00111111
.byte %00000000

; Tile 6: Corner BR
.byte %10000001
.byte %01000001
.byte %00100001
.byte %00010001
.byte %00001001
.byte %00000101
.byte %00000011
.byte %11111111
.byte %01111110
.byte %10111110
.byte %11011110
.byte %11101110
.byte %11110110
.byte %11111010
.byte %11111100
.byte %00000000

; Tile 7: Player sprite
.byte %00011000
.byte %00011000
.byte %00111100
.byte %01111110
.byte %11111111
.byte %10111101
.byte %00100100
.byte %00100100
.byte %00000000
.byte %00011000
.byte %00011000
.byte %00111100
.byte %01000010
.byte %01000010
.byte %00011000
.byte %00000000

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

Build It

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

The arena now has distinct colour regions. The border and floor are visually separate.

Next

The arena is complete. Unit 7 adds enemies to populate it.

What Changed

Unit 5 → Unit 6
+94-48
11 ; =============================================================================
2-; NEON NEXUS - Unit 5: Custom Tiles
2+; NEON NEXUS - Unit 6: Colour and Attributes
33 ; =============================================================================
4-; Design better tile graphics for the arena.
4+; Use the attribute table to add colour regions to the arena.
55 ; =============================================================================
66
77 ; -----------------------------------------------------------------------------
...
4040 TILE_EMPTY = 0
4141 TILE_BORDER = 1
4242 TILE_FLOOR = 2
43-TILE_CORNER_TL = 3 ; Top-left corner
44-TILE_CORNER_TR = 4 ; Top-right corner
45-TILE_CORNER_BL = 5 ; Bottom-left corner
46-TILE_CORNER_BR = 6 ; Bottom-right corner
43+TILE_CORNER_TL = 3
44+TILE_CORNER_TR = 4
45+TILE_CORNER_BL = 5
46+TILE_CORNER_BR = 6
4747
48-; Sprite tiles (after background tiles in pattern table 0)
48+; Sprite tiles
4949 SPRITE_PLAYER = 7
5050
5151 ; Arena boundaries
...
122122
123123 jsr load_palette
124124 jsr draw_arena
125+ jsr set_attributes
125126
126127 ; Set up player
127128 lda #PLAYER_START_X
...
151152 sta PPUSCROLL
152153 sta PPUSCROLL
153154
154- ; Enable rendering - both background and sprites from pattern table 0
155+ ; Enable rendering
155156 lda #%10000000
156157 sta PPUCTRL
157158 lda #%00011110
...
163164 jmp main_loop
164165
165166 ; -----------------------------------------------------------------------------
166-; Load Palette
167+; Load Palette - Four distinct background palettes
167168 ; -----------------------------------------------------------------------------
168169 load_palette:
169170 bit PPUSTATUS
...
182183 rts
183184
184185 ; -----------------------------------------------------------------------------
185-; Draw Arena (with corner tiles)
186+; Draw Arena (same as Unit 5)
186187 ; -----------------------------------------------------------------------------
187188 draw_arena:
188189 bit PPUSTATUS
...
197198 @draw_row:
198199 lda row_counter
199200
200- ; Row 0: top border with corners
201201 cmp #0
202202 beq @top_row
203203 cmp #1
204204 beq @top_row
205205
206- ; Row 28-29: bottom border with corners
207206 cmp #28
208207 beq @bottom_row
209208 cmp #29
210209 beq @bottom_row
211210
212- ; Middle rows
213211 jmp @middle_row
214212
215213 @top_row:
...
217215 cmp #0
218216 bne @top_row_inner
219217
220- ; Very top row with corners
221218 lda #TILE_CORNER_TL
222219 sta PPUDATA
223220 lda #TILE_BORDER
...
231228 jmp @next_row
232229
233230 @top_row_inner:
234- ; Second row - all border
235231 lda #TILE_BORDER
236232 ldx #32
237233 @top_inner_fill:
...
245241 cmp #29
246242 bne @bottom_row_inner
247243
248- ; Very bottom row with corners
249244 lda #TILE_CORNER_BL
250245 sta PPUDATA
251246 lda #TILE_BORDER
...
259254 jmp @next_row
260255
261256 @bottom_row_inner:
262- ; Row 28 - all border
263257 lda #TILE_BORDER
264258 ldx #32
265259 @bottom_inner_fill:
...
269263 jmp @next_row
270264
271265 @middle_row:
272- ; Left border
273266 lda #TILE_BORDER
274267 sta PPUDATA
275268 sta PPUDATA
276269
277- ; Floor
278270 lda #TILE_FLOOR
279271 ldx #28
280272 @floor_fill:
...
282274 dex
283275 bne @floor_fill
284276
285- ; Right border
286277 lda #TILE_BORDER
287278 sta PPUDATA
288279 sta PPUDATA
...
295286 jmp @draw_row
296287
297288 @done_drawing:
289+ rts
290+
291+; -----------------------------------------------------------------------------
292+; Set Attribute Table - Different palettes for different regions
293+; -----------------------------------------------------------------------------
294+set_attributes:
295+ bit PPUSTATUS
296+ lda #$23
297+ sta PPUADDR
298+ lda #$C0
299+ sta PPUADDR
300+
301+ ; Attribute table: 8 bytes per row, 8 rows = 64 bytes
302+ ; Each byte controls 4 16x16 pixel areas (32x32 total)
303+ ; Bits: 76=BR, 54=BL, 32=TR, 10=TL
304+
305+ ; Row 0-1: All border (palette 0)
306+ ldx #8
307+ lda #$00 ; All palette 0
308+@attr_top:
309+ sta PPUDATA
310+ dex
311+ bne @attr_top
312+
313+ ; Rows 2-5: Border edges (palette 0), floor varies
314+ ; Create a gradient effect: palette 1 in centre
315+ ldx #6 ; 6 rows of attributes for floor area
316+@attr_floor_rows:
317+ ; Byte 0: Left edge - palette 0
318+ lda #$00
319+ sta PPUDATA
320+
321+ ; Bytes 1-6: Floor area with palette 1
322+ lda #%01010101 ; All palette 1
323+ sta PPUDATA
324+ sta PPUDATA
325+ sta PPUDATA
326+ sta PPUDATA
327+ sta PPUDATA
328+ sta PPUDATA
329+
330+ ; Byte 7: Right edge - palette 0
331+ lda #$00
332+ sta PPUDATA
333+
334+ dex
335+ bne @attr_floor_rows
336+
337+ ; Row 7: Bottom border (palette 0)
338+ ldx #8
339+ lda #$00
340+@attr_bottom:
341+ sta PPUDATA
342+ dex
343+ bne @attr_bottom
344+
298345 rts
299346
300347 ; -----------------------------------------------------------------------------
...
401448 ; Data
402449 ; -----------------------------------------------------------------------------
403450 palette_data:
404- ; Background palettes - neon theme
405- .byte BG_COLOUR, $11, $21, $31 ; Blue gradient
406- .byte BG_COLOUR, $11, $21, $31
407- .byte BG_COLOUR, $11, $21, $31
408- .byte BG_COLOUR, $11, $21, $31
451+ ; Background palette 0: Blue border
452+ .byte BG_COLOUR, $11, $21, $31 ; Black, dark blue, light blue, cyan
453+ ; Background palette 1: Purple floor
454+ .byte BG_COLOUR, $13, $23, $33 ; Black, dark purple, purple, light purple
455+ ; Background palette 2: Green (unused for now)
456+ .byte BG_COLOUR, $19, $29, $39 ; Black, dark green, green, light green
457+ ; Background palette 3: Red (unused for now)
458+ .byte BG_COLOUR, $16, $26, $36 ; Black, dark red, red, light red
409459 ; Sprite palettes
410460 .byte BG_COLOUR, $30, $27, $17 ; White, orange, brown
411461 .byte BG_COLOUR, $30, $27, $17
...
421471 .word irq
422472
423473 ; -----------------------------------------------------------------------------
424-; CHR-ROM - Background Tiles (Pattern Table 0)
474+; CHR-ROM - Same tiles as Unit 5
425475 ; -----------------------------------------------------------------------------
426476 .segment "CHARS"
427477
...
430480 .byte $00,$00,$00,$00,$00,$00,$00,$00
431481
432482 ; Tile 1: Border (brick pattern)
433-.byte %11111111 ; Row 0
434-.byte %10000001 ; Row 1
435-.byte %10000001 ; Row 2
436-.byte %11111111 ; Row 3
437-.byte %11111111 ; Row 4
438-.byte %00010001 ; Row 5
439-.byte %00010001 ; Row 6
440-.byte %11111111 ; Row 7
441-; High plane - add colour variation
483+.byte %11111111
484+.byte %10000001
485+.byte %10000001
486+.byte %11111111
487+.byte %11111111
488+.byte %00010001
489+.byte %00010001
490+.byte %11111111
442491 .byte %00000000
443492 .byte %01111110
444493 .byte %01111110
...
457506 .byte %00000000
458507 .byte %00000000
459508 .byte %10000001
460-; High plane
461509 .byte %00000000
462510 .byte %00000000
463511 .byte %00000000
...
539587 .byte %11111100
540588 .byte %00000000
541589
542-; Sprite 0: Player (better ship design) - at tile index 0 in sprite table
543-; This goes immediately after background tiles
544-.byte %00011000 ; Row 0
545-.byte %00011000 ; Row 1
546-.byte %00111100 ; Row 2
547-.byte %01111110 ; Row 3
548-.byte %11111111 ; Row 4
549-.byte %10111101 ; Row 5
550-.byte %00100100 ; Row 6
551-.byte %00100100 ; Row 7
552-; High plane
590+; Tile 7: Player sprite
591+.byte %00011000
592+.byte %00011000
593+.byte %00111100
594+.byte %01111110
595+.byte %11111111
596+.byte %10111101
597+.byte %00100100
598+.byte %00100100
553599 .byte %00000000
554600 .byte %00011000
555601 .byte %00011000
...
559605 .byte %00011000
560606 .byte %00000000
561607
562-; Fill rest of CHR-ROM (8KB total = 8192 bytes, used 128 so far)
608+; Fill rest of CHR-ROM
563609 .res 8192 - 128, $00
564610