Skip to content
Game 1 Unit 5 of 128 1 hr learning time

Enemy Appears

A green enemy sprite drifts down the screen — your first autonomous game object. Three sprites, one enemy, and a reason to aim.

4% of Starfield

A green shape drifts down from the top of the screen. It doesn’t shoot, doesn’t dodge, doesn’t even notice you’re there. You can’t destroy it yet — but now there’s something to aim for.

A Third Sprite

The VIC-II has eight hardware sprites, numbered 0 to 7. We’ve used two so far — sprite 0 for the ship and sprite 1 for the bullet. Now we add sprite 2 for the enemy.

Each sprite has its own set of registers. The pattern is the same as before — position, colour, data pointer — just at different addresses:

RegisterSprite 0 (Ship)Sprite 1 (Bullet)Sprite 2 (Enemy)
X position$D000$D002$D004
Y position$D001$D003$D005
Colour$D027$D028$D029
Data pointer$07F8$07F9$07FA

The spacing is consistent: each sprite’s registers are two bytes apart. Sprite n has its X at $D000 + 2n and Y at $D001 + 2n.

        ; Sprite 2 setup (enemy)
        lda #130
        sta $07fa           ; Data pointer (block 130 = $2080)
        lda #$05
        sta $d029           ; Colour (green)

        ; Enemy starting position
        lda #100
        sta enemy_x
        sta $d004           ; X position
        lda #$32
        sta enemy_y
        sta $d005           ; Y position

        ; Enable sprites 0 and 2 (bullet starts disabled)
        lda #%00000101
        sta $d015

We store the enemy’s position in zero-page variables enemy_x and enemy_y, just like we did with bullet_y. The zero page is fast to access, and we’ll need to read and update these values every frame.

Enabling Multiple Sprites

The sprite enable register at $D015 controls which sprites are visible. Each bit corresponds to one sprite:

$D015 — Sprite Enable 7 Spr 7 0 6 Spr 6 0 5 Spr 5 0 4 Spr 4 0 3 Spr 3 0 2 Enemy 1 1 Bullet 0 0 Ship 1
Bits 0 and 2 active = ship and enemy visible. Bit 1 (bullet) enabled on fire. Value: $05.

At startup we write %00000101 — bits 0 and 2 — to enable the ship and enemy. The bullet’s bit stays off until the player fires.

When a bullet spawns, we use ORA to set bit 1 without disturbing the other bits:

lda $d015
ora #%00000010       ; Set bit 1 (bullet)
sta $d015

When the bullet leaves the screen, we use AND to clear bit 1:

lda $d015
and #%11111101       ; Clear bit 1 (bullet)
sta $d015

ORA sets bits. AND clears them. Together they let you control individual sprites without affecting the rest of the register. This is a pattern you’ll use constantly — not just for sprites, but for any register where different bits mean different things.

The Enemy Sprite

The ship points up. The enemy descends — so it faces down. A classic invader silhouette: antennae at the top, body widening, then legs splayed at the bottom.

$00 $66 $00 $00 $3C $00 $00 $7E $00 $00 $DB $00 $00 $FF $00 $01 $FF $80 $01 $7E $80 $01 $3C $80 $00 $A5 $00 $01 $81 $80 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00
The enemy sprite — 10 rows of pixel data, stored at $2080 (block 130). Green, because enemies are green.
; ------------------------------------------------
; Sprite data at $2080 (block 130) — enemy
; ------------------------------------------------
*= $2080
        !byte $00,$66,$00   ;      ##..##
        !byte $00,$3c,$00   ;       ####
        !byte $00,$7e,$00   ;      ######
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$ff,$00   ;     ########
        !byte $01,$ff,$80   ;    ##########
        !byte $01,$7e,$80   ;    #.######.#
        !byte $01,$3c,$80   ;    #..####..#
        !byte $00,$a5,$00   ;     #.#..#.#
        !byte $01,$81,$80   ;    ##......##
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;

The sprite data sits at $2080 — block 130. Same 63-byte format as the ship and bullet: 3 bytes per row, 21 rows, each byte encoding 8 pixels. The data pointer at $07FA tells the VIC-II where to find it.

Moving the Enemy

