Skip to content

Enemy Appears

Add a third sprite that moves on its own — a green enemy that drifts down the screen and keeps coming, respawning in a new column each time it leaves.

  • Commodore 64
  • Assembly

Your ship flies, shoots, and the shot has a voice. But it’s firing into an empty sky — there’s nothing up there. This unit adds the first thing that isn’t you: a green enemy that moves on its own, with no joystick behind it. It drifts down, leaves the screen, and comes straight back from a new direction.

Where we start

We have Unit 5’s ship: it moves, clamps to the edges, fires a bullet, and pews. The sky above it is empty.

The white ship at the bottom centre of an empty black screen — armed and sounding, but with nothing to shoot at.

Milestone 1 — a sprite that moves itself

So far every sprite has been driven by you (the ship) or by a button (the bullet). The enemy is different: nothing reads input for it. Each frame, code nudges it along — that’s all “movement” is on this machine.

It rides on sprite 2. The VIC-II gives all eight sprites the same shape of registers, spaced two bytes apart, so sprite 2’s are a short step on from the ones you already know:

Register Sprite 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

To make it visible we set bit 2 of the sprite-enable register — the same $d015 whose bit 0 is the ship and bit 1 the bullet:

$D015 — Sprite enable7—06—05—04—03—02Enemy11Bullet00Ship1
Bits 0 and 2 set ($05): ship and enemy on. The bullet's bit 1 still flips on and off as you fire.

The enemy faces down — it’s coming at you. A classic invader silhouette: antennae on top, a widening body, legs splayed below.

$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 — an invader facing down, stored in block 130 at $2080. Green, to read instantly as 'not you'.

We give it a starting position, then move it one pixel down every frame. Moving down means adding to the Y register — clc then adc, the addition from the Primer:

Step 1: a third sprite, drifting down on its own+50 added−2 removed
bullet_y      = $03     ; Bullet Y position
laser_timer   = $04     ; Frames of laser pitch-sweep remaining (0 = idle)
laser_freq    = $05     ; Our copy of the sweep pitch (SID freq regs are write-only)
Added: enemy_x       = $06     ; Enemy X position
Added: enemy_y       = $07     ; Enemy Y position

; ------------------------------------------------
; BASIC stub
…        sta $d001           ; Y position
        lda #$01
        sta $d027           ; Colour (white)
Removed:         lda #%00000001
Removed:         sta $d015           ; Enable sprite 0
Added:         lda #%00000101
Added:         sta $d015           ; Enable sprites 0 (ship) and 2 (enemy)
        lda #$00
        sta $d010           ; sprite high-X bits clear (ship starts under X=256)

…        sta $07f9           ; Data pointer (block 129 = $2040)
        lda #$07
        sta $d028           ; Colour (yellow)
Added: 
Added:         ; Sprite 2 setup (enemy)
Added:         lda #130
Added:         sta $07fa           ; Data pointer (block 130 = $2080)
Added:         lda #$05
Added:         sta $d029           ; Colour (green)
Added:         lda #100
Added:         sta enemy_x
Added:         sta $d004           ; X position
Added:         lda #$32
Added:         sta enemy_y
Added:         sta $d005           ; Y position (top of the play area)

        ; Bullet starts inactive
        lda #$00
…        sta $d010

no_bullet:
Added: 
Added:         ; --- Update enemy: drift down 1 pixel per frame ---
Added:         lda enemy_y
Added:         clc
Added:         adc #$01            ; clc before adc, the addition from the Primer
Added:         sta enemy_y
Added:         sta $d005           ; sprite 2 Y
Added:         ; (No bottom check yet — left alone, the Y byte wraps at 255 and the
Added:         ;  enemy pops back to the top in the same column. Step 2 handles this.)

        jmp game_loop

…        !byte $00,$00,$00
        !byte $00,$00,$00
        !byte $00,$00,$00
