Skip to content
Game 1 Unit 1 of 16 1 hr learning time

Ship on Screen

Put a hardware sprite on screen with the VIC-II — the first piece of your first C64 game, built from a blank screen up.

6% of Starfield

A white spaceship against black. Nothing moves, nothing makes sound — it just sits there. That's the goal of this unit, and it's the first piece of your first C64 game.

In the Primer you met the VIC-II through its border and background colours, wrote bytes straight into screen memory, and launched machine code with a SYS stub. This unit uses all of that — LDA/STA, memory-mapped registers, the 10 SYS 2061 stub — and adds the one new thing a game needs: a hardware sprite, a movable graphic the VIC-II draws for you, on top of the screen, wherever you tell it.

We'll get there from a blank screen, in two steps.

Where we start

A freshly booted C64, the way the Primer left it: the start-up message, and READY. Type RUN and our bare program does nothing yet — there's nothing on screen but BASIC.

A freshly booted Commodore 64 screen — light-blue border, dark-blue background, the start-up message and READY prompt, with RUN typed below.

Milestone 1 — a black canvas

Before the ship, we clear the stage: border and background to black, and the BASIC text wiped out of screen memory. This is the Primer's screen work — STA $d020/$d021 for the colours, a loop writing spaces across the 1,000 bytes of screen RAM at $0400.

Step 1: clear to a black screen
+16-1
99 !byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00
1010
1111 ; ------------------------------------------------
12-; Program — nothing yet, just hand control to a do-nothing loop
12+; Program
1313 ; ------------------------------------------------
1414 *= $080d
15+ ; Black screen
16+ lda #$00
17+ sta $d020 ; Border colour
18+ sta $d021 ; Background colour
19+
20+ ; Clear the screen (fill with spaces)
21+ ldx #$00
22+- lda #$20
23+ sta $0400,x
24+ sta $0500,x
25+ sta $0600,x
26+ sta $0700,x
27+ inx
28+ bne -
29+
1530 loop:
1631 jmp loop
1732
The complete step 1 program
; Starfield - Unit 1: Ship on Screen
; Cumulative steps: step-00 (bare program) -> step-01 (+ black screen) -> step-02 (+ the ship)
; Assemble: acme -f cbm -o <step>.prg <step>.asm

; ------------------------------------------------
; BASIC stub
; ------------------------------------------------
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00

; ------------------------------------------------
; Program
; ------------------------------------------------
*= $080d
        ; Black screen
        lda #$00
        sta $d020           ; Border colour
        sta $d021           ; Background colour

        ; Clear the screen (fill with spaces)
        ldx #$00
-       lda #$20
        sta $0400,x
        sta $0500,x
        sta $0600,x
        sta $0700,x
        inx
        bne -

loop:
        jmp loop

The four STA writes set the two colours; the loop counts the X register from 0, writing a space ($20) into each page of screen memory until INX wraps it back to 0 and BNE stops looping. The result is a clean black canvas:

A completely black screen — border, background, and text all cleared away.

Milestone 2 — the ship

Now the new part. A hardware sprite needs four things from the VIC-II, plus its picture in memory.

Step 2: define and switch on sprite 0
+38
2626 sta $0700,x
2727 inx
2828 bne -
29+
30+ ; Sprite 0 setup (ship)
31+ lda #128
32+ sta $07f8 ; Data pointer (block 128 = $2000)
33+ lda #172
34+ sta $d000 ; X position
35+ lda #220
36+ sta $d001 ; Y position
37+ lda #$01
38+ sta $d027 ; Colour (white)
39+ lda #%00000001
40+ sta $d015 ; Enable sprite 0
2941
3042 loop:
3143 jmp loop
44+
45+; ------------------------------------------------
46+; Sprite data at $2000 (block 128)
47+; ------------------------------------------------
48+*= $2000
49+ !byte $00,$18,$00 ; ##
50+ !byte $00,$3c,$00 ; ####
51+ !byte $00,$3c,$00 ; ####
52+ !byte $00,$7e,$00 ; ######
53+ !byte $00,$7e,$00 ; ######
54+ !byte $00,$ff,$00 ; ########
55+ !byte $00,$ff,$00 ; ########
56+ !byte $01,$ff,$80 ; ##########
57+ !byte $03,$ff,$c0 ; ############
58+ !byte $07,$ff,$e0 ; ##############
59+ !byte $07,$ff,$e0 ; ##############
60+ !byte $07,$e7,$e0 ; ###..####..###
61+ !byte $03,$c3,$c0 ; ##....##....##
62+ !byte $01,$ff,$80 ; ##########
63+ !byte $00,$ff,$00 ; ########
64+ !byte $00,$ff,$00 ; ########
65+ !byte $00,$db,$00 ; ##.##.##
66+ !byte $00,$db,$00 ; ##.##.##
67+ !byte $00,$66,$00 ; ##..##
68+ !byte $00,$24,$00 ; #..#
69+ !byte $00,$00,$00 ;
3270
The complete program
; Starfield - Unit 1: Ship on Screen
; Cumulative steps: step-00 (bare program) -> step-01 (+ black screen) -> step-02 (+ the ship)
; Assemble: acme -f cbm -o <step>.prg <step>.asm

; ------------------------------------------------
; BASIC stub
; ------------------------------------------------
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00

; ------------------------------------------------
; Program
; ------------------------------------------------
*= $080d
        ; Black screen
        lda #$00
        sta $d020           ; Border colour
        sta $d021           ; Background colour

        ; Clear the screen (fill with spaces)
        ldx #$00