The enemy drifts down the screen at one pixel per frame. No joystick input, no complex logic — just a constant downward march.

        ; --- Update enemy ---
        lda enemy_y
        clc
        adc #$01            ; Move down 1 pixel per frame
        sta enemy_y
        sta $d005           ; Update sprite 2 Y position

        ; Check if enemy has left the screen (Y > 248)
        cmp #$f8
        bcc no_respawn      ; Y < 248, still on screen

        ; Respawn at top with new X position
        lda #$32
        sta enemy_y
        sta $d005           ; Reset Y to top

        lda $d012           ; Pseudo-random from raster position
        and #$7f            ; Range 0-127
        clc
        adc #$30            ; Range 48-175
        sta enemy_x
        sta $d004           ; New X position

no_respawn:

CLC followed by ADC #$01 adds 1 to the Y position. CLC clears the carry flag first — without it, a stray carry from a previous operation could add an extra pixel. Always clear carry before addition.

The updated value goes into both the zero-page variable (enemy_y) and the VIC-II register ($D005). The variable tracks the position in code; the register tells the hardware where to draw.

Respawning

When the enemy’s Y position passes 248 ($F8), it’s below the visible screen. Time to respawn:

cmp #$f8
bcc no_respawn       ; Y < 248, still on screen

BCC (Branch if Carry Clear) branches when the accumulator is less than the comparison value. If Y is 248 or more, we fall through to the respawn code.

Respawning means resetting Y to the top ($32) and picking a new X position. For the X, we read the raster register:

lda $d012            ; Pseudo-random from raster position
and #$7f             ; Range 0-127
clc
adc #$30             ; Range 48-175

$D012 holds the current raster line — which line of the screen the electron beam is drawing right now. It changes constantly, making it a quick-and-dirty source of unpredictable values.

AND #$7F masks the value to 0–127. ADC #$30 shifts it up to 48–175, keeping the enemy within the visible horizontal range. It’s not truly random — but it’s good enough that the enemy doesn’t always reappear in the same column.

The Complete Code

; Starfield - Unit 5: Enemy Appears
; Assemble with: acme -f cbm -o starfield.prg starfield.asm

; ------------------------------------------------
; Zero-page variables
; ------------------------------------------------
bullet_active = $02     ; 0 = no bullet, 1 = active
bullet_y      = $03     ; Bullet Y position
enemy_x       = $04     ; Enemy X position
enemy_y       = $05     ; Enemy Y position

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

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

        ; Clear the screen
        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)

        ; Sprite 1 setup (bullet)
        lda #129
        sta $07f9           ; Data pointer (block 129 = $2040)
        lda #$07
        sta $d028           ; Colour (yellow)

        ; Sprite 2 setup (enemy)
        lda #130
        sta $07fa           ; Data pointer (block 130 = $2080)
        lda #$05
        sta $d029           ; Colour (green)

        ; Enemy starting position
        lda #100
        sta enemy_x
        sta $d004           ; X position
        lda #$32
        sta enemy_y
        sta $d005           ; Y position

        ; Enable sprites 0 and 2 (bullet starts disabled)
        lda #%00000101
        sta $d015

        ; Bullet starts inactive
        lda #$00
        sta bullet_active

        ; SID setup — voice 1 laser sound
        lda #$0f
        sta $d418           ; Volume to maximum

        lda #$00
        sta $d400           ; Frequency low byte
        lda #$10
        sta $d401           ; Frequency high byte ($1000 = mid-high pitch)

        lda #$09
        sta $d405           ; Attack=0, Decay=9
        lda #$00
        sta $d406           ; Sustain=0, Release=0

!ifdef SCREENSHOT_MODE {
        ; Place a static bullet for screenshot capture
        lda $d000
        sta $d002           ; Bullet X = ship X
        lda #140
        sta $d003           ; Bullet Y mid-screen

        ; Enable all three sprites
        lda #%00000111
        sta $d015
}

; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
        ; Wait for the raster beam to reach line 255
-       lda $d012
        cmp #$ff
        bne -

        ; --- Read joystick and move ship ---

        ; UP (bit 0)
        lda $dc00
        and #%00000001
        bne not_up
        dec $d001
        dec $d001
not_up:

        ; DOWN (bit 1)
        lda $dc00
        and #%00000010
        bne not_down
        inc $d001
        inc $d001
not_down:

        ; LEFT (bit 2)
        lda $dc00
        and #%00000100
        bne not_left
        dec $d000
        dec $d000
not_left:

        ; RIGHT (bit 3)
        lda $dc00
        and #%00001000
        bne not_right
        inc $d000
        inc $d000
