Enemy Appears
A green enemy sprite drifts down the screen — your first autonomous game object. Three sprites, one enemy, and a reason to aim.
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:
| 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 |
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:
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.
; ------------------------------------------------
; 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
$D015has bit 2 set, and$07FApoints to block 130. - Enemy stuck? Make sure
enemy_yis being incremented and stored to$D005. - Enemy never respawns? Check the
CMP #$F8/BCClogic —BCCbranches when less than, so values >= 248 fall through to respawn. - Enemy always spawns at the same X?
$D012changes 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) —ORAto enable individual bits,ANDto disable them. - Autonomous movement —
CLC+ADCeach 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.