Added: ; ------------------------------------------------
Added: ; Sprite data at $2080 (block 130) — enemy
Added: ; ------------------------------------------------
Added: *= $2080
Added:         !byte $00,$66,$00   ;      ##..##
Added:         !byte $00,$3c,$00   ;       ####
Added:         !byte $00,$7e,$00   ;      ######
Added:         !byte $00,$db,$00   ;     ##.##.##
Added:         !byte $00,$ff,$00   ;     ########
Added:         !byte $01,$ff,$80   ;    ##########
Added:         !byte $01,$7e,$80   ;    #.######.#
Added:         !byte $01,$3c,$80   ;    #..####..#
Added:         !byte $00,$a5,$00   ;     #.#..#.#
Added:         !byte $01,$81,$80   ;    ##......##
Added:         !byte $00,$00,$00   ;
Added:         !byte $00,$00,$00   ;
Added:         !byte $00,$00,$00   ;
Added:         !byte $00,$00,$00   ;
Added:         !byte $00,$00,$00   ;
Added:         !byte $00,$00,$00   ;
Added:         !byte $00,$00,$00   ;
Added:         !byte $00,$00,$00   ;
Added:         !byte $00,$00,$00   ;
Added:         !byte $00,$00,$00   ;
Added:         !byte $00,$00,$00   ;
The complete step 1 program
step-01.asm
; Starfield - Unit 6: Enemy Appears
; Cumulative steps: step-00 (ship + laser) -> step-01 (+ an enemy that drifts down) -> step-02 (+ respawn at a new column)
; Assemble: acme -f cbm -o <step>.prg <step>.asm

; ------------------------------------------------
; Zero-page variables
; ------------------------------------------------
bullet_active = $02     ; 0 = no bullet, 1 = active
bullet_y      = $03     ; Bullet Y position
laser_timer   = $04     ; Frames of laser pitch-sweep remaining (0 = idle)
laser_freq    = $05     ; Our copy of the sweep pitch (SID freq regs are write-only)
enemy_x       = $06     ; Enemy X position
enemy_y       = $07     ; 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)
        lda #%00000101
        sta $d015           ; Enable sprites 0 (ship) and 2 (enemy)
        lda #$00
        sta $d010           ; sprite high-X bits clear (ship starts under X=256)

        ; 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)
        lda #100
        sta enemy_x
        sta $d004           ; X position
        lda #$32
        sta enemy_y
        sta $d005           ; Y position (top of the play area)

        ; 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 #$06
        sta $d405           ; Attack=0, Decay=6 (a short, snappy fall)
        lda #$00
        sta $d406           ; Sustain=0, Release=0

; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
        ; Wait for the raster beam to reach line 255
        ; This syncs our code to the display (~50Hz PAL)
-       lda $d012
        cmp #$ff
        bne -

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

        ; UP (bit 0) — clamp to Y >= 50
        lda $dc00           ; Read joystick port 2
        and #%00000001      ; Isolate bit 0
        bne not_up          ; Bit is 1 = NOT pressed (active low)
        lda $d001
        cmp #52             ; 50 + room for a 2-pixel move
        bcc not_up          ; already at the top — don't move
        dec $d001           ; Move ship up (decrease Y)
        dec $d001           ; 2 pixels per frame
not_up:

        ; DOWN (bit 1) — clamp to Y <= 234
        lda $dc00
        and #%00000010
        bne not_down
        lda $d001
        cmp #233            ; 234 - room for a 2-pixel move
        bcs not_down        ; already at the bottom — don't move
        inc $d001           ; Move ship down (increase Y)
        inc $d001
not_down:

        ; LEFT (bit 2) — 9-bit X, clamp to X >= 24
        lda $dc00
        and #%00000100
        bne not_left
        lda $d010
        and #$01
        bne left_ok         ; high bit set: X >= 256, always safe to go left
        lda $d000
        cmp #26             ; 24 + room for a 2-pixel move
        bcc not_left        ; already at the left edge — don't move
left_ok:
        ; before each step, flip the 9th bit when X is about to wrap $00 -> $ff
        lda $d000
        bne +
        lda $d010
        eor #$01            ; the eor bit-flip from the Primer, on sprite 0's high X bit
        sta $d010
+       dec $d000
        lda $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+       dec $d000
not_left:

        ; RIGHT (bit 3) — 9-bit X, clamp to X <= 320
        lda $dc00
        and #%00001000
        bne not_right
        lda $d010
        and #$01
        beq right_ok        ; high bit clear: X < 256, always safe to go right
        lda $d000
        cmp #63             ; (320 - 256) - room for a 2-pixel move
        bcs not_right       ; already at the right edge — don't move
right_ok:
        ; after each step, flip the 9th bit when X wraps $ff -> $00
        inc $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+       inc $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+