not_right:

        ; --- Fire button (bit 4) ---
        lda $dc00
        and #%00010000
        bne no_fire         ; Bit is 1 = NOT pressed

        ; Fire is pressed — only spawn if no bullet active
        lda bullet_active
        bne no_fire         ; Already active, skip

        ; Spawn bullet at ship position
        lda $d000           ; Ship X → bullet X
        sta $d002
        lda $d001           ; Ship Y → bullet Y
        sta bullet_y

        ; Enable sprite 1 (keep other sprites enabled)
        lda $d015
        ora #%00000010
        sta $d015

        ; Mark bullet active
        lda #$01
        sta bullet_active

        ; Trigger laser sound (gate off then on to retrigger)
        lda #$20
        sta $d404           ; Sawtooth, gate OFF (reset envelope)
        lda #$21
        sta $d404           ; Sawtooth, gate ON (start sound)

no_fire:

        ; --- Update bullet ---
        lda bullet_active
        beq no_bullet       ; Not active, skip

        ; Move bullet up (4 pixels per frame)
        lda bullet_y
        sec
        sbc #$04
        sta bullet_y
        sta $d003           ; Update sprite 1 Y position

        ; Check if bullet has left the screen (Y < 30)
        cmp #$1e
        bcs no_bullet       ; Y >= 30, still on screen

        ; Deactivate bullet
        lda #$00
        sta bullet_active

        ; Disable sprite 1 (keep other sprites enabled)
        lda $d015
        and #%11111101
        sta $d015

no_bullet:

        ; --- Update enemy ---
        lda enemy_y
        clc
        adc #$01            ; Move down 1 pixel per frame
        sta enemy_y
        sta $d005           ; Update sprite 2 Y position

        ; Check if enemy has left the screen (Y > 248)
        cmp #$f8
        bcc no_respawn      ; Y < 248, still on screen

        ; Respawn at top with new X position
        lda #$32
        sta enemy_y
        sta $d005           ; Reset Y to top

        lda $d012           ; Pseudo-random from raster position
        and #$7f            ; Range 0-127
        clc
        adc #$30            ; Range 48-175
        sta enemy_x
        sta $d004           ; New X position

no_respawn:
        jmp game_loop

; ------------------------------------------------
; Sprite data at $2000 (block 128) — ship
; ------------------------------------------------
*= $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   ;

; ------------------------------------------------
; Sprite data at $2040 (block 129) — bullet
; ------------------------------------------------
*= $2040
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;

; ------------------------------------------------
; Sprite data at $2080 (block 130) — enemy
; ------------------------------------------------
*= $2080
        !byte $00,$66,$00   ;      ##..##
        !byte $00,$3c,$00   ;       ####
        !byte $00,$7e,$00   ;      ######
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$ff,$00   ;     ########
        !byte $01,$ff,$80   ;    ##########
        !byte $01,$7e,$80   ;    #.######.#
        !byte $01,$3c,$80   ;    #..####..#
        !byte $00,$a5,$00   ;     #.#..#.#
        !byte $01,$81,$80   ;    ##......##
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;

If It Doesn’t Work

  • No enemy visible? Check $D015 has bit 2 set, and $07FA points to block 130.
  • Enemy stuck? Make sure enemy_y is being incremented and stored to $D005.
  • Enemy never respawns? Check the CMP #$F8 / BCC logic — BCC branches when less than, so values >= 248 fall through to respawn.
  • Enemy always spawns at the same X? $D012 changes every frame — make sure you’re reading it at respawn time, not during init.

Try This: Change Enemy Speed

ADC #$01 moves the enemy down one pixel per frame — a slow drift. Try different values:

  • ADC #$02 — Twice as fast. A brisk descent.
  • ADC #$03 — Aggressive. Hard to track visually.

Higher speeds make the game feel more urgent, but they also mean less time for the player to line up a shot (once we add collision in Unit 6).

Try This: Change Enemy Colour

The colour register at $D029 accepts standard C64 colour values:

  • $02 — Red. Classic danger colour.
  • $07 — Yellow. Bright and visible.
  • $0A — Light red. Softer, pinker.
  • $0E — Light blue. Sci-fi alien feel.

What You’ve Learnt

  • Sprite 2 — registers at $D004/$D005, colour at $D029, data pointer at $07FA.
  • Sprite enable register ($D015)ORA to enable individual bits, AND to disable them.
  • Autonomous movementCLC + ADC each frame for constant-speed motion.
  • Respawn pattern — detect when a sprite leaves the screen, reset its position.
  • Pseudo-random values$D012 (raster register) as a quick source of unpredictable numbers.

What’s Next

The enemy drifts past untouched. In Unit 6, your bullets will finally connect — and the enemy will know about it.