Skip to content

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.

  • Commodore 64
  • Assembly

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 added−1 removed
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00

; ------------------------------------------------
Removed: ; Program — nothing yet, just hand control to a do-nothing loop
Added: ; Program
; ------------------------------------------------
*= $080d
Added:         ; Black screen
Added:         lda #$00
Added:         sta $d020           ; Border colour
Added:         sta $d021           ; Background colour
Added: 
Added:         ; Clear the screen (fill with spaces)
Added:         ldx #$00
Added: -       lda #$20
Added:         sta $0400,x
Added:         sta $0500,x
Added:         sta $0600,x
Added:         sta $0700,x
Added:         inx
Added:         bne -
Added: 
loop:
        jmp loop
The complete step 1 program
step-01.asm
; 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 added
        sta $0700,x
        inx
        bne -
Added: 
Added:         ; Sprite 0 setup (ship)
Added:         lda #128
Added:         sta $07f8           ; Data pointer (block 128 = $2000)
Added:         lda #172
Added:         sta $d000           ; X position
Added:         lda #220
Added:         sta $d001           ; Y position
Added:         lda #$01
Added:         sta $d027           ; Colour (white)
Added:         lda #%00000001
Added:         sta $d015           ; Enable sprite 0

loop:
        jmp loop
Added: 
Added: ; ------------------------------------------------
Added: ; Sprite data at $2000 (block 128)
Added: ; ------------------------------------------------
Added: *= $2000
Added:         !byte $00,$18,$00   ;        ##
Added:         !byte $00,$3c,$00   ;       ####
Added:         !byte $00,$3c,$00   ;       ####
Added:         !byte $00,$7e,$00   ;      ######
Added:         !byte $00,$7e,$00   ;      ######
Added:         !byte $00,$ff,$00   ;     ########
Added:         !byte $00,$ff,$00   ;     ########
Added:         !byte $01,$ff,$80   ;    ##########
Added:         !byte $03,$ff,$c0   ;   ############
Added:         !byte $07,$ff,$e0   ;  ##############
Added:         !byte $07,$ff,$e0   ;  ##############
Added:         !byte $07,$e7,$e0   ;  ###..####..###
Added:         !byte $03,$c3,$c0   ;   ##....##....##
Added:         !byte $01,$ff,$80   ;    ##########
Added:         !byte $00,$ff,$00   ;     ########
Added:         !byte $00,$ff,$00   ;     ########
Added:         !byte $00,$db,$00   ;     ##.##.##
Added:         !byte $00,$db,$00   ;     ##.##.##
Added:         !byte $00,$66,$00   ;      ##..##
Added:         !byte $00,$24,$00   ;       #..#
Added:         !byte $00,$00,$00   ;
The complete program
step-02.asm
; 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 Enable7Spr 706Spr 605Spr 504Spr 403Spr 302Spr 201Spr 100Spr 01
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$D02EVIC-II registersSprites, colours, screen$2040$2000$203FSprite data64 bytes (block 128)$0900$080D$08FFProgram code$0801$080CBASIC stubSYS 2061$0800$07F8$07FFSprite pointersBlock numbers$07E8$0400$07E7Screen RAM40×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.

Run it here

This unit's program, built for you.

Program
step-02.prg
Machine
Commodore 64 (PAL Breadbin)

Download step-02.prg