not_right:

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

        ; Only spawn if no bullet is already flying
        lda bullet_active
        bne no_fire

        ; Spawn the bullet at the ship's position
        lda $d000           ; Ship X (low byte) -> bullet X
        sta $d002
        lda $d001           ; Ship Y -> bullet Y
        sta bullet_y

        ; Copy the ship's 9th X bit (bit 0) to the bullet's (bit 1),
        ; so a shot fired from the right half spawns under the ship
        lda $d010
        and #%11111101      ; clear the bullet's 9th bit first
        sta $d010
        lda $d010
        and #$01            ; the ship's 9th bit
        asl                 ; shift it into the bullet's position (bit 1)
        ora $d010
        sta $d010

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

        lda #$01
        sta bullet_active

        ; Trigger laser sound: start the pitch high, gate off then on
        lda #$40
        sta laser_freq      ; start high
        sta $d401           ; SID frequency high byte
        lda #$20
        sta $d404           ; Sawtooth, gate OFF (reset envelope)
        lda #$21
        sta $d404           ; Sawtooth, gate ON (start sound)
        lda #$0a
        sta laser_timer     ; sweep down over 10 frames

no_fire:

        ; --- Laser pitch sweep: the 'pew' ---
        ; Drop the pitch a little each frame while the sweep is running.
        ; We keep our own copy because SID frequency registers are write-only.
        lda laser_timer
        beq no_sweep
        lda laser_freq
        sec
        sbc #$06
        sta laser_freq
        sta $d401           ; write the new pitch to the SID
        dec laser_timer
no_sweep:

        ; --- Update the bullet ---
        lda bullet_active
        beq no_bullet

        ; Move it up 4 pixels a frame
        lda bullet_y
        sec
        sbc #$04
        sta bullet_y
        sta $d003           ; sprite 1 Y

        ; Gone off the top? (Y < 30) -> remove it
        cmp #$1e
        bcs no_bullet

        lda #$00
        sta bullet_active
        lda $d015
        and #%11111101      ; disable sprite 1, keep sprite 0
        sta $d015
        lda $d010
        and #%11111101      ; clear the bullet's 9th bit
        sta $d010

no_bullet:

        ; --- Update enemy: drift down 1 pixel per frame ---
        lda enemy_y
        clc
        adc #$01            ; clc before adc, the addition from the Primer
        sta enemy_y
        sta $d005           ; sprite 2 Y
        ; (No bottom check yet — left alone, the Y byte wraps at 255 and the
        ;  enemy pops back to the top in the same column. Step 2 handles this.)

        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   ;

The enemy drifts steadily down with no input at all:

The enemy drifts down one pixel a frame and slides off the bottom — then, with no respawn logic, the Y byte wraps from 255 back to 0 and it pops in again at the top, in the very same column.

That reappearance is an accident, not a feature. The Y position is a single byte, so when it climbs past 255 it wraps to 0 — the top — and the enemy falls again down the same column. It works, but it’s mechanical and predictable. A real wave comes from somewhere new each time.

Milestone 2 — make it keep coming

We want the enemy to leave at the bottom and return from a fresh column, deliberately. Two pieces: notice when it’s gone, and pick a new spot.

Noticing is a compare. Once the Y position reaches the bottom (248), we respawn — cmp against the limit and branch away if we’re not there yet, the compare-and-decide from the Primer. For the new column we need an unpredictable number, and the C64 hands us a cheap one: $d012, the raster register, holds the screen line the beam is drawing right now. It races up the screen 50-odd times a second, so reading it at the unpredictable moment the enemy happens to exit gives a different value almost every time.

Step 2: respawn at the top in a new column+14 added−2 removed
        adc #$01            ; clc before adc, the addition from the Primer
        sta enemy_y
        sta $d005           ; sprite 2 Y
Removed:         ; (No bottom check yet — left alone, the Y byte wraps at 255 and the
Removed:         ;  enemy pops back to the top in the same column. Step 2 handles this.)
Added: 
Added:         ; Off the bottom? (Y >= 248) -> respawn at the top in a new column
Added:         cmp #$f8
Added:         bcc no_respawn      ; Y < 248, still on screen
Added:         lda #$32
Added:         sta enemy_y
Added:         sta $d005           ; back to the top
Added:         lda $d012           ; raster line — changes constantly, our pseudo-random
Added:         and #$7f            ; range 0-127
Added:         clc
Added:         adc #$30            ; shift to 48-175, keeping it within the visible width
Added:         sta enemy_x
Added:         sta $d004           ; new X column
Added: no_respawn:

        jmp game_loop