-       lda #$20
        sta $0400,x
        sta $0500,x
        sta $0600,x
        sta $0700,x
        inx
        bne -

        ; Sprite 0 setup (ship)
        lda #128
        sta $07f8           ; Data pointer (block 128 = $2000)
        lda #172
        sta $d000           ; X position
        lda #220
        sta $d001           ; Y position
        lda #$01
        sta $d027           ; Colour (white)
        lda #%00000001
        sta $d015           ; Enable sprite 0

loop:
        jmp loop

; ------------------------------------------------
; Sprite data at $2000 (block 128)
; ------------------------------------------------
*= $2000
        !byte $00,$18,$00   ;        ##
        !byte $00,$3c,$00   ;       ####
        !byte $00,$3c,$00   ;       ####
        !byte $00,$7e,$00   ;      ######
        !byte $00,$7e,$00   ;      ######
        !byte $00,$ff,$00   ;     ########
        !byte $00,$ff,$00   ;     ########
        !byte $01,$ff,$80   ;    ##########
        !byte $03,$ff,$c0   ;   ############
        !byte $07,$ff,$e0   ;  ##############
        !byte $07,$ff,$e0   ;  ##############
        !byte $07,$e7,$e0   ;  ###..####..###
        !byte $03,$c3,$c0   ;   ##....##....##
        !byte $01,$ff,$80   ;    ##########
        !byte $00,$ff,$00   ;     ########
        !byte $00,$ff,$00   ;     ########
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$66,$00   ;      ##..##
        !byte $00,$24,$00   ;       #..#
        !byte $00,$00,$00   ;

And there it is:

A single white spaceship sprite — a pointed hull with flared wings — near the bottom centre of an otherwise black screen.

The four registers

  1. Where's the picture? $07f8 holds a block number — the sprite's data divided by 64. Block 128 means address $2000 (128 × 64 = 8192 = $2000).
  2. Where on screen? $d000 (X) and $d001 (Y) place it. X runs left-to-right, Y top-to-bottom.
  3. What colour? $d027 sets sprite 0's colour. White is 1.
  4. Is it on? $d015 is the sprite-enable register — one bit per sprite. Set bit 0 to switch sprite 0 on.
$D015 — Sprite Enable 7 Spr 7 0 6 Spr 6 0 5 Spr 5 0 4 Spr 4 0 3 Spr 3 0 2 Spr 2 0 1 Spr 1 0 0 Spr 0 1
Setting bit 0 switches on sprite 0 — the only sprite this unit uses.

The sprite's picture

A C64 sprite is 24 pixels wide and 21 tall. Each row is 3 bytes (24 bits), 63 bytes in all, padded to 64 — which is why sprite data sits on a 64-byte boundary. Each bit is one pixel: a 1 draws in the sprite's colour, a 0 is transparent. Bit 7 of the first byte is the top-left pixel.

$00 $18 $00 $00 $3C $00 $00 $3C $00 $00 $7E $00 $00 $7E $00 $00 $FF $00 $00 $FF $00 $01 $FF $80 $03 $FF $C0 $07 $FF $E0 $07 $FF $E0 $07 $E7 $E0 $03 $C3 $C0 $01 $FF $80 $00 $FF $00 $00 $FF $00 $00 $DB $00 $00 $DB $00 $00 $66 $00 $00 $24 $00 $00 $00 $00
The ship — each filled cell is a 1 bit, each dark cell a 0. Blue lines mark the byte boundaries; read each row's three bytes against the hull.

Those bytes are the !byte rows at $2000 in the diff above. Here is everything together in memory:

$D000 $D02E VIC-II registers Sprites, colours, screen $2040 $2000 $203F Sprite data 64 bytes (block 128) $0900 $080D $08FF Program code $0801 $080C BASIC stub SYS 2061 $0800 $07F8 $07FF Sprite pointers Block numbers $07E8 $0400 $07E7 Screen RAM 40×25 characters
The memory the program touches. Addresses are not to scale.

When it's wrong, see why

A sprite that won't appear has a short list of causes, and they're all values you can check — re-read them in your code, and if your emulator has a monitor, watch them live. Run through them in order:

  • $d015 — is bit 0 set? $00 means every sprite is off, and you'll stare at a blank screen.
  • $07f8 — does the pointer match where the data is? 128 for data at $2000. A wrong pointer draws garbage or nothing.
  • $d000/$d001 — is the sprite on the visible screen? A Y of 0 or an X past the right edge hides it in the border.
  • $d027 — is the colour different from the background? A black sprite on black is there, just invisible.

Checking the actual values turns "nothing's showing" from a guessing game into a four-line checklist.

Before and after

We started at the machine's boot screen with nothing of ours on it, and finished with a sprite we placed and coloured ourselves — the first thing on screen that's truly yours.

Try this

  • Change the colour. Swap the $01 written to $d027 for any value 0–15: $02 red, $03 cyan, $05 green, $07 yellow, $0e light blue. Reassemble and the ship changes instantly.
  • Move the ship. Change the values written to $d000 (X) and $d001 (Y). X only reaches 255, but the screen is 320 wide — reaching the right edge needs an extra register ($d010), which is exactly what Unit 3 is about.
  • Draw your own. Click pixels below; the bytes update as you draw. Paste them over the !byte rows.
Output
Click to draw; copy the bytes into your sprite data section.

What you've learnt

  • Hardware sprites — the VIC-II draws a movable graphic for you, over the screen, wherever its registers say.
  • The four sprite registers — data pointer ($07f8), position ($d000/$d001), colour ($d027), and enable ($d015).
  • Sprite data format — 24 × 21 pixels, 3 bytes a row, on a 64-byte boundary; one bit per pixel.
  • Debugging by register — a missing sprite is one of four registers being wrong, and you can read each one.

What's next

The ship sits still. In Unit 2 you'll wrap it in a game loop and read the joystick, so pushing the stick moves it around the screen.