The complete program
step-02.asm
; Starfield - Unit 6: Enemy Appears
; Cumulative steps: step-00 (ship + laser) -> step-01 (+ an enemy that drifts down) -> step-02 (+ respawn at a new column)
; Assemble: acme -f cbm -o <step>.prg <step>.asm

; ------------------------------------------------
; Zero-page variables
; ------------------------------------------------
bullet_active = $02     ; 0 = no bullet, 1 = active
bullet_y      = $03     ; Bullet Y position
laser_timer   = $04     ; Frames of laser pitch-sweep remaining (0 = idle)
laser_freq    = $05     ; Our copy of the sweep pitch (SID freq regs are write-only)
enemy_x       = $06     ; Enemy X position
enemy_y       = $07     ; 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)
        lda #%00000101
        sta $d015           ; Enable sprites 0 (ship) and 2 (enemy)
        lda #$00
        sta $d010           ; sprite high-X bits clear (ship starts under X=256)

        ; 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)
        lda #100
        sta enemy_x
        sta $d004           ; X position
        lda #$32
        sta enemy_y
        sta $d005           ; Y position (top of the play area)

        ; 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 #$06
        sta $d405           ; Attack=0, Decay=6 (a short, snappy fall)
        lda #$00
        sta $d406           ; Sustain=0, Release=0

; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
        ; Wait for the raster beam to reach line 255
        ; This syncs our code to the display (~50Hz PAL)
-       lda $d012
        cmp #$ff
        bne -

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

        ; UP (bit 0) — clamp to Y >= 50
        lda $dc00           ; Read joystick port 2
        and #%00000001      ; Isolate bit 0
        bne not_up          ; Bit is 1 = NOT pressed (active low)
        lda $d001
        cmp #52             ; 50 + room for a 2-pixel move
        bcc not_up          ; already at the top — don't move
        dec $d001           ; Move ship up (decrease Y)
        dec $d001           ; 2 pixels per frame
not_up:

        ; DOWN (bit 1) — clamp to Y <= 234
        lda $dc00
        and #%00000010
        bne not_down
        lda $d001
        cmp #233            ; 234 - room for a 2-pixel move
        bcs not_down        ; already at the bottom — don't move
        inc $d001           ; Move ship down (increase Y)
        inc $d001
not_down:

        ; LEFT (bit 2) — 9-bit X, clamp to X >= 24
        lda $dc00
        and #%00000100
        bne not_left
        lda $d010
        and #$01
        bne left_ok         ; high bit set: X >= 256, always safe to go left
        lda $d000
        cmp #26             ; 24 + room for a 2-pixel move
        bcc not_left        ; already at the left edge — don't move
left_ok:
        ; before each step, flip the 9th bit when X is about to wrap $00 -> $ff
        lda $d000
        bne +
        lda $d010
        eor #$01            ; the eor bit-flip from the Primer, on sprite 0's high X bit
        sta $d010
+       dec $d000
        lda $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+       dec $d000
not_left:

        ; RIGHT (bit 3) — 9-bit X, clamp to X <= 320
        lda $dc00
        and #%00001000
        bne not_right
        lda $d010
        and #$01
        beq right_ok        ; high bit clear: X < 256, always safe to go right
        lda $d000
        cmp #63             ; (320 - 256) - room for a 2-pixel move
        bcs not_right       ; already at the right edge — don't move
right_ok:
        ; after each step, flip the 9th bit when X wraps $ff -> $00
        inc $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+       inc $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+
not_right:

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

        ; Only spawn if no bullet is already flying
        lda bullet_active
        bne no_fire

        ; Spawn the bullet at the ship's position
        lda $d000           ; Ship X (low byte) -> bullet X
        sta $d002
        lda $d001           ; Ship Y -> bullet Y
        sta bullet_y

        ; Copy the ship's 9th X bit (bit 0) to the bullet's (bit 1),
        ; so a shot fired from the right half spawns under the ship
        lda $d010
        and #%11111101      ; clear the bullet's 9th bit first
        sta $d010
        lda $d010
        and #$01            ; the ship's 9th bit
        asl                 ; shift it into the bullet's position (bit 1)
        ora $d010
        sta $d010

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

        lda #$01
        sta bullet_active

        ; Trigger laser sound: start the pitch high, gate off then on
        lda #$40
        sta laser_freq      ; start high
        sta $d401           ; SID frequency high byte
        lda #$20
        sta $d404           ; Sawtooth, gate OFF (reset envelope)
        lda #$21
        sta $d404           ; Sawtooth, gate ON (start sound)
        lda #$0a
        sta laser_timer     ; sweep down over 10 frames

no_fire:

        ; --- Laser pitch sweep: the 'pew' ---
        ; Drop the pitch a little each frame while the sweep is running.
        ; We keep our own copy because SID frequency registers are write-only.
        lda laser_timer
        beq no_sweep
        lda laser_freq
        sec
        sbc #$06
        sta laser_freq
        sta $d401           ; write the new pitch to the SID
        dec laser_timer
no_sweep:

        ; --- Update the bullet ---
        lda bullet_active
        beq no_bullet

        ; Move it up 4 pixels a frame
        lda bullet_y
        sec
        sbc #$04
        sta bullet_y
        sta $d003           ; sprite 1 Y

        ; Gone off the top? (Y < 30) -> remove it
        cmp #$1e
        bcs no_bullet

        lda #$00
        sta bullet_active
        lda $d015
        and #%11111101      ; disable sprite 1, keep sprite 0
        sta $d015
        lda $d010
        and #%11111101      ; clear the bullet's 9th bit
        sta $d010

no_bullet:

        ; --- Update enemy: drift down 1 pixel per frame ---
        lda enemy_y
        clc
        adc #$01            ; clc before adc, the addition from the Primer
        sta enemy_y
        sta $d005           ; sprite 2 Y

        ; Off the bottom? (Y >= 248) -> respawn at the top in a new column
        cmp #$f8
        bcc no_respawn      ; Y < 248, still on screen
        lda #$32
        sta enemy_y
        sta $d005           ; back to the top
        lda $d012           ; raster line — changes constantly, our pseudo-random
        and #$7f            ; range 0-127
        clc
        adc #$30            ; shift to 48-175, keeping it within the visible width
        sta enemy_x
        sta $d004           ; new X column
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   ;

When Y reaches 248 we snap it back to the top and read $d012. That raw line number could land anywhere, so we trim it: and #$7f keeps it in 0–127, then adc #$30 shifts it to 48–175 — safely inside the visible width, never tucked behind the border. It isn’t true randomness, but the enemy no longer marches down the same lane:

With respawn, the enemy reaches the bottom, jumps straight back to the top in a different column, and comes again — a stream instead of a single pass.

When it’s wrong, see why

An autonomous sprite has only a few ways to misbehave, and each names its own value:

  • No enemy at all. Either it’s disabled or pointed at empty data. Check $d015 has bit 2 set (the value should be $05, not $01), and that $07fa holds 130 — the block where the sprite data lives. A blank sprite usually means the pointer is wrong.
  • Enemy frozen at the top. The movement isn’t running. Watch enemy_y: it should climb by one every frame and be copied to $d005. If the variable changes but the sprite doesn’t move, you updated the zero-page copy but forgot to write the register (or wrote the wrong one).
  • It never comes back. The respawn test is inverted. bcc skips the respawn while Y is below 248; if the branch goes the wrong way the reset never runs and the enemy just wraps. Confirm the compare is cmp #$f8 and the branch is bcc.
  • Always the same column. You’re reading $d012 once at startup instead of at respawn. The raster value is only unpredictable if you read it at the moment the enemy exits — read it in the init code and it’s frozen.

Before and after

We started with an empty sky and finished with an enemy that arrives on its own, leaves at the bottom, and returns from a new column — the first object in the game the player doesn’t control, and the first reason the shooting will matter.

Try this

  • A faster descent. The enemy falls one pixel a frame (adc #$01). Make it two or three and feel how much less time you get to line up under it. Speed is difficulty, set by a single byte.
  • A different threat colour. $d029 takes any C64 colour: $02 red, $07 yellow, $0e light blue. Pick one that stands out against black without competing with the white ship.

What you’ve learnt

  • Sprite 2 — registers two bytes on from sprite 1 ($d004/$d005, colour $d029, pointer $07fa), following the VIC-II’s even spacing.
  • The enable register as bits — $d015 value $05 turns on sprites 0 and 2 together; each bit is one sprite.
  • Autonomous movement — clc + adc on a position every frame is all it takes to make a sprite move with no input.
  • Respawning — compare a position against a limit, and reset it when it’s past.
  • A cheap pseudo-random — the raster register $d012, read at an unpredictable moment, masked and shifted into a usable range.

What’s next

The enemy comes at you and your bullet flies past it — they don’t notice each other yet. Next you’ll add collision detection, so a shot that meets the enemy destroys it.

Run it here

This unit's program, built for you.

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

Download step-02.prg