You are on page 1of 218

INHALT: Step 27 - Enemy (Bat)

A C64 game, Step-by-Step Step 28 - Level Item


Step 2 - Charset Step 29 - Highscore FX
Step 3 - Sprites Step 30 - Name Entry FX
Step 4 - Joystick Step 31 - New Levels
Step 5 - Play Screen Step 32 - Bug Hunting
Step 6 - Collision Dectecting Step 33 - Gameplay Changes
Step 7 - Gravity & Jumping Step 34 - Optimizing
Step 8 - Enemies Step 35 - Annoyed State
Step 9 - Behaviour Step 36 - Burst in Smoke
Step 10 - Enemy Collision Step 37 - Player Death-Anim
Step 11 - Kill Player Step 38 - Second Player
Step 12 - Shooting Step 39 - MUSIC!
Step 13 - Items Step 40 - Spawn Spots
Step 14 - Level Buffer Step 41 - More Sprites
Step 15 - Next Level Step 42 - Sprites (Ghost, Fly)
Step 16 - Score/Lives Step 43 - Sprites (Frank &
Slime)
Step 17 - Player Animation
Step 44 - Sprites (Hand &
Step 18 - Enemy Animation Skeleton)
Step 19 - Enemy (Mummy) Step 45 - Ext. Level Editor
Step 20 - Effects Step 46 - Chapter Intro
Step 21 - Title Screen Step 47 - PowerUps
Step 22 - Highscores Step 48 - More Levels (>>)
Step 23 - Name Entry Step 49 - Bug Hunt
Step 24 - Save Function Step 50 - PowerUps II
Step 25 - Title Screen II Step 51 - Coop Mode
Step 26 - Enemy (Zombie) Step 52 - Visible Spawnpoints

<< Content
Step 53 - Boss Fight
Step 54 - Bug Hunt
Step 55 - The next 10 Levels
Step 56 - New Boss
Step 57 - Borderless Level
Step 58 - Custom Music (>>)
Step 58 - Boss No 3

<< Content
A C64 game, Step-by-Step
https://www.gamedev.net/blogs/blog/949-new-old-things/?page=5

Welcome!

Today's development is heaps and bounds beyond imagination from 20 years ago.
I've always had a soft spot for the C64 after all this years. So I sat down and tried to
start assembly programming on a C64.

Today I'll start with a sort of tutorial on how to write a C64 game. I have prepared 36
steps for now, planned are probably a few more. I'll start out very small but there will
be bigger steps later on. The code is supposed to be heavily commented but is
probably not clear for everyone. I'll be happy to answer questions regarding the
code. The code is written for the ACME cross compiler, which allows to compile the
code on any bigger OS.

Step #1 is a simple base for a game. It provides a Basic start (10 SYS 2064), sets up the
VIC relocation and shows a simple synchronized game loop. To show the loop
running the border color is flashed and the top left char is rotating throughout all
characters.

The not too eye-popping result looks like this: (Bild)

Find here the source code and binary for use in an emulator of your choice (I
recommend WinVICE):

First step explained in detail


As threatened the first step detailed. I reckon that the first step is overwhelming if it's
your first voyage into C64 game programming. There's quite a few assumptions made
about the viewer's knowledge and the later steps won't get exactly easier.

A note about the !zone macro. ACME allows for global and local labels. A local label is
starting with a . and is only visible inside a zone. This allows for easier reuse of
common names like loop or retry.

This snippet tells the ACME cross compiler to assemble the result into the file
"jmain.prg" with the cbm (Commodore Business Machines) type. This basically boils
down to the well known .prg format which contains a word with the loading address
at the start followed by the assembly.

<< Content
;compile to this filename
!to "jmain.prg",cbm

The next snipped just defines a constant. I try to use them throughout so you can
understand where I'm putting bytes at. The value 52224 is the address of the screen
buffer, where 25 lines a 40 characters are stored continously. This is not the default
memory location for the screen, a part of this base code relocates the screen.

;define constants here


;address of the screen buffer
SCREEN_CHAR = 52224

Now a very interesting piece which took me longer to work out than it should have. A
C64 has two types of files, Basic files and machine code files. A Basic file can be
started by RUN, a machine code file just contains the code and usually must be
jumped at with the SYS command. Any half decent game will provide a proper Basic
kick start that jumps directly at the machine code.

To allow for this we set the file start address to $801 (2049), the default Basic start.
The file content starts out with the tokenized bytes of a simple Basic line calling SYS
for us. The line is built by a word containing the address of the next Basic line.
Following is a word with the line number (10 in our sample). After that the token for
the SYS command ($9e) followed by a space ($20) and the ASCII representation of the
target address (2064 in our sample). After that there is one zero byte marking the
end of the line. The next zero word represents the end of the Basic file. I've got some
extra zero bytes which are actually wrong but also don't really hurt.

;this creates a basic start


*=$801 ;SYS 2064
!byte $0C,$8,$0A,$00,$9E,$20,$32,$30,$36,$34,$00,$00,$00,$00,$00

The next snippet disables any visible sprites, relocates the VICs memory bank
(resulting in a relocated screen buffer and charset address).

;init sprite registers


;no visible sprites
lda #0
sta VIC_SPRITE_ENABLE

;set charset
lda #$3c
sta VIC_MEMORY_CONTROL

;VIC bank
lda CIA_PRA
and #$fc

<< Content
sta CIA_PRA

This piece is the main game loop. It's rather easy, we increase the border color
(resulting in flashing), increase the top left character on the screen, wait for the
vertical blank (not exactly but to the effect) and rerun the loop.

;the main game loop


GameLoop
;border flashing
inc VIC_BORDER_COLOR

;top left char


inc SCREEN_CHAR
jsr WaitFrame
jmp GameLoop

This snippet is quite interesting. The C64 allows you to read the current raster line on
the screen that is currently being redrawn. The code checks for a certain raster
position at the bottom of the screen to sync the game to the computer's display
speed.

In detail we're waiting for the raster line to NOT be the position we want to wait for.
Once we are on any line but the wanted we now really wait for our raster line to
appear. This avoids the problem when the routine is called too fast in succession and
we end up on the same raster line.

!zone WaitFrame
;wait for the raster to reach line $f8
;this is keeping our timing stable

;are we on line $F8 already? if so, wait for the next full screen
;prevents mistimings if called too fast

WaitFrame
lda $d012
cmp #$F8
beq WaitFrame

;wait for the raster to reach line $f8 (should be closer to the
start of this line this way)
.WaitStep2
lda $d012
cmp #$F8
bne .WaitStep2

rts

<< Content
Step 2 - Charset
And onwards we stumble!

In the first part we prepared everything for the VIC, now we set up our modified
charset. Note that I used a selfmade tool (similar to CharPad), which is included
(Windows binary). The file J.CHR contains the charset and is included into the source
as binary. The memory layout of the game expects the modified charset at $f000.
Since the C64 can't load files to locations after $C000 we have to copy the charset to
the target memory at $f000. To be able to properly write at those addresses we need
to switch off the ROM overlay.

The current step should display "HELLO". The rest of the screen depends on the
current memory setup of your emulator/C64

First code piece we add is the copy routine. Interrupts are blocked because we turn
off the kernal ROM. If we didn't the IRQ code would jump in the middle of
uninitialised RAM, likely resulting in a crash. The RAM/ROM layout is influenced by
memory address $1.

;----------------------
;copy charset to target
;----------------------

;block interrupts
;since we turn ROMs off this would result in crashes if we did not
sei

;save old configuration


lda $1
sta PARAM1

;only RAM
;to copy under the IO rom
lda #%00110000
sta $1

;take source address from CHARSET


LDA #<CHARSET
STA ZEROPAGE_POINTER_1
LDA #>CHARSET
STA ZEROPAGE_POINTER_1 + 1

;now copy
jsr CopyCharSet

<< Content
;restore ROMs
lda PARAM1
sta $1
cli

The actual copy routine. Note that we only copy 254 characters. The last two
characters are omitted to not overwrite the default IRQ vectors residing at $fffb.
Since we deal with a 8 bit machine there is an extra loop taking care of the high bytes
of our addresses. At the end of the copy routine we include the binary charset data.

!zone CopyCharSet
CopyCharSet
;set target address ($F000)
lda #$00
sta ZEROPAGE_POINTER_2
lda #$F0
sta ZEROPAGE_POINTER_2 + 1
ldx #$00
ldy #$00
lda #0
sta PARAM2
.NextLine
lda (ZEROPAGE_POINTER_1),Y
sta (ZEROPAGE_POINTER_2),Y
inx
iny
cpx #$8
bne .NextLine

cpy #$00
bne .PageBoundaryNotReached

;we reached the next 256 bytes, inc high byte


inc ZEROPAGE_POINTER_1 + 1
inc ZEROPAGE_POINTER_2 + 1

.PageBoundaryNotReached
;only copy 254 chars to keep irq vectors intact
inc PARAM2
lda PARAM2
cmp #254
beq .CopyCharsetDone
ldx #$00
jmp .NextLine

.CopyCharsetDone
rts

CHARSET
!binary "j.chr"

<< Content
To display HELLO on the screen we simple poke the character codes on the
screen and also set the characters colors to white.

;test charset
lda #'H'
sta SCREEN_CHAR
lda #'E'
sta SCREEN_CHAR + 1
lda #'L'
sta SCREEN_CHAR + 2
sta SCREEN_CHAR + 3
lda #'O'
sta SCREEN_CHAR + 4

lda #1
sta SCREEN_COLOR
sta SCREEN_COLOR + 1
sta SCREEN_COLOR + 2
sta SCREEN_COLOR + 3
sta SCREEN_COLOR + 4

Clarifications:

The charset of the C64 is using 8 bytes per character. This totals at 256 characters a 8
bytes = 2048 bytes. A custom character set can be positioned almost everywhere in
RAM (at 2048 interval steps).

In hires text mode every bit corresponds to a pixel. In multicolor text mode pixels are
doubling width, so two bits make up one pixel. In multicolor mode two colors are
shared by all multi-color characters, one is the background color and one is the
current char color.

The memory layout looks like this (nicked from​ ​www.c64-wiki.de​):

$FFFF = 65535 ?????????????????????????????????


?---------------?|||||||||||||||? ||| = read by PEEK
?---------------?|||||||||||||||? --- = written to by POKE
?---------------?|||||||||||||||? +++ = read and write
?---------------?||| KERNAL- |||? other = not reachable from
BASIC
?---------------?||| ROM |||?
?---------------?|||||||||||||||?
?---------------?|||||||||||||||?
$E000 = 57344 ?????????????????????????????????????????????????
? ? ?+++++++++++++++?
? ? CHAR ROM ?+++++ I/O +++++?
? ? ?+++++++++++++++?
$D000 = 53248 ?????????????????????????????????????????????????
?+++++++++++++++?
?+++++++++++++++?

<< Content
?+++++++++++++++?
$C000 = 49152 ?????????????????????????????????
?---------------?|||||||||||||||?
?---------------?|||||||||||||||?
?---------------?||| BASIC- ||||?
?---------------?||| ROM ||||?
?---------------?|||||||||||||||?
?---------------?|||||||||||||||?
?---------------?|||||||||||||||?
$A000 = 40960 ?????????????????????????????????
?+++++++++++++++?
?+++ BASIC- ++++?
?+++ RAM ++++?
. .
?+++ BASIC- ++++?
?+++ RAM ++++?
$800 = 2048 ?+++++++++++++++?-?
$400 = 1024 ?+++++++++++++++?-?Default Screen Address
$0000 ?????????????????-?Zeropage and Enhanced Zeropage

<< Content
Step 3 - Sprites
Quite similar to step 2 this time we set up sprites. Note that I used another selfmade
tool (similar to SpritePad). The file J.SPR contains the sprites and is included into the
source as binary. The sprites are located "under" the I/O ROM at $D000 onwards.

And another note: Since I'm lazy I built the sprite copy loop to always copy packages
of 4 sprites (1 sprite comes as 63 + 1 byte, so 4 make nice 256 byte packages).

The current step should display the known charset garbage and "HELLO" as before,
but also show an elegantly handicrafted sprite.

Right below the charset copy routine we add the copy call.

​ ;take source address from SPRITES


lda #<SPRITES
sta ZEROPAGE_POINTER_1
lda #>SPRITES
sta ZEROPAGE_POINTER_1 + 1
jsr CopySprites

The sprite copy routine is quite similar to the charset copy but a tad shorter due to
the 256 byte packaging:

;------------------------------------------------------------
;copies sprites from ZEROPAGE_POINTER_1 to
ZEROPAGE_POINTER_2
; sprites are copied in numbers of four

;------------------------------------------------------------

!zone CopySprites
CopySprites
ldy #$00
ldx #$00
lda #00
sta ZEROPAGE_POINTER_2
lda #$d0
sta ZEROPAGE_POINTER_2 + 1

;4 sprites per loop


.SpriteLoop
lda (ZEROPAGE_POINTER_1),y
sta (ZEROPAGE_POINTER_2),y
iny

<< Content
bne .SpriteLoop

inx
inc ZEROPAGE_POINTER_1 + 1
inc ZEROPAGE_POINTER_2 + 1
cpx #NUMBER_OF_SPRITES_DIV_4
bne .SpriteLoop

rts

In front of GameLoop we put the sprite display code. The sprite is positioned at
coordinates 100,100, the sprite pointer set to the correct image and the sprite is
enabled.

;set sprite 1 pos


lda #100
sta VIC_SPRITE_X_POS
sta VIC_SPRITE_Y_POS

;set sprite image


lda #SPRITE_PLAYER
sta SPRITE_POINTER_BASE

;enable sprite 1
lda #1
sta VIC_SPRITE_ENABLE

<< Content
Step 4 - Joystick
Now we take a bigger step: Moving the sprite with the joystick. Since we want to
make a real game we also allow to move the sprite over to the right side (in other
words we'll take care of the extended x bit).

For clarification: The C64 has 8 hardware sprites. That's 8 objects of the size 24 x 21
pixels. They can be placed anywhere. The coordinates are stored in memory
addresses. However since the X resolution is higher than 256 all the sprites 9th bit is
stored in another memory location (which makes it highly annoying to work with).

Sprite coordinates are set in X, Y pairs via the memory locations 53248 (=X sprite 0),
53249 (=Y sprite 0), 53250 (=X sprite 1), etc. The extended sprite bits are stored in
53248 + 16.

Since I don't plan to allow sprites to go off screen in the game later there is no
defined behaviour if you move the sprite off screen too far. It'll simply bounce back in
once the coordinate wraps around.

The joystick ports can be checked via the memory locations 56320 (Port 2) or 56321
(Port 1). The lower 5 bits are cleared(!) if either up, down, left, right for fire is
pressed.

This step shows:

-Joystick control
-Sprite extended x bit

Inside the GameLoop we add a call to the players control function:

​ jsr PlayerControl

PlayerControl itself checks the joystick port (II) and calls the proper direction
move routines. Note that the move routines themselves simply set the object
index to 0 (for the player) and call a generic sprite move routine.

;------------------------------------------------------------
;check joystick (player control)

;------------------------------------------------------------
!zone PlayerControl
PlayerControl
lda #$2

<< Content
bit $dc00
bne .NotDownPressed
jsr PlayerMoveDown

.NotDownPressed
lda #$1
bit $dc00
bne .NotUpPressed
jsr PlayerMoveUp

.NotUpPressed
lda #$4
bit $dc00
bne .NotLeftPressed
jsr PlayerMoveLeft

.NotLeftPressed
lda #$8
bit $dc00
bne .NotRightPressed
jsr PlayerMoveRight

.NotRightPressed
rts

PlayerMoveLeft
ldx #0
jsr MoveSpriteLeft
rts

PlayerMoveRight
ldx #0
jsr MoveSpriteRight
rts

PlayerMoveUp
ldx #0
jsr MoveSpriteUp
rts

PlayerMoveDown
ldx #0
jsr MoveSpriteDown
rts

The sprite move routines are rather simple, update the position counter variables
and set the actual sprite registers. A bit more complicated are the X move functions.
If X reaches the wraparound, the extended x bit (the 9th bit) is looked up in a table
and then added/removed.

<< Content
;------------------------------------------------------------
;Move Sprite Left
;expect x as sprite index (0 to 7)

;------------------------------------------------------------
!zone MoveSpriteLeft
MoveSpriteLeft
dec SPRITE_POS_X,x
bpl .NoChangeInExtendedFlag
lda BIT_TABLE,x
eor #$ff
and SPRITE_POS_X_EXTEND
sta SPRITE_POS_X_EXTEND
sta VIC_SPRITE_X_EXTEND
.NoChangeInExtendedFlag
txa
asl
tay
lda SPRITE_POS_X,x
sta VIC_SPRITE_X_POS,y
rts

;------------------------------------------------------------
;Move Sprite Right
;expect x as sprite index (0 to 7)

;------------------------------------------------------------
!zone MoveSpriteRight
MoveSpriteRight
inc SPRITE_POS_X,x
lda SPRITE_POS_X,x
bne .NoChangeInExtendedFlag
lda BIT_TABLE,x
ora SPRITE_POS_X_EXTEND
sta SPRITE_POS_X_EXTEND
sta VIC_SPRITE_X_EXTEND
.NoChangeInExtendedFlag
txa
asl
tay
lda SPRITE_POS_X,x
sta VIC_SPRITE_X_POS,y
rts

;------------------------------------------------------------
;Move Sprite Up
;expect x as sprite index (0 to 7)

;------------------------------------------------------------
!zone MoveSpriteUp

<< Content
MoveSpriteUp
dec SPRITE_POS_Y,x
txa
asl
tay
lda SPRITE_POS_Y,x
sta 53249,y
rts

;------------------------------------------------------------
;Move Sprite Down
;expect x as sprite index (0 to 7)

;------------------------------------------------------------
!zone MoveSpriteDown
MoveSpriteDown
inc SPRITE_POS_Y,x
txa
asl
tay
lda SPRITE_POS_Y,x
sta 53249,y
rts

<< Content
Step 5 - Play Screen
Our next big step:

Obviously we want some play screen, and more obviously, we are not going to store
full screens (we've got only 64Kb Ram after all). Therefore there's a level build
routine that allows to build a screen with various building elements. For now we'll
start out with vertical and horizontal lines. Since we always have a level border at the
screen edges we'll have the border as a second screen data block
(LEVEL_BORDER_DATA).

To allow faster screen building we also have a table with the precalculated char
offsets of a vertical line (SCREEN_LINE_OFFSET_TABLE_LO and
SCREEN_LINE_OFFSET_TABLE_HI). Note that with the C64s mnemonics the best way
to get stuff done is tables, tables, tables.

The BuildScreen sub routine clears the play screen area, builds the level data and
then the border. The level data is a collection of primitives which are then worked
through until LD_END is hit.

The first addition is the call to the buildroutine:

;setup level
lda #0
sta LEVEL_NR
jsr BuildScreen

The BuildScreen routine sets up a level completely. It starts out with clearing the
current play area (not the full screen). Then LEVEL_NR is used to look up the location
of the level data in table SCREEN_DATA_TABLE.

.BuildLevel is jumped to to actually work through the level primitives and put them
on screen. Then we use LEVEL_BORDER_DATA as second level to display a border on
the screens edges.

.BuildLevel uses Y as index through the data. Depending on the first byte different
routines are called (.LineH, .LineV, .LevelComplete). Since I think levels may be
complex and use more than 256 bytes we add Y to the level data pointers so we can
start out with Y=0 for the next primitive.

;------------------------------------------------------------
;BuildScreen
;creates a screen from level data

<< Content
;------------------------------------------------------------
!zone BuildScreen
BuildScreen
lda #0
ldy #6
jsr ClearPlayScreen

;get pointer to real level data from table


ldx LEVEL_NR
lda SCREEN_DATA_TABLE,x
sta ZEROPAGE_POINTER_1
lda SCREEN_DATA_TABLE + 1,x
sta ZEROPAGE_POINTER_1 + 1
jsr .BuildLevel

;get pointer to real level data from table


lda #<LEVEL_BORDER_DATA
sta ZEROPAGE_POINTER_1
lda #>LEVEL_BORDER_DATA
sta ZEROPAGE_POINTER_1 + 1
jsr .BuildLevel
rts

.BuildLevel
;work through data
ldy #255
.LevelDataLoop
iny
lda (ZEROPAGE_POINTER_1),y
cmp #LD_END
beq .LevelComplete
cmp #LD_LINE_H
beq .LineH
cmp #LD_LINE_V
beq .LineV
.LevelComplete
rts

.NextLevelData
pla

;adjust pointers so we are able to access more


;than 256 bytes of level data
clc
adc #1
adc ZEROPAGE_POINTER_1
sta ZEROPAGE_POINTER_1
lda ZEROPAGE_POINTER_1 + 1
adc #0
sta ZEROPAGE_POINTER_1 + 1
ldy #255

<< Content
jmp .LevelDataLoop
The primitive display routines .LineH and .LineV are rather straight forward. Read the
parameters, and put the character and color values in place.

.LineH
;X pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM1

;Y pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM2

;width
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM3

;char
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM4

;color
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM5

;store target pointers to screen and color ram


ldx PARAM2
lda SCREEN_LINE_OFFSET_TABLE_LO,x
sta ZEROPAGE_POINTER_2
sta ZEROPAGE_POINTER_3
lda SCREEN_LINE_OFFSET_TABLE_HI,x
sta ZEROPAGE_POINTER_2 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8
sta ZEROPAGE_POINTER_3 + 1
tya
pha
ldy PARAM1
.NextChar
lda PARAM4
sta (ZEROPAGE_POINTER_2),y
lda PARAM5
sta (ZEROPAGE_POINTER_3),y
iny
dec PARAM3
bne .NextChar

<< Content
jmp .NextLevelData

.LineV
;X pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM1

;Y pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM2

;height
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM3

;char
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM4

;color
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM5

;store target pointers to screen and color ram


ldx PARAM2
lda SCREEN_LINE_OFFSET_TABLE_LO,x
sta ZEROPAGE_POINTER_2
sta ZEROPAGE_POINTER_3
lda SCREEN_LINE_OFFSET_TABLE_HI,x
sta ZEROPAGE_POINTER_2 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8
sta ZEROPAGE_POINTER_3 + 1
tya
pha

ldy PARAM1
.NextCharV
lda PARAM4
sta (ZEROPAGE_POINTER_2),y
lda PARAM5
sta (ZEROPAGE_POINTER_3),y

;adjust pointer
lda ZEROPAGE_POINTER_2
clc

<< Content
adc #40
sta ZEROPAGE_POINTER_2
sta ZEROPAGE_POINTER_3
lda ZEROPAGE_POINTER_2 + 1
adc #0
sta ZEROPAGE_POINTER_2 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8
sta ZEROPAGE_POINTER_3 + 1
dec PARAM3
bne .NextCharV
jmp .NextLevelData

<< Content
Step 6 - Collision Dectecting
And onwards we go: Obviously we don't want the player to move through
walls. In this step we check the chars in the players way to see if they are
blocking.

To make this easier we store the character pos and the character delta pos (0 to 7)
for x and y for every sprite (SPRITE_CHAR_POS_X, SPRITE_CHAR_POS_X_DELTA). If
the sprite is not at a character brink the move is allowed, if it hits the brink, check the
characters at the target.

For this step any character equal or above index 128 is considered blocking, any
below is free to move.

The collision code assumes that the collision box of a sprite is one char wide and two
chars high.

This step shows:

-calculating character positions while moving about


-checking the position for blocking chars
-calculating the required sprite position from character pos (for starting a sprite at a
specific place)

Since the code is basically the same for all four directions I'll only go into details on
one of them:

;------------------------------------------------------------
;PlayerMoveLeft
;------------------------------------------------------------
!zone PlayerMoveLeft
PlayerMoveLeft
ldx #0

;check if we are on the brink of a character


lda SPRITE_CHAR_POS_X_DELTA
beq .CheckCanMoveLeft

;no, we are not

.CanMoveLeft
dec SPRITE_CHAR_POS_X_DELTA
jsr MoveSpriteLeft
rts

<< Content
.CheckCanMoveLeft
lda SPRITE_CHAR_POS_Y_DELTA
beq .NoThirdCharCheckNeeded

;find the character in the screen buffer


ldy SPRITE_CHAR_POS_Y
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
lda SCREEN_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1
lda SPRITE_CHAR_POS_X
clc
adc #39 ;39 equals one line down (40 chars) and one to
the left (-1)
tay
lda (ZEROPAGE_POINTER_1),y
jsr IsCharBlocking
bne .BlockedLeft

.NoThirdCharCheckNeeded
ldy SPRITE_CHAR_POS_Y
dey
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
lda SCREEN_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1
ldy SPRITE_CHAR_POS_X
dey
lda (ZEROPAGE_POINTER_1),y
jsr IsCharBlocking
bne .BlockedLeft

tya
clc
adc #40
tay
lda (ZEROPAGE_POINTER_1),y
jsr IsCharBlocking
bne .BlockedLeft

lda #8
sta SPRITE_CHAR_POS_X_DELTA
dec SPRITE_CHAR_POS_X
jmp .CanMoveLeft

.BlockedLeft
rts

The subroutine IsCharBlocking is rather primitive, as described it only checks if


the character is smaller than 128:

<< Content
;------------------------------------------------------------
;IsCharBlocking
;checks if a char is blocking
;A contains the character
;returns 1 for blocking, 0 for not blocking
;------------------------------------------------------------
!zone IsCharBlocking
IsCharBlocking
cmp #128
bpl .Blocking

lda #0
rts

.Blocking
lda #1
rts

<< Content
Step 7 - Gravity & Jumping
Now it's starting to resemble a game. Loosely.

In this step we add gravity and jumping. The player will fall if there is no blocking char
below. On joystick up the player jumps in a curve.
Both fall speed and jump speed are non linear and based on tables.

This step shows:

-gravity (accelerating)
-jumping (following a delta y curve)

Most prominent addition are the jump and fall table. These hold the deltas we use to
make the movement not look linear but somewhat naturalistic:

PLAYER_JUMP_POS
!byte 0
PLAYER_JUMP_TABLE
!byte 8,7,5,3,2,1,1,1,0,0
PLAYER_FALL_POS
!byte 0
FALL_SPEED_TABLE
!byte 1,1,2,2,3,3,3,3,3,3

The jump is only possible if the player is not falling. Once the player jumped the
PLAYER_JUMP_POS is increased on every frame and the player moved upwards for
entry-of-jump-table pixel. If the player is blocked moving upwards the jump is
aborted:

.PlayerIsJumping
inc PLAYER_JUMP_POS
lda PLAYER_JUMP_POS
cmp #JUMP_TABLE_SIZE
bne .JumpOn

lda #0
sta PLAYER_JUMP_POS
jmp .JumpComplete

.JumpOn
ldx PLAYER_JUMP_POS
lda PLAYER_JUMP_TABLE,x
beq .JumpComplete

sta PARAM5

<< Content
.JumpContinue
jsr PlayerMoveUp
beq .JumpBlocked
dec PARAM5
bne .JumpContinue
jmp .JumpComplete

.JumpBlocked
lda #0
sta PLAYER_JUMP_POS
jmp .JumpStopped

To check for falling an attempt is made to move the player down one pixel. If he is
blocked he is standing on solid ground. If he can fall the fall counter is increased. The
fall counter is increased in every frame up to the max number of entries in the fall
table.

If the player is falling the player is moved down entry-of-fall-table pixel.

.PlayerFell
ldx PLAYER_FALL_POS
lda FALL_SPEED_TABLE,x
beq .FallComplete
sta PARAM5

.FallLoop
dec PARAM5
beq .FallComplete
jsr PlayerMoveDown
jmp .FallLoop

.FallComplete
lda PLAYER_FALL_POS
cmp #( FALL_TABLE_SIZE - 1 )
beq .FallSpeedAtMax
inc PLAYER_FALL_POS
.FallSpeedAtMax

<< Content
Step 8 - Enemies
Of course a game isn't a game without some challenge. Therefore we need enemies.
Since we have some neat little level build code why not use it for enemies as well?

We add a new level primitive type LD_OBJECT which adds objects (= sprites). We use
it for both player and enemies. A new table SPRITE_ACTIVE is added to see if a sprite
is used (and which type).

One central function to this is FindEmptySpriteSlot. It iterates over the sprite active
table and looks for a free slot to use. If there is a free slot we set the object active,
apply the object startup values and use the previously created
CalcSpritePosFromCharPos to place the sprite.

Note that we don't plan to use more than 8 objects so we can directly map object to
sprites.

Find an empty slot:

;------------------------------------------------------------
;Looks for an empty sprite slot, returns in X
;#1 in A when empty slot found, #0 when full
;------------------------------------------------------------
!zone FindEmptySpriteSlot
FindEmptySpriteSlot
ldx #0
.CheckSlot
lda SPRITE_ACTIVE,x
beq .FoundSlot
inx
cpx #8
bne .CheckSlot
lda #0
rts

.FoundSlot
lda #1
rts

How we add an object during level buildup:

.Object
;X pos
iny
lda (ZEROPAGE_POINTER_1),y

<< Content
sta PARAM1

;Y pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM2

;type
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM3

;store y for later


tya
pha

;add object to sprite array


jsr FindEmptySpriteSlot
beq .NoFreeSlot

lda PARAM3
sta SPRITE_ACTIVE,x

;PARAM1 and PARAM2 hold x,y already


jsr CalcSpritePosFromCharPos

;enable sprite
lda BIT_TABLE,x
ora VIC_SPRITE_ENABLE
sta VIC_SPRITE_ENABLE
lda #SPRITE_PLAYER
sta SPRITE_POINTER_BASE,x

.NoFreeSlot
jmp .NextLevelData

<< Content
Step 9 - Behaviour
What are enemies if they just sit put and don't move at all? Therefore we now add
the sub routine ObjectControl. ObjectControl loops through all objects (even the
player) and jumps to the behaviour function depending on the object type. This
incurs that the behaviour is tied to the object type. We provide a table with function
pointers to every object's behaviour code (including the player).

ObjectControl takes the object type as index into the table and jumps to the target
address. For now we have two enemy types, dumb moving up/down or left/right. For
moving we reuse the previously created functions we already use for the player,
namely ObjectMoveLeft/ObjectMoveRight etc.

Loop over all active objects and jump at their behaviour code. Note that we apply a
nasty trick. Since jsr doesn't allow for indirect jumps we manually push the return
address on the stack and then call indirect jmp. This allows for the behaviour code to
return with rts.

;------------------------------------------------------------
;Enemy Behaviour
;------------------------------------------------------------
!zone ObjectControl
ObjectControl
ldx #0
.ObjectLoop
ldy SPRITE_ACTIVE,x
beq .NextObject

;enemy is active
dey
lda ENEMY_BEHAVIOUR_TABLE_LO,y
sta ZEROPAGE_POINTER_2
lda ENEMY_BEHAVIOUR_TABLE_HI,y
sta ZEROPAGE_POINTER_2 + 1

;set up return address for rts


lda #>( .NextObject - 1 )
pha
lda #<( .NextObject - 1 )
pha
jmp (ZEROPAGE_POINTER_2)

.NextObject
inx
cpx #8
bne .ObjectLoop

<< Content
rts

The main game loop is now changed; removed the call of PlayerControl and
added the call to ObjectControl:

;------------------------------------------------------------
;the main game loop
;------------------------------------------------------------
GameLoop
jsr WaitFrame
jsr ObjectControl
jmp GameLoop

The behaviour table is built from the behaviour code addresses. Actually we
use two tables for high and low byte, this way we don't have to mess with the
index. The < and > operators return the low and high byte of a 16 bit value.

ENEMY_BEHAVIOUR_TABLE_LO
!byte <PlayerControl
!byte <BehaviourDumbEnemyLR
!byte <BehaviourDumbEnemyUD

ENEMY_BEHAVIOUR_TABLE_HI
!byte >PlayerControl
!byte >BehaviourDumbEnemyLR
!byte >BehaviourDumbEnemyUD

<< Content
Step 10 - Enemy Collision
So you found out the enemies couldn't hurt you? Well, we're working towards that
goal in this step. We add collision checks. Since I'm not completely sure about later
changes we are NOT relying on the VICs collision checks but roll our own. Remember
the object size contraints from step #6? We apply those to the object collision checks
as well.

We add a new subroutine CheckCollisions which in turn uses


IsEnemyCollidingWithPlayer. We do not check for collisions between enemies. The
check is not completely exact (behaves more like 9 pixel * 16 pixel), but that's good
enough. To test the function a collision is signalled by setting the border color to
white.

The routine CheckCollisions is simply added to the main game loop:

GameLoop
jsr WaitFrame
jsr ObjectControl
jsr CheckCollisions
jmp GameLoop

The function CheckCollision just loops through the active object list and calls
IsEnemyCollidingWithPlayer for every active entry:

;------------------------------------------------------------
;check object collisions (enemy vs. player etc.)
;x
;------------------------------------------------------------
CheckCollisions
ldx #1
.CollisionLoop
lda SPRITE_ACTIVE,x
bne .CheckObject

.NextObject
inx
cpx #8
bne .CollisionLoop

lda #0
sta VIC_BORDER_COLOR
rts

.CheckObject
stx PARAM2

<< Content
jsr IsEnemyCollidingWithPlayer
bne .PlayerCollidedWithEnemy

ldx PARAM2
jmp .NextObject

.PlayerCollidedWithEnemy
lda #1
sta VIC_BORDER_COLOR
;ldx #0
;jsr RemoveObject
rts

IsEnemyCollidingWithPlayer employs a few tricks to ease the calculation.

First we do the Y coordinate check to weed out. For the X coordinate: Since the
actual X position is 9 bits we half the value (half the X coordinate and add 128 if the
extended X bit is set). Now the comparation is easy.

The routine then returns 1 if a collision occurs and 0 if not.

;------------------------------------------------------------
;check object collision with player (object 0)
;x = enemy index
;return a = 1 when colliding, a = 0 when not
;------------------------------------------------------------
!zone IsEnemyCollidingWithPlayer

.CalculateSimpleXPos
;Returns a with simple x pos (x halved + 128 if > 256)
;modifies y
lda BIT_TABLE,x
and SPRITE_POS_X_EXTEND
beq .NoXBit

lda SPRITE_POS_X,x
lsr
clc
adc #128
rts

.NoXBit
lda SPRITE_POS_X,x
lsr
rts

IsEnemyCollidingWithPlayer
;modifies X
;check y pos
lda SPRITE_POS_Y,x

<< Content
sec
sbc #( OBJECT_HEIGHT )

;offset to bottom
cmp SPRITE_POS_Y
bcs .NotTouching
clc
adc #( OBJECT_HEIGHT + OBJECT_HEIGHT - 1 )
cmp SPRITE_POS_Y
bcc .NotTouching

;X = Index in enemy-table
jsr .CalculateSimpleXPos
sta PARAM1

ldx #0
jsr .CalculateSimpleXPos
sec
sbc #4

;position X-Anfang Player - 12 Pixel


cmp PARAM1
bcs .NotTouching
adc #8
cmp PARAM1
bcc .NotTouching

lda #1
rts

.NotTouching
lda #0
rts

<< Content
Step 11 - Kill Player
From colliding to dying is a small step. Once the player collides with an enemy we kill
him by removing the player object. A "Press Fire to Restart" message is displayed and
a press on the button will revive the player object.

We add the function RemoveObject which simply removes the object from the
SPRITE_ACTIVE table and disables its sprite. While we wait for the player to press the
button all the rest of the game moves on.

First of all we add the getting killed part in our old routine "CheckCollisions". Nothing
ground breaking, a call to the text display function follows by removing the object
and resetting the button released flag.

.PlayerCollidedWithEnemy
;display text
lda #<TEXT_PRESS_FIRE
sta ZEROPAGE_POINTER_1
lda #>TEXT_PRESS_FIRE
sta ZEROPAGE_POINTER_1 + 1
lda #10
sta PARAM1
lda #23
sta PARAM2
jsr DisplayText

ldx #0
stx BUTTON_PRESSED
stx BUTTON_RELEASED
jsr RemoveObject
rts

A new call is added to the main game loop which controls behaviour when the player
is dead:

GameLoop
jsr WaitFrame
jsr DeadControl
jsr ObjectControl
jsr CheckCollisions
jmp GameLoop

Surprisingly easy. We check if the player is really dead, if he isn't, bail out. Then we
check for the joystick button being pressed, but only allow to go on, if the button has
been released before. If all that happened, we simply force the player object back

<< Content
into life (for now with hard coded values).

!zone DeadControl
DeadControl
lda SPRITE_ACTIVE
beq .PlayerIsDead
rts

.PlayerIsDead
lda #$10
bit $dc00
bne .ButtonNotPressed

;button pushed
lda BUTTON_RELEASED
bne .Restart
rts

.ButtonNotPressed
lda #1
sta BUTTON_RELEASED
rts

.Restart
lda #5
sta PARAM1
lda #4
sta PARAM2

;type
lda #TYPE_PLAYER
sta PARAM3
ldx #0
lda PARAM3
sta SPRITE_ACTIVE,x

;PARAM1 and PARAM2 hold x,y already


jsr CalcSpritePosFromCharPos

;enable sprite
lda BIT_TABLE,x
ora VIC_SPRITE_ENABLE
sta VIC_SPRITE_ENABLE

;initialise enemy values


lda #SPRITE_PLAYER
sta SPRITE_POINTER_BASE,x

;look right per default


lda #0
sta SPRITE_DIRECTION,x

<< Content
rts

<< Content
Step 12 - Shooting
One of the more complex steps. And also one I someday need to heavily optimize.
The player can now shoot an enemy.

The central function for this is FireShot. We don't use a bullet but insta-shot.
However walls should block the shot as well. This means, we need to take the current
player direction and position, and work our way to the left/right until we hit an
enemy or a wall.

Since there's no direct collision involved we take the character pos of the player, add
or decrease the x pos and compare against all alive enemies pos. Rinse and repeat
until done.

I've given the enemies 5 HP, and the player has a shot delay of 10 frames. Therefore
it takes a while for the enemy to disappear (best tested with the box on top). If the
player is purple the shot delay is active.

We start with adding a fire delay to PlayerControl:

​ lda PLAYER_SHOT_PAUSE
bne .FirePauseActive

lda #1
sta VIC_SPRITE_COLOR
lda #$10
bit $dc00
bne .NotFirePushed
jsr FireShot
jmp .FireDone

.FirePauseActive
dec PLAYER_SHOT_PAUSE
.FireDone
.NotFirePushed

This simply checks for PLAYER_SHOT_PAUSE. If it is higher than 0 the player is still
pausing. If so the counter is decreased and the fire function skipped. If the counter is
zero, we check the fire button and if pressed call the FireShot routine.

The FireShot routine is not that complicated, however it's taking its processing time.
First set the fire pause to 10 frames. Mark the player as shooting by changing his
color.

<< Content
Now the hard part. There is no visible bullet. So we take the current player position,
increase/decrease X and check for a blocking char or a hittable enemy. If the bullet is
blocked, done. If an enemy is hit, decrease its health by one point. Once the health is
down to zero the enemy is removed.

!zone FireShotFireShot
;frame delay until next shot
lda #10
sta PLAYER_SHOT_PAUSE

;mark player as shooting


lda #4
sta VIC_SPRITE_COLOR
ldy SPRITE_CHAR_POS_Y
dey
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
lda SCREEN_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1

ldy SPRITE_CHAR_POS_X

.ShotContinue
lda SPRITE_DIRECTION
beq .ShootRight

;shooting left
dey
lda (ZEROPAGE_POINTER_1),y
jsr IsCharBlocking
bne .ShotDone

jmp .CheckHitEnemy

.ShootRight
iny
lda (ZEROPAGE_POINTER_1),y
jsr IsCharBlocking
bne .ShotDone

.CheckHitEnemy
;hit an enemy?
ldx #1
.CheckEnemy
stx PARAM2
lda SPRITE_ACTIVE,x
beq .CheckNextEnemy
tax
lda IS_TYPE_ENEMY,x
beq .CheckNextEnemy

<< Content
;sprite pos matches on x?
ldx PARAM2
sty PARAM1
lda SPRITE_CHAR_POS_X,x
cmp PARAM1
bne .CheckNextEnemy

;sprite pos matches on y?


lda SPRITE_CHAR_POS_Y,x
cmp SPRITE_CHAR_POS_Y
beq .EnemyHit

;sprite pos matches on y + 1?


clc
adc #1
cmp SPRITE_CHAR_POS_Y
beq .EnemyHit

;sprite pos matches on y - 1?


sec
sbc #2
cmp SPRITE_CHAR_POS_Y
bne .CheckNextEnemy

.EnemyHit
;enemy hit!
dec SPRITE_HP,x
lda SPRITE_HP,x
beq .EnemyKilled
jmp .ShotDone

.EnemyKilled
jsr RemoveObject
jmp .ShotDone

.CheckNextEnemy
ldx PARAM2
inx
cpx #8
bne .CheckEnemy

jmp .ShotContinue

.ShotDone
rts

<< Content
Step 13 - Items
And yet again a bigger step. Of course we need lots of goodies from the killed
enemies. We add items. Items are displayed as 2x2 block of characters. There's a new
list of possible items with location added (ITEM_ACTIVE, etc.). The list also stores the
original background behind the items.

To get an item spawned just walk beside one of the enemies (look in their direction)
and keep fire pressed until it dies.

Note that the player cannot collect the items yet (that's up for the next step).

Inside the player subroutine FireShot we add another subroutine call after killing an
enemy:

.EnemyKilled
jsr RemoveObject
jsr SpawnItem

SpawnItem itself resembles the AddObject routine. First we loop over the active item
table (ITEM_ACTIVE) to find a free slot.

Once found we randomly chose the item type (for now there are two types). The ugly
part below that stores the original character and color at the item position and puts
the items char and color in its place. Remember the item is sized 2x2 chars, so we
need to store 8 bytes overall. However to keep the code comfortable, we actually use
8 tables. This allows use to only work with the item index instead of manually
accessing a second index. There's only two index registers after all.

;------------------------------------------------------------
;spawns an item at char position from object x
;X = object index
;------------------------------------------------------------
!zone SpawnItem
SpawnItem
;find free item slot
ldy #0
.CheckNextItemSlot
lda ITEM_ACTIVE,y
cmp #ITEM_NONE
beq .FreeSlotFound
iny
cpy #ITEM_COUNT
bne .CheckNextItemSlot
rts

<< Content
.FreeSlotFound
jsr GenerateRandomNumber
and #$1
sta ITEM_ACTIVE,y
lda SPRITE_CHAR_POS_X,x
sta ITEM_POS_X,y
lda SPRITE_CHAR_POS_Y,x
sta ITEM_POS_Y,y
sty PARAM1

;find address in screen buffer...


tay
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
sta ZEROPAGE_POINTER_2
lda SCREEN_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1

;...and for the color buffer


clc
adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8
sta ZEROPAGE_POINTER_2 + 1
ldy SPRITE_CHAR_POS_X,x
ldx PARAM1

;store old background and put item


;we do not take overlapping items in account yet!
lda (ZEROPAGE_POINTER_1),y
sta ITEM_BACK_CHAR_UL,x
lda (ZEROPAGE_POINTER_2),y
sta ITEM_BACK_COLOR_UL,x
lda ITEM_CHAR_UL,x
sta (ZEROPAGE_POINTER_1),y
lda ITEM_COLOR_UL,x
sta (ZEROPAGE_POINTER_2),y
iny
lda (ZEROPAGE_POINTER_1),y
sta ITEM_BACK_CHAR_UR,x
lda (ZEROPAGE_POINTER_2),y
sta ITEM_BACK_COLOR_UR,x
lda ITEM_CHAR_UR,x
sta (ZEROPAGE_POINTER_1),y
lda ITEM_COLOR_UR,x
sta (ZEROPAGE_POINTER_2),y
tya
clc
adc #39
tay
lda (ZEROPAGE_POINTER_1),y
sta ITEM_BACK_CHAR_LL,x

<< Content
lda (ZEROPAGE_POINTER_2),y
sta ITEM_BACK_COLOR_LL,x
lda ITEM_CHAR_LL,x
sta (ZEROPAGE_POINTER_1),y
lda ITEM_COLOR_LL,x
sta (ZEROPAGE_POINTER_2),y
iny
lda (ZEROPAGE_POINTER_1),y
sta ITEM_BACK_CHAR_LR,x
lda (ZEROPAGE_POINTER_2),y
sta ITEM_BACK_COLOR_LR,x
lda ITEM_CHAR_LR,x
sta (ZEROPAGE_POINTER_1),y
lda ITEM_COLOR_LR,x
sta (ZEROPAGE_POINTER_2),y
rts

<< Content
Step 14 - Level Buffer
And onwards we go. Picking up items showed a new problem. When an item is
picked up we want the background behind restored (plus the item characters should
not cut holes into the play field, allowing the player to fall through floors).

In the end I decided to have a second back buffer screen that contains the original
play screen. Now every time an item is removed the character blocks are copied from
the back buffer. Also, the back buffer is now used for collision detection. I could not
avoid having to redraw the still existing item images in case the removed item was
overlapping.

Effectively we double the work during level building. We start out with the new
"buffers":

;address of the screen backbuffer


SCREEN_BACK_CHAR = $C800

;address of the screen backbuffer


SCREEN_BACK_COLOR = $C400

After calling the BuildScreen subroutine we copy the screen and color RAM to the
backup buffers. Note the check for 230 bytes. We only have a play field of 40x23
characters, so only 4 * 230 = 920 bytes are needed.

;copy level data to back buffer


ldx #$00
.ClearLoop
lda SCREEN_CHAR,x
sta SCREEN_BACK_CHAR,x
lda SCREEN_CHAR + 230,x
sta SCREEN_BACK_CHAR + 230,x
lda SCREEN_CHAR + 460,x
sta SCREEN_BACK_CHAR + 460,x
lda SCREEN_CHAR + 690,x
sta SCREEN_BACK_CHAR + 690,x
inx
cpx #230
bne .ClearLoop

ldx #$00
.ColorLoop
lda SCREEN_COLOR,x
sta SCREEN_BACK_COLOR,x
lda SCREEN_COLOR + 230,x
sta SCREEN_BACK_COLOR + 230,x

<< Content
lda SCREEN_COLOR + 460,x
sta SCREEN_BACK_COLOR + 460,x
lda SCREEN_COLOR + 690,x
sta SCREEN_BACK_COLOR + 690,x
inx
cpx #230
bne .ColorLoop

The repaint item function is thusly modified to simply copy the character and color
values from the backup buffer:

;------------------------------------------------------------
;remove item image from screen
;Y = item index
;------------------------------------------------------------
!zone RemoveItemImage
RemoveItemImage
sty PARAM2

;set up pointers
lda ITEM_POS_Y,y
tay
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
sta ZEROPAGE_POINTER_2
sta ZEROPAGE_POINTER_3
sta ZEROPAGE_POINTER_4
lda SCREEN_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8
sta ZEROPAGE_POINTER_2 + 1
sec
sbc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) & 0xff00 ) >>
8
sta ZEROPAGE_POINTER_3 + 1
sec
sbc #( ( SCREEN_BACK_CHAR - SCREEN_BACK_COLOR ) & 0xff00
) >> 8
sta ZEROPAGE_POINTER_4 + 1
ldx PARAM2
ldy ITEM_POS_X,x

;... and copying


lda (ZEROPAGE_POINTER_4),y
sta (ZEROPAGE_POINTER_2),y
lda (ZEROPAGE_POINTER_3),y
sta (ZEROPAGE_POINTER_1),y
iny
lda (ZEROPAGE_POINTER_4),y
sta (ZEROPAGE_POINTER_2),y

<< Content
lda (ZEROPAGE_POINTER_3),y
sta (ZEROPAGE_POINTER_1),y
tya
clc
adc #39
tay
lda (ZEROPAGE_POINTER_4),y
sta (ZEROPAGE_POINTER_2),y
lda (ZEROPAGE_POINTER_3),y
sta (ZEROPAGE_POINTER_1),y
iny
lda (ZEROPAGE_POINTER_4),y
sta (ZEROPAGE_POINTER_2),y
lda (ZEROPAGE_POINTER_3),y
sta (ZEROPAGE_POINTER_1),y

;repaint other items to avoid broken overlapped items


ldx #0
.RepaintLoop
lda ITEM_ACTIVE,x
cmp #ITEM_NONE
beq .RepaintNextItem
txa
pha
jsr PutItemImage
pla
tax
.RepaintNextItem
inx
cpx #ITEM_COUNT
bne .RepaintLoop

ldy PARAM2
rts

<< Content
Step 15 - Next Level
Now we start with a few game essentials: Progressing to the next level.

Not too complicated. We keep a counter of enemies alive


(NUMBER_ENEMIES_ALIVE) which is initially set to 0. Since we already have a lookup
table IS_TYPE_ENEMY we simply add a check inside AddObject. If the new object is
an enemy, increase the counter:

;adjust enemy counter


ldx PARAM3
lda IS_TYPE_ENEMY,x
beq .NoEnemy

inc NUMBER_ENEMIES_ALIVE
.NoEnemy

The other spot where this comes in is when we kill an enemy. Inside our FireShot
routine we add:

.EnemyKilled
ldy SPRITE_ACTIVE,x
lda IS_TYPE_ENEMY,y
beq .NoEnemy

dec NUMBER_ENEMIES_ALIVE

.NoEnemy
jsr RemoveObject

For the level change we add a new control routine (GameFlowControl) in the main
game loop. Once the enemy count reaches 0 we increase a level done delay so we
don't immediately jump onwards. Once reached, disable all sprites, build next level
and continue.

For now there's two simple levels with the last looping back to the first.

;------------------------------------------------------------
;the main game loop
;------------------------------------------------------------
GameLoop
jsr WaitFrame
jsr GameFlowControl
jsr DeadControl
jsr ObjectControl
jsr CheckCollisions

<< Content
jmp GameLoop

;------------------------------------------------------------
;controls the game flow
;------------------------------------------------------------
!zone GameFlowControl
GameFlowControl
inc DELAYED_GENERIC_COUNTER
lda DELAYED_GENERIC_COUNTER
cmp #8
bne .NoTimedActionYet

lda #0
sta DELAYED_GENERIC_COUNTER

;level done delay


lda NUMBER_ENEMIES_ALIVE
bne .NotDoneYet

inc LEVEL_DONE_DELAY
lda LEVEL_DONE_DELAY
cmp #20
beq .GoToNextLevel

inc VIC_BORDER_COLOR

.NotDoneYet
.NoTimedActionYet
rts

.GoToNextLevel
lda #0
sta VIC_SPRITE_ENABLE
inc LEVEL_NR
jsr BuildScreen
jsr CopyLevelToBackBuffer
rts

<< Content
Step 16 - Score/Lives
Now for some rather small addition, which however feels like a bigger step:
Score/Live/Level display.

We already have a text display function, so we add a new default text for the display
with initial 00000 values. Note that the score doesn't fit into a byte easily. We only
update the numbers on the screen, we do not store the score in another location.

This makes it quite easy to update. For every step we start at the right most digit and
increase it. If it hits the digit after '9', set to '0' again and repeat the step on char to
the left. For retro sake we don't start at the right most score digit, but the second
right most (making increase steps always being 10). If you look closer at a lot of older
games you'll see that their right most score digit never changes (Bubble Bobble, etc.)

Small text entry:

TEXT_DISPLAY
!text " SCORE: 000000 LIVES: 03 LEVEL: 00 *"

Increase score bit:

;------------------------------------------------------------
;increases score by A
;note that the score is only shown
; not held in a variable
;------------------------------------------------------------
!zone IncreaseScore
IncreaseScore
sta PARAM1
stx PARAM2
sty PARAM3
.IncreaseBy1
ldx #4
.IncreaseDigit
inc SCREEN_CHAR + ( 23 * 40 + 8 ),x
lda SCREEN_CHAR + ( 23 * 40 + 8 ),x
cmp #58
bne .IncreaseBy1Done

;looped digit, increase next


lda #48
sta SCREEN_CHAR + ( 23 * 40 + 8 ),x
dex

;TODO - this might overflow

<< Content
jmp .IncreaseDigit

.IncreaseBy1Done
dec PARAM1
bne .IncreaseBy1

;increase complete, restore x,y


ldx PARAM2
ldy PARAM3
rts

Another neat effect is the display of the level number and lives. Due to the hard
coded screen position I've made two specialized functions instead of a generic one.

Interesting anecdote:

When I first had to display a decimal number I was stumped due to no available div
operator. You actually need to divide by yourself (subtract divisor and increase count
until done). That's what the call to DivideBy10 does.

;------------------------------------------------------------
;displays level number
;------------------------------------------------------------
!zone DisplayLevelNumber
DisplayLevelNumber
lda LEVEL_NR
clc
adc #1
jsr DivideBy10
pha

;10 digit
tya
clc
adc #48
sta SCREEN_CHAR + ( 23 * 40 + 37 )
pla
clc
adc #48
sta SCREEN_CHAR + ( 23 * 40 + 38 )
rts

;------------------------------------------------------------
;displays live number
;------------------------------------------------------------
!zone DisplayLiveNumber
DisplayLiveNumber
lda PLAYER_LIVES
jsr DivideBy10
pha

<< Content
;10 digit
tya
clc
adc #48
sta SCREEN_CHAR + ( 23 * 40 + 24 )
pla
clc
adc #48
sta SCREEN_CHAR + ( 23 * 40 + 25 )
rts

;------------------------------------------------------------
;divides A by 10
;returns remainder in A
;returns result in Y
;------------------------------------------------------------
!zone DivideBy10
DivideBy10
sec
ldy #$FF

.divloop
iny
sbc #10
bcs .divloop

adc #10
rts

<< Content
Step 17 - Player Animation
Another rather small step, but visually pleasing. We're enhancing the player sprite
with animation and better jump abilities.

All the hard work is added to PlayerControl. On every movement we update the
sprite while checking the player states like jumping, recoil, falling, etc. Suddenly
things look more interesting ;-)

It's basically updating and checking counters during different control parts.
SPRITE_ANIM_DELAY is used for controlling animation speed while
SPRITE_ANIM_POS is used for the animation frame.

Here are the new parts for walking left:

;animate player
lda SPRITE_FALLING
bne .NoAnimLNeeded

lda PLAYER_JUMP_POS
bne .NoAnimLNeeded

inc SPRITE_ANIM_DELAY
lda SPRITE_ANIM_DELAY
cmp #8
bne .NoAnimLNeeded

lda #0
sta SPRITE_ANIM_DELAY
inc SPRITE_ANIM_POS
lda SPRITE_ANIM_POS
and #$3
sta SPRITE_ANIM_POS

.NoAnimLNeeded

The same for right movement:

;animate player
lda SPRITE_FALLING
bne .NoAnimRNeeded
lda PLAYER_JUMP_POS
bne .NoAnimRNeeded

inc SPRITE_ANIM_DELAY
lda SPRITE_ANIM_DELAY

<< Content
cmp #8
bne .NoAnimRNeeded

lda #0
sta SPRITE_ANIM_DELAY

inc SPRITE_ANIM_POS
lda SPRITE_ANIM_POS
and #$3
sta SPRITE_ANIM_POS

.NoAnimRNeeded

And all the missing animation for jumping, falling, recoil and combined states. Note
that the sprites are arranged in right/left pairs, so that adding SPRITE_DIRECTION (0 =
facing right, 1 = facing left) to the sprite frame results in the proper sprite.

;update player animation


lda SPRITE_FALLING
bne .AnimFalling

lda PLAYER_JUMP_POS
bne .AnimJumping

;is player shooting?


lda PLAYER_SHOT_PAUSE
beq .AnimNoRecoil

;recoil anim
lda SPRITE_ANIM_POS
asl
clc
adc SPRITE_DIRECTION
adc #SPRITE_PLAYER_WALK_R_1
adc #8
sta SPRITE_POINTER_BASE
rts

.AnimNoRecoil
lda SPRITE_ANIM_POS
asl
clc
adc SPRITE_DIRECTION
adc #SPRITE_PLAYER_WALK_R_1
sta SPRITE_POINTER_BASE
rts

.AnimFalling
lda PLAYER_SHOT_PAUSE
bne .AnimFallingNoRecoil

<< Content
lda #SPRITE_PLAYER_FALL_R
clc
adc SPRITE_DIRECTION
sta SPRITE_POINTER_BASE
rts

.AnimFallingNoRecoil
lda #SPRITE_PLAYER_FALL_RECOIL_R
clc
adc SPRITE_DIRECTION
sta SPRITE_POINTER_BASE
rts

.AnimJumping
lda PLAYER_SHOT_PAUSE
bne .AnimJumpingNoRecoil

lda #SPRITE_PLAYER_JUMP_R
clc
adc SPRITE_DIRECTION
sta SPRITE_POINTER_BASE
rts

.AnimJumpingNoRecoil
lda #SPRITE_PLAYER_JUMP_RECOIL_R
clc
adc SPRITE_DIRECTION
sta SPRITE_POINTER_BASE
rts

<< Content
Step 18 - Enemy Animation
This time we add some enemy animation and a path based movement enemy type.
The movement path is stored in a table of delta X and delta Y values. Values with the
highest bit set are treated as negative.

The animation of the bat is also stored in a table (it's a simple ping pong loop).

Every objects get an animation delay (SPRITE_ANIM_DELAY), animation pos


(SPRITE_ANIM_POS) and movement pos counter (SPRITE_MOVE_POS).

Remember, adding a new type means just adding the new constant and entries to
the startup value tables.

If you wonder about the flickering white border on the bottom half: It's an easy way
to see how long the actual per frame code runs. You'll notice more complex code
taking quite a bit more time.

Here's a detailed look at the path code. It's actually pretty straight forward. Read the
next byte. Check if the high bit is set and use the result to either move left/right.
Rinse and repeat for Y.

;------------------------------------------------------------
;move in flat 8
;------------------------------------------------------------
!zone BehaviourBat8
BehaviourBat8
;do not update animation too fast
lda DELAYED_GENERIC_COUNTER
and #$3
bne .NoAnimUpdate
inc SPRITE_ANIM_POS,x
lda SPRITE_ANIM_POS,x
and #$3
sta SPRITE_ANIM_POS,x
tay
lda BAT_ANIMATION,y
sta SPRITE_POINTER_BASE,x
.NoAnimUpdate
inc SPRITE_MOVE_POS,x
lda SPRITE_MOVE_POS,x
and #31
sta SPRITE_MOVE_POS,x
;process next path pos
tay

<< Content
lda PATH_8_DX,y
beq .NoXMoveNeeded

sta PARAM1
and #$80
beq .MoveRight

;move left
lda PARAM1
and #$7f
sta PARAM1

.MoveLeft
jsr MoveSpriteLeft
dec PARAM1
bne .MoveLeft
jmp .XMoveDone

.MoveRight
jsr MoveSpriteRight
dec PARAM1
bne .MoveRight

.NoXMoveNeeded
.XMoveDone
ldy SPRITE_MOVE_POS,x
lda PATH_8_DY,y
beq .NoYMoveNeeded
sta PARAM1
and #$80
beq .MoveDown

;move up
lda PARAM1
and #$7f
sta PARAM1

.MoveUp
jsr MoveSpriteUp
dec PARAM1
bne .MoveUp
rts

.MoveDown
jsr MoveSpriteDown
dec PARAM1
bne .MoveDown

.NoYMoveNeeded
rts

<< Content
The tables themselves are handmade. For the planned path we just need to
make sure we end up where we started:

PATH_8_DX
!byte $86
!byte $86
!byte $85
!byte $84
!byte $83
!byte $82
!byte $81
!byte 0
!byte 0
!byte 1
!byte 2
!byte 3
!byte 4
!byte 5
!byte 6
!byte 6
!byte 6
!byte 6
!byte 5
!byte 4
!byte 3
!byte 2
!byte 1
!byte 0
!byte 0
!byte $81
!byte $82
!byte $83
!byte $84
!byte $85
!byte $86
!byte $86

PATH_8_DY
!byte 0
!byte 1
!byte 2
!byte 3
!byte 4
!byte 5
!byte 6
!byte 6
!byte 6
!byte 6
!byte 5
!byte 4
!byte 3

<< Content
!byte 2
!byte 1
!byte 0
!byte 0
!byte $81
!byte $82
!byte $83
!byte $84
!byte $85
!byte $86
!byte $86
!byte $86
!byte $86
!byte $85
!byte $84
!byte $83
!byte $82
!byte $81
!byte 0

<< Content
Step 19 - Enemy (Mummy)
Nothing really new this time, just constant use of existing parts. A new enemy, some
sort of mummy, which slowly walks back and forth. And takes a few more hits than
the bats.

It's a new behaviour added to the table and some extra code that checks for gaps
before walking.

Adding the enemy itself is nothing more than adding new entries to the existing
tables

;object type constants


TYPE_PLAYER = 1
TYPE_BAT_LR = 2
TYPE_BAT_UD = 3
TYPE_BAT_8 = 4
TYPE_MUMMY = 5

ENEMY_BEHAVIOUR_TABLE_LO
!byte <PlayerControl
!byte <BehaviourBatLR
!byte <BehaviourBatUD
!byte <BehaviourBat8
!byte <BehaviourMummy

ENEMY_BEHAVIOUR_TABLE_HI
!byte >PlayerControl
!byte >BehaviourBatLR
!byte >BehaviourBatUD
!byte >BehaviourBat8
!byte >BehaviourMummy

IS_TYPE_ENEMY
!byte 0 ;dummy entry for inactive object
!byte 0 ;player
!byte 1 ;bat_lr
!byte 1 ;bat_ud
!byte 1 ;bat 8
!byte 1 ;mummy

TYPE_START_SPRITE
!byte 0 ;dummy entry for inactive object
!byte SPRITE_PLAYER_STAND_R
!byte SPRITE_BAT_1
!byte SPRITE_BAT_1
!byte SPRITE_BAT_2

<< Content
!byte SPRITE_MUMMY_R_1

TYPE_START_COLOR
!byte 0
!byte 10
!byte 3
!byte 3
!byte 8
!byte 1

TYPE_START_MULTICOLOR
!byte 0
!byte 1
!byte 0
!byte 0
!byte 0
!byte 0

TYPE_START_HP
!byte 0
!byte 1
!byte 5
!byte 5
!byte 5
!byte 10

The behaviour code looks quite simple, as most of the new code is inside subroutines
ObjectWalkLeft/ObjectWalkRight.

;------------------------------------------------------------
;simply walk left/right, do not fall off
;------------------------------------------------------------
!zone BehaviourMummy
BehaviourMummy
lda DELAYED_GENERIC_COUNTER
and #$3
beq .MovementUpdate
rts

.MovementUpdate
inc SPRITE_MOVE_POS,x
lda SPRITE_MOVE_POS,x
and #$7
sta SPRITE_MOVE_POS,x
cmp #4
bpl .CanMove
rts

.CanMove
lda SPRITE_DIRECTION,x
beq .MoveRight

<< Content
;move left
jsr ObjectWalkLeft
beq .ToggleDirection

rts

.MoveRight
jsr ObjectWalkRight
beq .ToggleDirection
rts

.ToggleDirection
lda SPRITE_DIRECTION,x
eor #1
sta SPRITE_DIRECTION,x
rts

The new subroutines are a more complex version of


ObjectMoveLeft/ObjectMoveRight. They add a new check, so the enemy only walks
forward, if it's not blocked and there is no gap in the floor in front:

;------------------------------------------------------------
;walk object right if not blocked, do not fall off
;x = object index
;------------------------------------------------------------
!zone ObjectWalkRight
ObjectWalkRight
lda SPRITE_CHAR_POS_X_DELTA,x
beq .CheckCanMoveRight

.CanMoveRight
inc SPRITE_CHAR_POS_X_DELTA,x
lda SPRITE_CHAR_POS_X_DELTA,x
cmp #8
bne .NoCharStep

lda #0
sta SPRITE_CHAR_POS_X_DELTA,x
inc SPRITE_CHAR_POS_X,x

.NoCharStep
jsr MoveSpriteRight
lda #1
rts

.CheckCanMoveRight
ldy SPRITE_CHAR_POS_Y,x
iny
lda SCREEN_LINE_OFFSET_TABLE_LO,y

<< Content
sta ZEROPAGE_POINTER_1
lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1
ldy SPRITE_CHAR_POS_X,x
iny
lda (ZEROPAGE_POINTER_1),y
jsr IsCharBlockingFall
beq .BlockedRight

ldy SPRITE_CHAR_POS_Y,x
dey
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1
ldy SPRITE_CHAR_POS_X,x
iny
lda (ZEROPAGE_POINTER_1),y
jsr IsCharBlocking
bne .BlockedRight
tya
clc
adc #40
tay
lda (ZEROPAGE_POINTER_1),y
jsr IsCharBlocking
bne .BlockedRight
jmp .CanMoveRight

.BlockedRight
lda #0
rts

<< Content
Step 20 - Effects
More refinement, this time with more impact on the gameplay. For one, the shotgun
needs to be reloaded (stand still) and the collected items now also have an effect.
Namely a new bullet slot and invincibility.

Adding the reload part is done with the following code piece. If the joystick is not
moved and the player isn't waiting for the recoil to end the variable
PLAYER_STAND_STILL_TIME is increased. Once it hits 40 frames one shell is added.

;check if player moved


lda $dc00
and #$1f
cmp #$1f
bne .PlayerMoved

;do not reload while recoil


lda PLAYER_SHOT_PAUSE
bne .PlayerMoved
inc PLAYER_STAND_STILL_TIME
lda PLAYER_STAND_STILL_TIME
cmp #40
bne .HandleFire

;reload
lda #1
sta PLAYER_STAND_STILL_TIME

;already fully loaded?


lda PLAYER_SHELLS
cmp PLAYER_SHELLS_MAX
beq .HandleFire

inc PLAYER_SHELLS

;display loaded shells


ldy PLAYER_SHELLS
lda #2
sta SCREEN_COLOR + 23 * 40 + 18,y
lda #7
sta SCREEN_COLOR + 24 * 40 + 18,y

The other addition enhances the PickItem routine. Simply check the picked item type,
and either add a new bullet slot or increase the player live counter.

!zone PickItem
PickItem

<< Content
lda ITEM_ACTIVE,y
cmp #ITEM_BULLET
beq .EffectBullet
cmp #ITEM_HEALTH
beq .EffectHealth

.RemoveItem
lda #ITEM_NONE
sta ITEM_ACTIVE,y
lda #3
jsr IncreaseScore
jsr RemoveItemImage
rts

.EffectBullet
lda PLAYER_SHELLS_MAX
cmp #5
beq .RemoveItem

ldx PLAYER_SHELLS_MAX
lda #224
sta SCREEN_CHAR + 23 * 40 + 19,x
lda #225
sta SCREEN_CHAR + 24 * 40 + 19,x
lda #6
sta SCREEN_COLOR + 23 * 40 + 19,x
sta SCREEN_COLOR + 24 * 40 + 19,x
inc PLAYER_SHELLS_MAX
jmp .RemoveItem

.EffectHealth
lda PLAYER_LIVES
cmp #99
beq .RemoveItem
inc PLAYER_LIVES
sty PARAM1
jsr DisplayLiveNumber
ldy PARAM1
jmp .RemoveItem

<< Content
Step 21 - Title Screen
Now some more tidbits. A game is not complete without a neat title screen: For now
this is nothing more than a almost empty screen and a separate control loop. The
loop simply waits for a button press and then jumps to the game main loop. Once the
player lost all lives he is returned to the title loop.

Note that for both cases we also check if the button has been released before
allowing to go forward. Nothing's more annyoing than accidental commencing.

Rather unspectacular, the title screen code. First the screen is displayed and then the
check button loop entered. Once the button has been pressed the game variables are
reset and the code runs into the main game loop.

;------------------------------------------------------------
;the title screen game loop
;------------------------------------------------------------
!zone TitleScreen
TitleScreen
ldx #0
stx BUTTON_PRESSED
stx BUTTON_RELEASED
sta VIC_SPRITE_ENABLE

;clear screen
lda #32
ldy #0
jsr ClearScreen

;display title logo


lda #<TEXT_TITLE
sta ZEROPAGE_POINTER_1
lda #>TEXT_TITLE
sta ZEROPAGE_POINTER_1 + 1
lda #0
sta PARAM1
lda #1
sta PARAM2
jsr DisplayText

;display start text


lda #<TEXT_FIRE_TO_START
sta ZEROPAGE_POINTER_1
lda #>TEXT_FIRE_TO_START
sta ZEROPAGE_POINTER_1 + 1
lda #11
sta PARAM1

<< Content
lda #23
sta PARAM2
jsr DisplayText

.TitleLoop
jsr WaitFrame
lda #$10
bit $dc00
bne .ButtonNotPressed

;button pushed
lda BUTTON_RELEASED
bne .Restart
jmp .TitleLoop

.ButtonNotPressed
lda #1
sta BUTTON_RELEASED
jmp .TitleLoop

.Restart
;game start values
lda #3
sta PLAYER_LIVES

;setup level
jsr StartLevel
lda #0
sta LEVEL_NR
jsr BuildScreen
jsr CopyLevelToBackBuffer

;------------------------------------------------------------
;the main game loop
;------------------------------------------------------------

Of course the counter part needs to be added to the DeadControl routine. If the
player lost his last life, return to the title screen:

!zone DeadControl
DeadControl
lda SPRITE_ACTIVE
beq .PlayerIsDead
rts

.PlayerIsDead
lda #$10
bit $dc00
bne .ButtonNotPressed

;button pushed

<< Content
lda BUTTON_RELEASED
bne .Restart
rts

.ButtonNotPressed
lda #1
sta BUTTON_RELEASED
rts

.Restart
;if last live return to title
lda PLAYER_LIVES
bne .RestartLevel
jmp TitleScreen

.RestartLevel

<< Content
Step 22 - Highscores
The title screen looks somewhat empty, doesn't it? Well, let's fill it with highscores!
Lazy as I am I'm storing the highscore scores and names in two separate texts which
are simply displayed with the common display routine.

After Game Over we check for a new spot. If we manage to enter the list all lower
entries are moved down. For now that happens automatically and only affects the
scores. Name entry follows up next.

First the newly added text fields, note that '-' is used as line break and '*' as end of
text marker.

Another note: If you default fill high scores always have an easily beatable top score.
People like to be better than someone else.

HIGHSCORE_SCORE
!text "00050000-"
!text "00040000-"
!text "00030000-"
!text "00020000-"
!text "00010000-"
!text "00001000-"
!text "00000300-"
!text "00000100*"

HIGHSCORE_NAME
!text "SUPERNATURAL-"
!text "SUPERNATURAL-"
!text "SUPERNATURAL-"
!text "SUPERNATURAL-"
!text "SUPERNATURAL-"
!text "SUPERNATURAL-"
!text "SUPERNATURAL-"
!text "SUPERNATURAL*"

In the title screen layout code we add the display routines:

;display high scores


;x,y pos of name
lda #6
sta PARAM1
lda #10
sta PARAM2
lda #<HIGHSCORE_NAME
sta ZEROPAGE_POINTER_1

<< Content
lda #>HIGHSCORE_NAME
sta ZEROPAGE_POINTER_1 + 1
jsr DisplayText

;x,y pos of score


lda #25
sta PARAM1
lda #10
sta PARAM2
lda #<HIGHSCORE_SCORE
sta ZEROPAGE_POINTER_1
lda #>HIGHSCORE_SCORE
sta ZEROPAGE_POINTER_1 + 1
jsr DisplayText

And last but not least, the actual checking for a highscore. You'll see that the score is
not actually kept in a variable. Instead the screen chars at the display are used to
compare against the text.

;------------------------------------------------------------
;check if the player got a new highscore entry
;------------------------------------------------------------
!zone CheckForHighscore
CheckForHighscore
lda #0
sta PARAM1
ldy #0

.CheckScoreEntry
ldx #0
sty PARAM2

.CheckNextDigit
lda SCREEN_CHAR + ( 23 * 40 + 8 ),x
cmp HIGHSCORE_SCORE,y
bcc .NotHigher
bne .IsHigher

;need to check next digit


iny
inx
cpx #HIGHSCORE_SCORE_SIZE
beq .IsHigher
jmp .CheckNextDigit

.NotHigher
inc PARAM1
lda PARAM1
cmp #HIGHSCORE_ENTRY_COUNT
beq .NoNewHighscore

<< Content
;y points somewhere inside the score, recalc next line
pos
lda PARAM2
clc
adc #( HIGHSCORE_SCORE_SIZE + 1 )
tay
jmp .CheckScoreEntry

.NoNewHighscore
jmp TitleScreen

.IsHigher
;shift older entries down, add new entry
lda #( HIGHSCORE_ENTRY_COUNT - 1 )
sta PARAM2

;y carries the offset in the score text, position at


start of second last entry
ldy #( ( HIGHSCORE_SCORE_SIZE + 1 ) * (
HIGHSCORE_ENTRY_COUNT - 2 ) )

.CopyScore
lda PARAM2
cmp PARAM1
beq .SetNewScore

;copy score
ldx #0

.CopyNextScoreDigit
lda HIGHSCORE_SCORE,y
sta HIGHSCORE_SCORE + ( HIGHSCORE_SCORE_SIZE + 1 ),y
iny
inx
cpx #HIGHSCORE_SCORE_SIZE
bne .CopyNextScoreDigit
tya
sec
sbc #( HIGHSCORE_SCORE_SIZE + HIGHSCORE_SCORE_SIZE + 1 )
tay
dec PARAM2
jmp .CopyScore

.SetNewScore
;y points at score above the new entry
tya
clc
adc #( HIGHSCORE_SCORE_SIZE + 1 )
tay
ldx #0

<< Content
.SetNextScoreDigit
lda SCREEN_CHAR + ( 23 * 40 + 8 ),x
sta HIGHSCORE_SCORE,y
iny
inx
cpx #HIGHSCORE_SCORE_SIZE
bne .SetNextScoreDigit
jmp TitleScreen

<< Content
Step 23 - Name Entry
Highscore display is all nice and dandy, but we want to see our score and name in
there! So this step adds name entry. For this we rely on the Kernal (it's with 'a' on the
C64) functions to read the pressed keys from the keyboard.

Adding to the score moving we find the spot for the name, shift all lower score
names down and let the player enter his name.

The Kernal routine we're using is called GETIN and is placed in the ROM at $FFE4.
The following huge code snippet is placed in the old CheckForHighscore routine. For
explanation reasons it's separated.

The first block moves the name entries of lower scores downwards. Remember, the
name entries are stored continously in the location at HIGHSCORE_NAME.

;move names down


;shift older entries down, add new entry
lda #( HIGHSCORE_ENTRY_COUNT - 1 )
sta PARAM2

;y carries the offset in the score text, position at


start of second last entry
ldy #( ( HIGHSCORE_NAME_SIZE + 1 ) * (
HIGHSCORE_ENTRY_COUNT - 2 ) )
.CopyName
lda PARAM2
cmp PARAM1
beq .SetNewName

;copy name
ldx #0
.CopyNextNameChar
lda HIGHSCORE_NAME,y
sta HIGHSCORE_NAME + ( HIGHSCORE_NAME_SIZE + 1 ),y
iny
inx
cpx #HIGHSCORE_NAME_SIZE
bne .CopyNextNameChar

tya
sec
sbc #( HIGHSCORE_NAME_SIZE + HIGHSCORE_NAME_SIZE + 1 )
tay
dec PARAM2
jmp .CopyName

<< Content
Here's the interesting part. First the proper offset of the new name inside the big text
is calculated and the old name cleared out.

.SetNewName
;calc y for new name offset
ldy PARAM1
lda #0

.AddNameOffset
cpy #0
beq .NameOffsetFound
clc
adc #( HIGHSCORE_NAME_SIZE + 1 )
dey
jmp .AddNameOffset

.NameOffsetFound
tay

;clear old name


ldx #0
sty PARAM3
lda #32

.ClearNextChar
sta HIGHSCORE_NAME,y
iny
inx
cpx #HIGHSCORE_NAME_SIZE
bne .ClearNextChar

Here's the meat, the actual name entry. Of course with Backspace support and Enter
to finalize the entry. Out of sheer lazyness the newly entered char is only inserted in
the text and the text then fully displayed.

ldy PARAM3

;enter name
ldx #0
stx PARAM3
jmp .ShowChar

.GetNextChar
sty PARAM4

;use ROM routines, read char


jsr KERNAL_GETIN
beq .GetNextChar

<< Content
;return pressed?
cmp #13
beq .EnterPressed

;DEL pressed?
cmp #20
bne .NotDel

;DEL pressed
ldy PARAM4
ldx PARAM3
beq .GetNextChar

dec PARAM3
dey
dex
lda #32
sta HIGHSCORE_NAME,y
jmp .ShowChar

.NotDel
ldy PARAM4

;pressed key >= 32 or <= 96?


cmp #32
bcc .GetNextChar
cmp #96
bcs .GetNextChar

;max length reached already?


ldx PARAM3
cpx #HIGHSCORE_NAME_SIZE
bcs .GetNextChar

;save text
sta HIGHSCORE_NAME,y
iny
inx
.ShowChar
stx PARAM3
sty PARAM4

;display high scores


;x,y pos of name
lda #6
sta PARAM1
lda #10
sta PARAM2
lda #<HIGHSCORE_NAME
sta ZEROPAGE_POINTER_1
lda #>HIGHSCORE_NAME

<< Content
sta ZEROPAGE_POINTER_1 + 1
jsr DisplayText

ldx PARAM3
ldy PARAM4
jmp .GetNextChar

.EnterPressed
;fill entry with blanks
lda #32
ldx PARAM3
ldy PARAM4
.FillNextChar
cpx #HIGHSCORE_NAME_SIZE
beq .FilledUp

sta HIGHSCORE_NAME,y
iny
inx
jmp .FillNextChar

.FilledUp
jmp TitleScreen

<< Content
Step 24 - Save Function
What use are highscores if they aren't saved?

With this step highscores are saved and loaded from your previously used medium
(tape or disk). On startup the currently used medium is stored, save and load is done
via bog standard kernal routines.

The additions for this are rather miniscule. Both save and load routines work on a
continious piece of memory. Fortunately both highscore names and scores are right
next to each other.

For saving note that the auto-delete function in the common floppy drive is borked.
Therefore we manually delete the file first and save it afterwards.

The "S0:" in front of the file name is the special code for Scratching the file.

HIGHSCORE_DELETE_FILENAME
!text "S0:HIGHSCORE"

HIGHSCORE_DELETE_FILENAME_END

!zone SaveScores
SaveScores
;delete old save file first
lda #HIGHSCORE_DELETE_FILENAME_END -
HIGHSCORE_DELETE_FILENAME
ldx #<HIGHSCORE_DELETE_FILENAME
ldy #>HIGHSCORE_DELETE_FILENAME
jsr KERNAL_SETNAM

lda #$0F ; file number 15


ldx DRIVE_NUMBER
ldy #$0F ; secondary address 15
jsr KERNAL_SETLFS
jsr $FFC0 ; call OPEN

; if carry set, the file could not be opened


bcs .ErrorDelete
ldx #$0F ; filenumber 15
jsr $FFC9 ; call CHKOUT (file 15 now used as output)

.close
lda #$0F ; filenumber 15
jsr $FFC3 ; call CLOSE

<< Content
ldx #$00 ; filenumber 0
jsr $FFC9 ; call CHKOUT (reset output device)
jmp .SaveNow

.ErrorDelete
;Akkumulator contains BASIC error code
;most likely errors:
;A = $5 (DEVICE NOT PRESENT)
;... error handling for open errors ...
lda #65
sta $cc00
jmp .close

; even if OPEN failed, the file has to be closed

.SaveNow
lda #9
ldx #<HIGHSCORE_FILENAME
ldy #>HIGHSCORE_FILENAME
jsr KERNAL_SETNAM

lda #$00
ldx DRIVE_NUMBER
ldy #$00
jsr KERNAL_SETLFS

lda #<HIGHSCORE_SCORE
sta $C1
lda #>HIGHSCORE_SCORE
sta $C2
ldx #<HIGHSCORE_DATA_END
ldy #>HIGHSCORE_DATA_END
lda #$C1 ; start address located in $C1/$C2
jsr $FFD8 ; call SAVE
;if carry set, a save error has happened;
bcs .SaveError
rts

Loading the score back in means mostly inserting the corresponding load calls:

;--------------------------------------------------
;load high scores
;returns 1 if ok, 0 otherwise
;--------------------------------------------------
!zone LoadScores
LoadScores
;disable kernal messages (do not want to see load error
etc.)
lda #$00
jsr KERNAL_SETMSG

<< Content
;set logical file parameters
lda #15
ldx DRIVE_NUMBER
ldy #0
jsr KERNAL_SETLFS

;set filename
lda #9
ldx #<HIGHSCORE_FILENAME
ldy #>HIGHSCORE_FILENAME
jsr KERNAL_SETNAM

;load to address
lda #$00 ; 0 = load
ldx #<HIGHSCORE_SCORE
ldy #>HIGHSCORE_SCORE
jsr KERNAL_LOAD
bcs .LoadError

;flag whether ok or not is set into the Carry flag


lda #1
rts

.LoadError
lda #0
rts

<< Content
Step 25 - Title Screen II
Time to spruce up the title screen again.

This time we split the title screen display, upper part will be a bitmap logo, lower
path text mode.

Some clarification: The graphic chip of the C64, the VIC II, has some neat features. It
displays line per line whatever mode currently is set. This means, with skilled
modification you can change the mode while a screen is being displayed.

The VIC II aids the programmer with a raster interrupt. You can provide a line number
where a interrupt should occur. This avoids having to actively wait. Since we need to
show two modes we need to toggle modes two times.

Once the raster is above the inner visible area we activate the bitmap mode. Once
the raster hits the end of the bitmap we want to display switch to text mode again.
There's a bit more to it if you want exact splits, but the way the image is set up allows
us to approach that rather naively.

The bitmap data itself was created by a Lua script from an actual image. Note that a
bitmap is compiled of several parts: The actual image data, the color ram part (for 1
of the three possible colors per cell) and the screen data (for the other two possible
colors per cell).

Bitmap data cannot be put in memory randomly, there are only a handful of aligned
locations where they need to reside. Nice trick: Since the layout of the code is
completely fixed you can actually force the compiler to put data at a specific location.
This way you avoid to having to copy the bitmap data somewhere else.

Here we store the bitmap data at $2000:

;place the data at a valid bitmap position, this avoids copying


the data
* = $2000
TITLE_LOGO_BMP_DATA
!byte
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
!byte
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
...
One of the color parts can also be fixed in memory:

* = $2c00

<< Content
TITLE_LOGO_SCREEN_CHAR
!byte
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
!byte
0,0,0,0,0,0,0,0,0,0,32,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
...
The final part is copied to its required target location on
demand:

TITLE_LOGO_COLORRAM
!byte
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
...

During settup up the title screen state we need to copy the color information to the
color RAM. Since we only need 64 pixel height for the logo we only copy 320 bytes:

ldx #0

.FillColor
lda TITLE_LOGO_COLORRAM,x
sta SCREEN_COLOR,x
inx
bne .FillColor

.FillColor2
lda TITLE_LOGO_COLORRAM + 256,x
sta SCREEN_COLOR + 256,x
inx
cpx #( 320 - 256 )
bne .FillColor2

Now to set up the initial raster interrupt. Note that before doing that we call jsr
WaitFrame once so the raster line is at a specific location. You don't want to hit the
interrupt on the wrong side.

;-----------------------------------
;init IRQ
;-----------------------------------
!zone InitTitleIRQ
InitTitleIRQ
sei
lda #$37 ; make sure that IO regs at $dxxx
sta $1 ;are visible
lda #$7f ;disable cia #1 generating timer irqs
sta $dc0d ;which are used by the system to flash cursor,
etc

lda #$1 ;tell VIC we want him generate raster irqs


sta $d01a

<< Content
lda #$10 ;nr of rasterline we want our irq occur at
sta $d012
lda #$1b ;MSB of d011 is the MSB of the requested
rasterline
sta $d011 ;as rastercounter goes from 0-312

;set irq vector to point to our routine


lda #<IrqSetBitmapMode
sta $314
lda #>IrqSetBitmapMode
sta $315

;acknowledge any pending cia timer interrupts


;this is just so we are 100% safe
lda $dc0d
lda $dd0d
cli
rts

Note the routine IrqSetBitmapMode (and IrqSetTextMode). These routines set their
respective mode and setup the raster interrupt for the other part.

;-----------------------------------
;IRQ Title - set bitmap mode
;-----------------------------------
!zone IrqSetBitmapMode
IrqSetBitmapMode
;acknowledge VIC irq
lda $d019
sta $d019

;install top part


lda #<IrqSetTextMode
sta $314
lda #>IrqSetTextMode
sta $315

;nr of rasterline we want our irq occur at


lda #$71
sta $d012

;bitmap modus an
lda #$3b
sta $D011

;set VIC to bank 0


lda $DD00
and #$fc
ora #$3
sta $dd00

<< Content
;bitmap to lower half, screen char pos at 3 * 1024 ( +
16384)
lda #%10111000
sta $D018
JMP $ea31

;-----------------------------------
;IRQ Title - set text mode
;-----------------------------------
!zone IrqSetTextMode
IrqSetTextMode
;acknowledge VIC irq
lda $d019
sta $d019

;install scroller irq


lda #<IrqSetBitmapMode
sta $314
lda #>IrqSetBitmapMode
sta $315

;nr of rasterline we want our irq occur at


lda #$10
sta $d012

;disable bitmap mode


lda #$1b
sta $D011

;set VIC to bank 3


lda $DD00
and #$fc
sta $dd00

;bitmap to lower half, screen char pos at 3 * 1024 ( +


16384)
lda #%00111100
sta $D018
jmp $ea31

Once the title screen state is left the interrupt needs to be disabled:

;-----------------------------------
;release IRQ
;-----------------------------------
!zone ReleaseTitleIRQ
ReleaseTitleIRQ
sei
lda #$37 ; make sure that IO regs at $dxxx
sta $1 ;are visible

<< Content
lda #$ff ;enable cia #1 generating timer irqs
sta $dc0d ;which are used by the system to flash cursor,
etc

;no more raster irqs


lda #$00
sta $d01a
lda #$31
sta $314
lda #$EA
sta $315

;acknowledge any pending cia timer interrupts


;this is just so we are 100% safe
lda $dc0d
lda $dd0d
cli
rts

<< Content
Step 26 - Enemy (Zombie)
More gameplay action, whee!

We add a new enemy, the beloved zombie. The zombie can appear from
underground, walks about slowly, and if shot, dives in the ground again.

And while underground the zombie will move about. With the effect, that the zombie
will probably appear some where else, not where the player is waiting with the
shotgun.

Adding a new enemy is pretty easy now, a new entry in the behaviour table. There
are a few more entries in other tables (start color, start sprite, etc.) which we don't
go into detail now.

ENEMY_BEHAVIOUR_TABLE_LO
!byte <PlayerControl
!byte <BehaviourBatLR
!byte <BehaviourBatUD
!byte <BehaviourBat8
!byte <BehaviourMummy
!byte <BehaviourZombie

ENEMY_BEHAVIOUR_TABLE_HI
!byte >PlayerControl
!byte >BehaviourBatLR
!byte >BehaviourBatUD
!byte >BehaviourBat8
!byte >BehaviourMummy
!byte >BehaviourZombie

First off the generic counter is checked to slow the zombie movement. The most
interesting part in this step is the added state for the enemies. The zombie makes
good use of it, as it comes with several states:

-normal walk
-collapsing (2 part)
-reappearing (2 part)
-hidden (walk)

!zone BehaviourZombie
BehaviourZombie
lda DELAYED_GENERIC_COUNTER
and #$3
beq .MovementUpdate

<< Content
rts

.MovementUpdate
lda SPRITE_STATE,x
bne .OtherStates
jmp .NormalWalk

.OtherStates
;collapsing?
cmp #128
beq .Collapsing1
cmp #129
beq .Collapsing2
cmp #131
beq .WakeUp1
cmp #132
beq .WakeUp2
cmp #130
bne .NotHidden
jmp .Hidden

.NotHidden
rts

.Collapsing1
lda #SPRITE_ZOMBIE_COLLAPSE_R_2
clc
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x
inc SPRITE_STATE,x
rts

.Collapsing2
lda #SPRITE_INVISIBLE
sta SPRITE_POINTER_BASE,x
inc SPRITE_STATE,x

;generate hidden time


jsr GenerateRandomNumber
clc
adc #25
sta SPRITE_MOVE_POS,x

;normalise position on full char


ldy SPRITE_CHAR_POS_X_DELTA,x
sty PARAM5

.CheckXPos
beq .XPosClear
jsr ObjectMoveLeft
dec PARAM5

<< Content
jmp .CheckXPos

.XPosClear
ldy SPRITE_CHAR_POS_Y_DELTA,x
sty PARAM5

.CheckYPos
beq .YPosClear
jsr ObjectMoveUp
dec PARAM5
jmp .CheckYPos

.YPosClear
rts

.WakeUp1
lda #SPRITE_ZOMBIE_COLLAPSE_R_1
clc
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x
inc SPRITE_STATE,x
rts

.WakeUp2
lda #SPRITE_ZOMBIE_WALK_R_1
clc
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x
lda #0
sta SPRITE_STATE,x
sta SPRITE_MOVE_POS,x
rts

.NormalWalk
inc SPRITE_MOVE_POS,x
lda SPRITE_MOVE_POS,x
and #$7
sta SPRITE_MOVE_POS,x
cmp #4
bpl .CanMove
rts

.CanMove
lda SPRITE_DIRECTION,x
beq .MoveRight

;move left
jsr ObjectWalkLeft
beq .ToggleDirection
rts

<< Content
.MoveRight
jsr ObjectWalkRight
beq .ToggleDirection
rts

.ToggleDirection
lda SPRITE_DIRECTION,x
eor #1
sta SPRITE_DIRECTION,x
rts

.Hidden
;are we apt to wake up?
dec SPRITE_MOVE_POS,x
bne .RandomMove

;wake up
inc SPRITE_STATE,x
lda #SPRITE_ZOMBIE_COLLAPSE_R_2
clc
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x
rts

.RandomMove
;move randomly left/right
jsr GenerateRandomNumber
and #$1
beq .MoveLeft

;move right if possible


jsr CanWalkRight
beq .Blocked
inc SPRITE_CHAR_POS_X,x
ldy #8
sty PARAM5

.MoveSpriteRight
jsr MoveSpriteRight
dec PARAM5
bne .MoveSpriteRight
rts

.MoveLeft
jsr CanWalkLeft
beq .Blocked
dec SPRITE_CHAR_POS_X,x
ldy #8
sty PARAM5

.MoveSpriteLeft

<< Content
jsr MoveSpriteLeft
dec PARAM5
bne .MoveSpriteLeft
rts

.Blocked
rts

Since shooting the zombie has a different effect than for any other enemy the hurt
table acquires a new entry:

;behaviour for an enemy being hit


ENEMY_HIT_BEHAVIOUR_TABLE_LO
!byte <HitBehaviourHurt ;bat LR
!byte <HitBehaviourHurt ;bat UD
!byte <HitBehaviourHurt ;bat8
!byte <HitBehaviourHurt ;mummy
!byte <HitBehaviourCrumble ;zombie

ENEMY_HIT_BEHAVIOUR_TABLE_HI
!byte >HitBehaviourHurt ;bat LR
!byte >HitBehaviourHurt ;bat UD
!byte >HitBehaviourHurt ;bat8
!byte >HitBehaviourHurt ;mummy
!byte >HitBehaviourCrumble ;zombie

HitBehaviourCrumble is completely tailored to the zombie, it merely sets the collapse


state. The collapsing itself is done in the behaviour code.

!zone HitBehaviourCrumble
HitBehaviourCrumble
lda SPRITE_STATE,x
bne .NoHit
lda #SPRITE_ZOMBIE_COLLAPSE_R_1
clc
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x
lda #128
sta SPRITE_STATE,x

.NoHit
rts

<< Content
Step 27 - Enemy (Bat)
Actually a rather simple step, but with a few new changes.
We're adding a new enemy, a bat, that once shot, hides. And randomly reappears to
attack you, only to vanish again.

The first problem:


The bat vanishes. However not the object. To tackle that an empty sprite was added
to the sheet. This makes the object stay but the image gone. However collision still
checks the bat.

Since we have a SPRITE_STATE per object anyway a new design decision get added.
Any states >= 128 are treated as non colliding. In other words, bat gets hit, set state
to a value >= 128 and set empty sprite. Done. To reappear, repeat reverse.
Let's start with the simpler getting hit part. Set invisible sprite, set sprite state to 128.
Voila!

!zone HitBehaviourVanish
HitBehaviourVanish
lda SPRITE_STATE,x
bne .NoHit
lda #SPRITE_BAT_VANISH

sta SPRITE_POINTER_BASE,x
lda #128

sta SPRITE_STATE,x
.NoHit

rts

The behaviour code is at first quite similar to the left/right moving bat. The neat part
is the attack flight. The bat appears in any location diagonal from the player. It then
swoops toward the player to the opposite side and vanishes again.

A few cautions are taken to avoid trying to appear or move outside the screen. The
bat may actually take V shaped route.

!zone BehaviourBatVanishing
BehaviourBatVanishing
lda SPRITE_STATE,x
bne .NotNormal
jmp .NormalUpdate

.NotNormal

<< Content
cmp #128
beq .Vanish1
cmp #129
beq .Hidden
cmp #130
beq .Spawn
cmp #1
bne .NoSpecialBehaviour
jmp .AttackFlight

.NoSpecialBehaviour
rts

.Vanish1
lda DELAYED_GENERIC_COUNTER
and #$7
bne .NoSpecialBehaviour

lda #SPRITE_INVISIBLE
sta SPRITE_POINTER_BASE,x

inc SPRITE_STATE,x
jsr GenerateRandomNumber
adc #24
sta SPRITE_MOVE_POS,x
rts

.Spawn
lda DELAYED_GENERIC_COUNTER
and #$7
bne .NoSpecialBehaviour

lda #1
sta SPRITE_STATE,x
lda #SPRITE_BAT_1
sta SPRITE_POINTER_BASE,x
rts

.Hidden
dec SPRITE_MOVE_POS,x
beq .Unhide
rts

.Unhide

;position diagonal above/below player


lda SPRITE_CHAR_POS_X
cmp #10
bcc .SpawnOnRight
cmp #30
bcs .SpawnOnLeft

<< Content
;randomly choose
jsr GenerateRandomNumber
and #$1
beq .SpawnOnRight

.SpawnOnLeft
lda SPRITE_CHAR_POS_X
sec
sbc #5
sta PARAM1

lda #0
sta SPRITE_DIRECTION,x
jmp .FindYSpawnPos

.SpawnOnRight
lda SPRITE_CHAR_POS_X
clc
adc #5
sta PARAM1

lda #1
sta SPRITE_DIRECTION,x

.FindYSpawnPos
lda SPRITE_CHAR_POS_Y
cmp #5
bcc .SpawnBelow
cmp #15
bcs .SpawnAbove

;randomly choose
jsr GenerateRandomNumber
and #$1
beq .SpawnAbove

.SpawnBelow
lda SPRITE_CHAR_POS_Y
clc
adc #5
sta PARAM2

lda #0
sta SPRITE_FALLING,x
jmp .Reposition

.SpawnAbove
lda SPRITE_CHAR_POS_Y

<< Content
sec
sbc #5
sta PARAM2

lda #1
sta SPRITE_FALLING,x

.Reposition
jsr CalcSpritePosFromCharPos

inc SPRITE_STATE,x

lda #SPRITE_BAT_VANISH
sta SPRITE_POINTER_BASE,x
rts

.NormalUpdate
lda DELAYED_GENERIC_COUNTER
and #$3
bne .NoAnimUpdate

inc SPRITE_ANIM_POS,x
lda SPRITE_ANIM_POS,x
and #$3
sta SPRITE_ANIM_POS,x

tay
lda BAT_ANIMATION,y
sta SPRITE_POINTER_BASE,x

.NoAnimUpdate
lda SPRITE_STATE,x
bne .NoAction

lda SPRITE_DIRECTION,x
beq .MoveRight

;move left
jsr ObjectMoveLeftBlocking
beq .ToggleDirection
rts

.MoveRight
jsr ObjectMoveRightBlocking
beq .ToggleDirection
rts

.ToggleDirection
lda SPRITE_DIRECTION,x
eor #1

<< Content
sta SPRITE_DIRECTION,x
.NoAction
rts

.AttackFlight
inc SPRITE_MOVE_POS,x
lda SPRITE_MOVE_POS,x
cmp #80
beq .AttackDone
cmp #40
beq .ChangeFlyDirection

;fly towards player


lda SPRITE_DIRECTION,x
beq .FlyRight

stx PARAM5
jsr ObjectMoveLeft
jmp .FlyUpDown

.FlyRight
stx PARAM5
jsr ObjectMoveRight

.FlyUpDown
ldx PARAM5
lda SPRITE_FALLING,x
beq .FlyUp

jsr ObjectMoveDown
rts

.FlyUp
jsr ObjectMoveUp
rts

.ChangeFlyDirection
;change direction to avoid flying out of the screen
lda SPRITE_CHAR_POS_Y,x
cmp #5
bcc .ChangeY
cmp #18
bcc .CheckXDir

.ChangeY
lda SPRITE_FALLING,x
eor #$1
sta SPRITE_FALLING,x

.CheckXDir

<< Content
lda SPRITE_CHAR_POS_X,x
cmp #5
bcc .ChangeX
cmp #32
bcs .ChangeX
rts

.ChangeX
lda SPRITE_DIRECTION,x
eor #$1
sta SPRITE_DIRECTION,x
rts

.AttackDone
;auto-vanish
lda #0
sta SPRITE_STATE,x
jmp HitBehaviourVanish

<< Content
Step 28 - Level Item
The levels are currently rather ermm. sparse. Remember that a screen is built by
several primitve types. To get things rolling a few new building types are added:

Namely a character/color area, an alternating line horizontally and vertically


(alternating means toggling the characters +1/-1) and a quad (a 2x2 tile).

We'll start with the new constants:

LD_AREA = 4 ;data contains x,y,width,height,char,color


LD_LINE_H_ALT = 5 ;data contains x,y,width,char,color
LD_LINE_V_ALT = 6 ;data contains x,y,height,char,color
LD_QUAD = 7 ;data contains x,y,quad_id

...the new level building code table entries...

LEVEL_ELEMENT_TABLE_LO
!byte <.LevelComplete
!byte !byte !byte !byte !byte !byte !byte
LEVEL_ELEMENT_TABLE_HI
!byte >.LevelComplete
!byte >LevelLineH
!byte >LevelLineV
!byte >LevelObject
!byte >LevelArea
!byte >LevelLineHAlternating
!byte >LevelLineVAlternating
!byte >LevelQuad

Here's the area code. Straight forward, fetch all needed parameters and build the
area row by row:

!zone LevelArea
LevelArea
;X pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM1

;Y pos
iny
lda (ZEROPAGE_POINTER_1),y
tax
lda SCREEN_LINE_OFFSET_TABLE_LO,x
sta ZEROPAGE_POINTER_2

<< Content
sta ZEROPAGE_POINTER_3
lda SCREEN_LINE_OFFSET_TABLE_HI,x
sta ZEROPAGE_POINTER_2 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8
sta ZEROPAGE_POINTER_3 + 1

;width
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM2
sta PARAM6

;height
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM3

;char
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM4

;color
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM5

;store target pointers to screen and color ram


tya
pha

.NextLineArea
ldy PARAM1
.NextCharArea
lda PARAM4
sta (ZEROPAGE_POINTER_2),y
lda PARAM5
sta (ZEROPAGE_POINTER_3),y
iny
dec PARAM2
bne .NextCharArea

dec PARAM3
beq .AreaDone

;move pointers down a line


tya
sec
sbc #40
tay

<< Content
lda ZEROPAGE_POINTER_2
clc
adc #40
sta ZEROPAGE_POINTER_2
sta ZEROPAGE_POINTER_3

lda ZEROPAGE_POINTER_2 + 1
adc #0
sta ZEROPAGE_POINTER_2 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8
sta ZEROPAGE_POINTER_3 + 1

lda PARAM6
sta PARAM2
jmp .NextLineArea

.AreaDone
jmp NextLevelData

I won't go into details about the alternating lines, they are almost the same as the
common lines, with one small addition: After every set character the lowest bit of the
character number is toggled.

Way more interesting is the quad where we fall back to a hint from one of the first
steps: Tables, tables, tables!

The quad primitive uses indices into quad tables. To easy the workload every quad is
split into 8 tables, characters upper left, upper right, lower left, lower right and the
same for the colors. This way the quad index can also be used for the content.

For the record: This makes building the quad tables really annoying but the speedup
is worth it.

!zone LevelQuad
LevelQuad
;X pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM1

;Y pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM2

;item
iny

<< Content
lda (ZEROPAGE_POINTER_1),y
sta PARAM3

;store y for next data


tya
pha

ldy PARAM2
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_2
sta ZEROPAGE_POINTER_3
lda SCREEN_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_2 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8
sta ZEROPAGE_POINTER_3 + 1

ldy PARAM1
ldx PARAM3

;put image
lda BLOCK_TABLE_UL_LOCATION0,x
sta (ZEROPAGE_POINTER_2),y
lda BLOCK_TABLE_UL_COLOR_LOCATION0,x
sta (ZEROPAGE_POINTER_3),y

iny
lda BLOCK_TABLE_UR_LOCATION0,x
sta (ZEROPAGE_POINTER_2),y
lda BLOCK_TABLE_UR_COLOR_LOCATION0,x
sta (ZEROPAGE_POINTER_3),y

tya
clc
adc #39
tay

lda BLOCK_TABLE_LL_LOCATION0,x
sta (ZEROPAGE_POINTER_2),y
lda BLOCK_TABLE_LL_COLOR_LOCATION0,x
sta (ZEROPAGE_POINTER_3),y

iny
lda BLOCK_TABLE_LR_LOCATION0,x
sta (ZEROPAGE_POINTER_2),y
lda BLOCK_TABLE_LR_COLOR_LOCATION0,x
sta (ZEROPAGE_POINTER_3),y
jmp NextLevelData

<< Content
Step 29 - Highscore FX
And yet another cosmetic step. The highscores receive a nice color warp. Pretty easy
to implement, on every frame simply exchange the color bits where the score is
displayed, and voila!

First setup up the fade counter in front of the TitleLoop:

;init color fade counter


lda #0
sta COLOR_FADE_POS

And the quite simple effect. First increase the counter (and check for overflow via
AND). To get the proper offsets in the color RAM we reuse the screen line offset table
(the lo byte is the same) and add on the differece in the hi byte.

For every line the fade pos is offset by one so the effect is applied diagonally. Read
the color value from the table, write to color RAM, repeat for all lines, done.

;apply color fade


inc COLOR_FADE_POS
lda COLOR_FADE_POS
and #( COLOR_FADE_LENGTH - 1 )
sta COLOR_FADE_POS

lda #0
sta PARAM1

.FadeLine
lda PARAM1
clc
adc #10
tay
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
lda SCREEN_LINE_OFFSET_TABLE_HI,y
clc
adc #( ( ( SCREEN_COLOR - SCREEN_CHAR ) & $ff00 ) >> 8 )
sta ZEROPAGE_POINTER_1 + 1

ldy #6
lda COLOR_FADE_POS
clc
adc PARAM1
and #( COLOR_FADE_LENGTH - 1 )
tax

<< Content
.FadeColorNextChar
lda COLOR_FADE_1,x
sta (ZEROPAGE_POINTER_1),y

iny
cpy #35
beq .FadeColorLineDone
inx
cpx #COLOR_FADE_LENGTH
bne .FadeColorNextChar
ldx #0
jmp .FadeColorNextChar

.FadeColorLineDone
inc PARAM1
lda PARAM1
cmp #8
bne .FadeLine

.. common TitleLoop code

And here's the used constants. You can play around with these, but keep in mind,
COLOR_FADE_LENGTH needs to be a power of 2. Also, since the charset mode is set
to multi color you can only safely use color indices below 8.

COLOR_FADE_LENGTH = 16

COLOR_FADE_1
!byte 0,0,6,6,3,3,1,1,1,1,1,1,3,3,6,6

<< Content
Step 30 - Name Entry FX
And a new cosmetic refinement, high score name entry. Now the entry is done on a
title screen like layout, with a blinking cursor as well.

This requires setting the screen up similar to the title screen (with the raster split
between bitmap logo and text mode).

Now it pays off that we had the IRQ initialisation tucked away in a subroutine. Inside
the check for highscore routine we sprinkle a few changes. The other parts were
mostly in place already (highscore placement, name entry).

First, calling InitTitleIRQ sets up the screen mode. During name entry we also add a
blinking cursor. This is for now rather simply implemented as calling EOR (XOR) on
the current cursor char.

Highscore entry:

jsr InitTitleIRQ

ldy PARAM3

ldx #0
stx PARAM3
jmp .ShowChar

.GetNextChar
sty PARAM4

;blink cursor
jsr WaitFrame

lda PARAM3
clc
adc #6
tay
lda (ZEROPAGE_POINTER_4),y
eor #123
sta (ZEROPAGE_POINTER_4),y

;restore Y
ldy PARAM4

;use ROM routines, read char


jsr KERNAL_GETIN
beq .GetNextChar

<< Content
;return pressed?
cmp #13
beq .EnterPressed

;DEL pressed?
cmp #20
bne .NotDel

;DEL pressed
ldy PARAM4
ldx PARAM3
beq .GetNextChar
dec PARAM3
dey
dex
lda #32
sta HIGHSCORE_NAME,y
jmp .ShowChar

.NotDel
ldy PARAM4
;pressed key >= 32 or <= 96?
cmp #32
bcc .GetNextChar
cmp #96
bcs .GetNextChar

;max length reached already?


ldx PARAM3
cpx #HIGHSCORE_NAME_SIZE
bcs .GetNextChar

;save text
sta HIGHSCORE_NAME,y
iny
inx

.ShowChar
stx PARAM3
sty PARAM4

;display high scores


;x,y pos of name
lda #6
sta PARAM1
lda #10
sta PARAM2

lda # sta ZEROPAGE_POINTER_1


lda #>HIGHSCORE_NAME

<< Content
sta ZEROPAGE_POINTER_1 + 1

jsr DisplayText

ldx PARAM3
ldy PARAM4

jmp .GetNextChar

.EnterPressed
;fill entry with blanks
lda #32
ldx PARAM3
ldy PARAM4

.FillNextChar
cpx #HIGHSCORE_NAME_SIZE
beq .FilledUp
sta HIGHSCORE_NAME,y
iny
inx
jmp .FillNextChar

.FilledUp
jsr SaveScores
jmp TitleScreenWithoutIRQ

At the end of the routine, where we used to jump to the title screen before we now
do not need to setup the IRQ any more. Since we're always low on memory we use
the power of goto to jump smackdab into the previous TitleScreen routine. This does
nothing else but skip the IRQ initialisation call:

!zone TitleScreen
TitleScreen
jsr InitTitleIRQ

TitleScreenWithoutIRQ

...

Note that this is something that I've done on several occasions. Write sub routines in
a way that you can use them for several use cases. There's always a trade off
between nice generic reusable code (with a bit more overhead) and specialised
routines, which may need to be written more than once.

<< Content
Step 31 - New Levels
And another rather unspectacular step: Just a bunch of new stages. Things do get
prettier once the editor is used. Currently stages are assembled manually ;)

The code changes are rather unspectacular. Since the framework is already in place
adding new levels is nothing more than adding the level data and adding the level
pointer in the level table.

Sometimes you just want to add a bit more fluff, just to be able to play the game. Try
several ideas to get a feel for where the game might be going. I've been changing the
flow of the game throughout quite a bit. In the current incarnation the game is
leaning towards Bubble Bobble style gameplay.
Adding the level pointer:

SCREEN_DATA_TABLE
!word LEVEL_1
!word LEVEL_2
!word LEVEL_3
!word LEVEL_4
!word LEVEL_5
!word LEVEL_6
!word LEVEL_7
!word LEVEL_8
!word 0

The level data itself is built by hand (for now).

LEVEL_4
!byte LD_AREA,1,1,38,21,2,13
!byte LD_LINE_H_ALT,1,5,20,96,13
!byte LD_LINE_H_ALT,25,5,14,96,13
!byte LD_LINE_H_ALT,1,8,10,96,13
!byte LD_LINE_H_ALT,15,8,24,96,13
!byte LD_LINE_H_ALT,1,11,18,96,13
!byte LD_LINE_H_ALT,23,11,16,96,13
!byte LD_LINE_H_ALT,1,14,33,96,13
!byte LD_LINE_H_ALT,38,14,2,96,13
!byte LD_LINE_H_ALT,6,17,33,96,13
!byte LD_LINE_H_ALT,12,20,6,96,13
!byte LD_OBJECT,3,21,TYPE_PLAYER
!byte LD_OBJECT,3,4,TYPE_MUMMY
!byte LD_OBJECT,33,4,TYPE_ZOMBIE
!byte LD_OBJECT,23,7,TYPE_ZOMBIE
!byte LD_OBJECT,10,10,TYPE_ZOMBIE
!byte LD_OBJECT,30,13,TYPE_ZOMBIE

<< Content
!byte LD_OBJECT,20,16,TYPE_ZOMBIE
!byte LD_OBJECT,35,21,TYPE_ZOMBIE
!byte LD_END

LEVEL_5
!byte LD_LINE_H_ALT,5,7,4,96,13
!byte LD_LINE_H_ALT,5,10,9,96,13
!byte LD_LINE_H_ALT,4,13,3,96,13
!byte LD_LINE_H_ALT,1,16,3,96,13
!byte LD_LINE_H_ALT,10,19,6,96,13
!byte LD_LINE_H_ALT,16,10,4,96,13
!byte LD_LINE_H_ALT,22,10,4,96,13
!byte LD_LINE_H_ALT,24,7,15,96,13
!byte LD_LINE_H_ALT,24,13,11,96,13
!byte LD_LINE_H_ALT,24,16,11,96,13
!byte LD_LINE_H_ALT,28,19,4,96,13
!byte LD_OBJECT,13,18,TYPE_PLAYER

!byte LD_OBJECT,18,5,TYPE_BAT_LR
!byte LD_OBJECT,34,8,TYPE_BAT_LR
!byte LD_OBJECT,9,11,TYPE_BAT_LR
!byte LD_OBJECT,15,14,TYPE_BAT_LR
!byte LD_OBJECT,25,17,TYPE_BAT_LR
!byte LD_END

LEVEL_6
!byte LD_LINE_H_ALT,1,10,5,96,13
!byte LD_LINE_H_ALT,1,13,9,96,13
!byte LD_LINE_H_ALT,1,16,13,96,13
!byte LD_LINE_H_ALT,1,19,17,96,13
!byte LD_LINE_H_ALT,34,10,5,96,13
!byte LD_LINE_H_ALT,30,13,9,96,13
!byte LD_LINE_H_ALT,26,16,13,96,13
!byte LD_LINE_H_ALT,22,19,17,96,13
!byte LD_OBJECT,19,21,TYPE_PLAYER

!byte LD_OBJECT,5,5,TYPE_BAT_LR
!byte LD_OBJECT,15,5,TYPE_BAT_LR
!byte LD_OBJECT,25,5,TYPE_BAT_LR
!byte LD_OBJECT,35,5,TYPE_BAT_LR
!byte LD_END

LEVEL_7
!byte LD_LINE_H_ALT,1,5,5,96,13
!byte LD_LINE_H_ALT,1,8,5,96,13
!byte LD_LINE_H_ALT,1,11,5,96,13
!byte LD_LINE_H_ALT,1,14,5,96,13
!byte LD_LINE_H_ALT,1,17,5,96,13
!byte LD_LINE_H_ALT,1,20,5,96,13
!byte LD_LINE_H_ALT,34,5,5,96,13

<< Content
!byte LD_LINE_H_ALT,34,8,5,96,13
!byte LD_LINE_H_ALT,34,11,5,96,13
!byte LD_LINE_H_ALT,34,14,5,96,13
!byte LD_LINE_H_ALT,34,17,5,96,13
!byte LD_LINE_H_ALT,34,20,5,96,13
!byte LD_LINE_V_ALT,6,8,11,128,9

!byte LD_LINE_V_ALT,33,8,11,128,9
!byte LD_OBJECT,19,21,TYPE_PLAYER

!byte LD_OBJECT,15,5,TYPE_BAT_LR
!byte LD_OBJECT,20,5,TYPE_BAT_LR
!byte LD_OBJECT,25,5,TYPE_BAT_LR
!byte LD_OBJECT,17,9,TYPE_BAT_LR
!byte LD_OBJECT,23,9,TYPE_BAT_LR
!byte LD_END

LEVEL_8
!byte LD_LINE_H_ALT,1,5,5,96,13
!byte LD_LINE_H_ALT,10,5,20,96,13
!byte LD_LINE_H_ALT,34,5,5,96,13
!byte LD_LINE_H_ALT,1,8,5,96,13
!byte LD_LINE_H_ALT,10,8,20,96,13
!byte LD_LINE_H_ALT,34,8,5,96,13
!byte LD_LINE_V_ALT,10,6,3,2,13
!byte LD_LINE_V_ALT,16,6,3,2,13
!byte LD_LINE_V_ALT,23,6,3,2,13
!byte LD_LINE_V_ALT,29,6,3,2,13
!byte LD_LINE_H_ALT,5,11,7,96,13
!byte LD_LINE_H_ALT,28,11,7,96,13
!byte LD_LINE_H_ALT,10,14,20,96,13
!byte LD_LINE_H_ALT,5,17,7,96,13
!byte LD_LINE_H_ALT,28,17,7,96,13
!byte LD_LINE_H_ALT,10,20,20,96,13
!byte LD_OBJECT,19,21,TYPE_PLAYER
!byte LD_OBJECT,15,5,TYPE_BAT_LR
!byte LD_OBJECT,20,5,TYPE_BAT_LR
!byte LD_OBJECT,25,5,TYPE_BAT_LR
!byte LD_OBJECT,17,9,TYPE_BAT_LR
!byte LD_OBJECT,23,9,TYPE_BAT_LR
!byte LD_END

<< Content
Step 32 - Bug Hunting
A completely unspectacular step, but necessary every now and then anyhow: Bug
fixing.

During the last few steps several bugs cropped up that are being addressed now:

o Items could appear outside the play area (in the border)
o On respawn/restart the shells are filled to the max
o On respawn the player now appears at the level start pos
o Autofire on game restart is now blocked
o All active items are removed when the player gets killed

On restarting the player we add these snippets:


The first half removes all active items, the second fills up the players' shells to the
max. The last four lines set the player location to the level start position.

;remove all items


ldy #0
.RemoveItem

lda ITEM_ACTIVE,y
cmp #ITEM_NONE
beq .RemoveNextItem
lda #ITEM_NONE

sta ITEM_ACTIVE,y
jsr RemoveItemImage
.RemoveNextItem

iny
cpy #ITEM_COUNT
bne .RemoveItem
;refill shells

ldx #0
.RefillShellImage
lda #2
sta SCREEN_COLOR + 23 * 40 + 19,x
lda #7
sta SCREEN_COLOR + 24 * 40 + 19,x
inx

cpx PLAYER_SHELLS_MAX
bne .RefillShellImage
lda PLAYER_SHELLS_MAX

<< Content
sta PLAYER_SHELLS
;respawn at correct position

lda PLAYER_START_POS_X
sta PARAM1
lda PLAYER_START_POS_Y
sta PARAM2

The item spawn code is enhanced with a simple check for the position to stay inside
the wanted bounds.

!zone SpawnItem
SpawnItem
;find free item slot
ldy #0
.CheckNextItemSlot

lda ITEM_ACTIVE,y
cmp #ITEM_NONE
beq .FreeSlotFound
iny
cpy #ITEM_COUNT
bne .CheckNextItemSlot
rts
.FreeSlotFound

jsr GenerateRandomNumber
and #$1
sta ITEM_ACTIVE,y

sta PARAM1
lda #0

sta ITEM_TIME,y
lda SPRITE_CHAR_POS_X,x

sta ITEM_POS_X,y
;keep item in bounds
cmp #37
bmi .XIsOk
lda #37
sta ITEM_POS_X,y
.XIsOk
lda SPRITE_CHAR_POS_Y,x
sec
sbc #1
sta ITEM_POS_Y,y
cmp #21
bne .YIsOk
lda #20

<< Content
sta ITEM_POS_X,y
.YIsOk
stx PARAM5
tya
tax
jsr PutItemImage
ldx PARAM5

rts

To be able to respawn the player at the level start pos we now store the object
location during level creation:

...
lda PARAM3
sta SPRITE_ACTIVE,x
cmp #TYPE_PLAYER
bne .IsNotPlayer
lda PARAM1

sta PLAYER_START_POS_X
lda PARAM2
sta PLAYER_START_POS_Y
.IsNotPlayer

;PARAM1 and PARAM2 hold x,y already


jsr CalcSpritePosFromCharPos

<< Content
Step 33 - Gameplay Changes
More refinement in this step. This step marks a design change in gameplay. Before
the game was planned to be more survival based, with enemies that hunt you about.
Beginning with this step the game play gets more Bubble Bobble like.

For one the zombies are changed. Their animation is enhanced and they learned to
jump. Also, mummies now attack if they see the player (ie. look in his direction). Plus
the left/right moving bat is changed to move diagonally.

Until now the jump variables were only available to the player. Now we rename the
PLAYER_JUMP_xxx variables to SPRITE_JUMP_xxx and allow a range of all 8 sprites.

The diagonal moving bat is added. This is quite simple, we add a second direction
variable (SPRITE_DIRECTION_Y) for the vertical direction. Copy over the left/right
moving code and adjust it to vertical movement. Done!

;------------------------------------------------------------
;simply move diagonal
;------------------------------------------------------------
!zone BehaviourBatDiagonal
BehaviourBatDiagonal
lda DELAYED_GENERIC_COUNTER
and #$3
bne .NoAnimUpdate

inc SPRITE_ANIM_POS,x
lda SPRITE_ANIM_POS,x
and #$3
sta SPRITE_ANIM_POS,x

tay
lda BAT_ANIMATION,y
sta SPRITE_POINTER_BASE,x

.NoAnimUpdate
lda SPRITE_DIRECTION,x
beq .MoveRight

;move left
jsr ObjectMoveLeftBlocking
beq .ToggleDirection
jmp .MoveY

.MoveRight
jsr ObjectMoveRightBlocking

<< Content
beq .ToggleDirection
jmp .MoveY

.ToggleDirection
lda SPRITE_DIRECTION,x
eor #1
sta SPRITE_DIRECTION,x

.MoveY
lda SPRITE_DIRECTION_Y,x
beq .MoveDown

;move up
jsr ObjectMoveUpBlocking
beq .ToggleDirectionY
rts

.MoveDown
jsr ObjectMoveDownBlocking
beq .ToggleDirectionY
rts

.ToggleDirectionY
lda SPRITE_DIRECTION_Y,x
eor #1
sta SPRITE_DIRECTION_Y,x
rts

For the mummy to react to the player we add this snippet in front of its behaviour
code. Detecting the player is done the naive way. The player has to be at the same
height (the same char y pos), and the mummy has to look in its direction.

If all these conditions are matched the mummy assumes its attack stance and hurries
towards the player.

lda SPRITE_CHAR_POS_Y,x
cmp SPRITE_CHAR_POS_Y
bne .NoPlayerInSight

;player on same height


;looking at the player?
lda SPRITE_DIRECTION,x
beq .LookingRight

lda SPRITE_CHAR_POS_X,x
cmp SPRITE_CHAR_POS_X
bpl .LookingAtPlayer
jmp .NoPlayerInSight

.LookingRight

<< Content
lda SPRITE_CHAR_POS_X,x
cmp SPRITE_CHAR_POS_X
bmi .LookingAtPlayer
jmp .NoPlayerInSight

.LookingAtPlayer
lda #SPRITE_MUMMY_ATTACK_R
clc
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x

lda SPRITE_DIRECTION,x
beq .AttackRight

;attack to left
jsr ObjectMoveLeftBlocking
jsr ObjectMoveLeftBlocking
beq .ToggleDirection
rts

.AttackRight
;attack to left
jsr ObjectMoveRightBlocking
jsr ObjectMoveRightBlocking
beq .ToggleDirection
rts

.NoPlayerInSight
... old behaviour

Allowing the zombie to jump is pretty easy as well. We already have jumping code for
the player. In this step we however just copy the code over. The actual jump code
starts at the .jump label.

Increase the jump cointer, fetch the Y movement from a jump table and move the
sprite upwards. Once the zombie reaches the end of the table (the peak of the jump)
we simply let gravity set in.

!zone BehaviourZombie
BehaviourZombie
lda SPRITE_JUMP_POS,x
bne .IsJumping
jsr ObjectMoveDownBlocking
bne .Falling

.IsJumping
lda DELAYED_GENERIC_COUNTER
and #$3
beq .MovementUpdate
rts

<< Content
.Falling
rts

.MovementUpdate
lda SPRITE_JUMP_POS,x
bne .UpdateJump
lda SPRITE_STATE,x
bne .OtherStates

jsr GenerateRandomNumber
cmp #17
beq .Jump
jmp .NormalWalk

.OtherStates
;collapsing?
cmp #128
beq .Collapsing1
cmp #129
beq .Collapsing2
cmp #131
bne .NotWakeUp1
jmp .WakeUp1
.NotWakeUp1
cmp #132
bne .NotWakeUp2
jmp .WakeUp2
.NotWakeUp2
cmp #130
bne .NotHidden
jmp .Hidden

.NotHidden
rts

.Jump
;start jump
lda #SPRITE_ZOMBIE_JUMP_R
clc
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x

.UpdateJump
inc SPRITE_JUMP_POS,x
lda SPRITE_JUMP_POS,x
cmp #JUMP_TABLE_SIZE
bne .JumpOn

;jump done
lda #0

<< Content
sta SPRITE_JUMP_POS,x
rts

.JumpOn
ldy SPRITE_JUMP_POS,x
lda JUMP_TABLE,y
bne .KeepJumping
rts

.KeepJumping
sta PARAM5

.JumpContinue
jsr ObjectMoveUpBlocking
beq .JumpBlocked

dec PARAM5
bne .JumpContinue
rts

.JumpBlocked
lda #0
sta SPRITE_JUMP_POS,x
rts

... common zombie behaviour

Step 34 - Optimizing
And another refinement step: Two new items are added, one for faster reload,
another for temporary invincibility. Also, not every kill is getting you an item to
collect.

The most interesting new part is the state of a sprite. For now only used for the
player, but will be put to good use for enemies as well. The state of an object is used
for different behaviour steps, but also to describe, if an object will react to collisions.
If the highest bit is set (0x80), an object will not kill the player (or the player cannot
be killed).

We'll start by defining the new constants for the items:

;item types
ITEM_FAST_RELOAD = 2

<< Content
ITEM_INVINCIBLE = 3
;effect counters

PLAYER_FAST_RELOAD
!byte 0
PLAYER_INVINCIBLE
!byte 0
;item images

ITEM_CHAR_UL
!byte 4,8,16,20
ITEM_COLOR_UL
!byte 7,2,1,1
ITEM_CHAR_UR
!byte 5,9,17,21
ITEM_COLOR_UR
!byte 4,2,2,7
ITEM_CHAR_LL
!byte 6,10,18,22
ITEM_COLOR_LL
!byte 7,2,2,7
ITEM_CHAR_LR
!byte 7,11,19,23
ITEM_COLOR_LR
!byte 4,2,2,4

The actual collect item code is simple, we're just setting some new variables. Note
the SPRITE_STATE, which is set to 0x80.

.EffectFastReload
lda #200
sta PLAYER_FAST_RELOAD
jmp .RemoveItem

.EffectInvincible
lda #200
sta PLAYER_INVINCIBLE
lda #128
sta SPRITE_STATE
jmp .RemoveItem

Thus the CheckCollision adds a check for the highest bit in SPRITE_STATE, to allow for
player invincibility.

CheckCollisions
lda SPRITE_ACTIVE
bne .PlayerIsAlive
rts
.PlayerIsAlive

<< Content
lda SPRITE_STATE
cmp #128
bmi .IsVulnerable
rts
.IsVulnerable

ldx #1
.CollisionLoop

To visualize the invincibility state we add this code at the start of PlayerControl. If
PLAYER_INVINCIBLE is set, the players color is cycled.

lda PLAYER_INVINCIBLE
beq .NotInvincible
;count down invincibility

inc VIC_SPRITE_COLOR
dec PLAYER_INVINCIBLE
bne .NotInvincible
lda #0

sta SPRITE_STATE
lda #10
sta VIC_SPRITE_COLOR
.NotInvincible

... previous code


In the players reload code we add the double speed reload check:

...
lda PLAYER_FAST_RELOAD
beq .NoFastReload
dec PLAYER_FAST_RELOAD
inc PLAYER_STAND_STILL_TIME
.NoFastReload
...

At the location, where we previously always spawned an item we add this:

;only spawn item randomly


lda GenerateRandomNumber
and #02
beq .CreateItem
jmp .ShotDone
...

.CreateItem
jsr SpawnItem
jmp .ShotDone

<< Content
To get things running clean we change a few spots:
On restarting the temporary states need to be reset.

lda #0
sta PLAYER_FAST_RELOAD
sta PLAYER_INVINCIBLE
sta SPRITE_STATE

Step 35 - Annoyed State


And onwards we go. Enemies now get an annoyed state once they got hit. For
some enemies annoyance is even increasing. And to top it off, a new jumping
spider enemy was added.

Adding the new enemy follows the common pattern we've seen in earlier
steps.

Add new constants:

SPRITE_SPIDER_STAND = SPRITE_BASE + 44
SPRITE_SPIDER_WALK_1 = SPRITE_BASE + 45
SPRITE_SPIDER_WALK_2 = SPRITE_BASE + 46

TYPE_SPIDER = 8

Add the behaviour code:

;------------------------------------------------------------
;run left/right, jump off directional
;------------------------------------------------------------
!zone BehaviourSpider
BehaviourSpider
;animate spider
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #2
bne .NoAnimUpdate

lda #0
sta SPRITE_ANIM_DELAY,x

inc SPRITE_ANIM_POS,x
lda SPRITE_ANIM_POS,x

<< Content
and #$3
sta SPRITE_ANIM_POS,x

tay
lda SPIDER_ANIMATION_TABLE,y
sta SPRITE_POINTER_BASE,x

.NoAnimUpdate
lda SPRITE_JUMP_POS,x
bne .NoFallHandling

jsr UpdateSpriteFall
sta SPRITE_FALLING,x

.NoFallHandling

lda #3
sta PARAM6
.MoveStep
dec PARAM6
beq .MoveDone

lda SPRITE_DIRECTION,x
beq .MoveRight

;move left
lda SPRITE_JUMP_POS,x
ora SPRITE_FALLING,x
bne .OnlyMoveLeft

jsr ObjectWalkOrJumpLeft
beq .ToggleDirection
jmp .MoveStep

.MoveDone
lda SPRITE_JUMP_POS,x
beq .NotJumping
jsr UpdateSpriteJump
.NotJumping
rts

.OnlyMoveLeft
jsr ObjectMoveLeftBlocking
beq .ToggleDirection
jmp .MoveStep

.MoveRight
lda SPRITE_JUMP_POS,x
ora SPRITE_FALLING,x
bne .OnlyMoveRight

<< Content
jsr ObjectWalkOrJumpRight
beq .ToggleDirection
jmp .MoveStep

.OnlyMoveRight
jsr ObjectMoveRightBlocking
beq .ToggleDirection
jmp .MoveStep

.ToggleDirection
lda SPRITE_DIRECTION,x
eor #1
sta SPRITE_DIRECTION,x
jmp .MoveStep

The behaviour of this spider enemy: Walk. If a gap below is encountered,


jump. If the way is blocked, turn. This behaviour is put in a general
subroutine, since it might come in handy for other enemies later (and it will):

;------------------------------------------------------------
;walk object left if could fall off jump if blocked turn
;x = object index
;------------------------------------------------------------
!zone ObjectWalkOrJumpLeft
ObjectWalkOrJumpLeft

lda SPRITE_CHAR_POS_X_DELTA,x
beq .CheckCanMoveLeft

.CanMoveLeft
dec SPRITE_CHAR_POS_X_DELTA,x

jsr MoveSpriteLeft
lda #1
rts

.CheckCanMoveLeft
jsr CanWalkOrJumpLeft
beq .Blocked

cmp #1
beq .WalkLeft

;jump
lda SPRITE_JUMP_POS,x
bne .WalkLeft

lda #1
sta SPRITE_JUMP_POS,x

<< Content
.WalkLeft
lda #8
sta SPRITE_CHAR_POS_X_DELTA,x
dec SPRITE_CHAR_POS_X,x

jmp .CanMoveLeft

.Blocked
rts

;------------------------------------------------------------
;checks if an object can walk or jump left (jump if would
fall off)
;x = object index
;returns 0 if blocked
;returns 1 if possible
;returns 2 if jump required (not blocked, but in front of
hole)
;------------------------------------------------------------
!zone CanWalkOrJumpLeft
CanWalkOrJumpLeft
ldy SPRITE_CHAR_POS_Y,x
dey
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1

ldy SPRITE_CHAR_POS_X,x
dey

lda (ZEROPAGE_POINTER_1),y

jsr IsCharBlocking
bne .BlockedLeft

tya
clc
adc #40
tay
lda (ZEROPAGE_POINTER_1),y
jsr IsCharBlocking
bne .BlockedLeft

;is a hole in front


ldy SPRITE_CHAR_POS_Y,x
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1

<< Content
lda SPRITE_CHAR_POS_X,x
clc
adc #39
tay

lda (ZEROPAGE_POINTER_1),y
jsr IsCharBlockingFall
bne .NoHole

lda #2
rts

.NoHole
lda #1
rts

.BlockedLeft
lda #0
rts

To get and show annoyed behaviour a new state array SPRITE_ANNOYED is


created. Enemies are supposed to change color to see once they get angry.
For this we add a new color table:

TYPE_ANNOYED_COLOR
!byte 0 ;dummy
!byte 10 ;player
!byte 2 ;bat diagonal
!byte 2 ;bat up down
!byte 4 ;bat 8
!byte 2 ;mummy
!byte 13 ;zombie
!byte 2 ;bat vanish
!byte 2 ;spider

Add a little change to the common HitBehaviourHurt routine to increase the


annoyance state:

!zone HitBehaviourHurt
HitBehaviourHurt
inc SPRITE_ANNOYED,x

ldy SPRITE_ACTIVE,x
lda TYPE_ANNOYED_COLOR,y
sta VIC_SPRITE_COLOR,x
rts

Voila! In a later step we'll add some enemies that change behaviour once they
get angry.

<< Content
Step 36 - Burst in Smoke
I called this one "Satisfaction Refinement". Enemies when shot are hit back for a few
steps, it's a shotgun after all, dammit! On their final blow enemies now burst in
smoke.

For the smoke/explosion we'll add a new object type:

SPRITE_EXPLOSION_1 = SPRITE_BASE + 47
SPRITE_EXPLOSION_2 = SPRITE_BASE + 48
SPRITE_EXPLOSION_3 = SPRITE_BASE + 49

TYPE_EXPLOSION = 9

Now, once the enemy loses its last hit point, we don't simply remove the object but
rather replace it with an explosion. This time we don't call
RemoveObject/SpawnObject but rather put the required values in place right there.

lda #TYPE_EXPLOSION
sta SPRITE_ACTIVE,x
lda #15
sta VIC_SPRITE_COLOR,x
lda BIT_TABLE,x
ora VIC_SPRITE_MULTICOLOR
sta VIC_SPRITE_MULTICOLOR
lda #SPRITE_EXPLOSION_1
sta SPRITE_POINTER_BASE,x
lda #0
sta SPRITE_ANIM_DELAY,x
sta SPRITE_ANIM_POS,x

The explosion behaviour itself is quite simple. Move upwards slowly and animate.
Once the end of the animation is reached remove itself.

;------------------------------------------------------------
;explosion
;------------------------------------------------------------
!zone BehaviourExplosion
BehaviourExplosion
jsr MoveSpriteUp
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #3
beq .UpdateAnimation

<< Content
rts

.UpdateAnimation
lda #0
sta SPRITE_ANIM_DELAY,x
inc SPRITE_ANIM_POS,x
lda SPRITE_ANIM_POS,x
cmp #4
beq .ExplosionDone
clc
adc #SPRITE_EXPLOSION_1
sta SPRITE_POINTER_BASE,x
rts

.ExplosionDone
jsr RemoveObject
rts

For the hit back of the enemies we'll add this code at the beginning of all
participating enemies. The SPRITE_HITBACK counter is checked, if it's set, it's
decreased and the object is moved in the hit back direction.

lda SPRITE_HITBACK,x
beq .NoHitBack

dec SPRITE_HITBACK,x
lda SPRITE_HITBACK_DIRECTION,x
beq .HitBackRight

;move left
jsr ObjectMoveLeftBlocking
rts

.HitBackRight
jsr ObjectMoveRightBlocking
rts

.NoHitBack

The current HitBack methods are enhanced with the following snippet. Set the hit
back counter (to 8) and use the direction from the player as the hit back dir. Since the
player shots are instant this correctly holds the direction away from the player.

lda #8
sta SPRITE_HITBACK,x

;hitback dir determined from player dir (equal shot dir)


lda SPRITE_DIRECTION
sta SPRITE_HITBACK_DIRECTION,x

<< Content
Step 37 - Player Death-Anim
Currently the player is simply vanishing when getting killed. Kinda looks odd,
doesn't it? We add a nice new animation and then send the player back in.

First we remove the previous player kill code (which removed the sprite and
displayed a GET READY message) and replace it with this.

Note that we keep the sprite object but set the state to invincible (>= 128).

.PlayerCollidedWithEnemy
;player killed
ldx PARAM6
lda #129
sta SPRITE_STATE,x
lda #SPRITE_PLAYER_DEAD
sta SPRITE_POINTER_BASE,x
lda #0
sta SPRITE_MOVE_POS,x

On top of the PlayerControl routine we add this part. Note that the routine is
jumped into PlayerControl, but .PlayerIsDying is above. The reason for this is
the limited range of branch mnemonics. They only allow for -128/127 byte
jumps. Just by rearranging the code the branch can be kept.

A different solution would be to replace the branch by a reverse branch with a


followup jmp.

.PlayerIsDying
inc SPRITE_MOVE_POS,x
lda SPRITE_MOVE_POS,x
cmp #64
beq .PlayerRespawn
and #$03
bne .NoUpMove

jsr MoveSpriteUp

.NoUpMove
rts

.PlayerRespawn
dec PLAYER_LIVES
jsr DisplayLiveNumber

<< Content
;game over?
lda PLAYER_LIVES
bne .RestartPlayer
jmp CheckForHighscore

.RestartPlayer
lda SPRITE_ACTIVE

;refill shells
ldy #0

.RefillShellImage
lda #2
sta SCREEN_COLOR + 23 * 40 + 19,y
lda #7
sta SCREEN_COLOR + 24 * 40 + 19,y
iny
cpy PLAYER_SHELLS_MAX
bne .RefillShellImage

lda PLAYER_SHELLS_MAX
sta PLAYER_SHELLS

;respawn at correct position


lda PLAYER_START_POS_X
sta PARAM1
lda PLAYER_START_POS_Y
sta PARAM2

;PARAM1 and PARAM2 hold x,y already


jsr CalcSpritePosFromCharPos

;enable sprite
lda BIT_TABLE
ora VIC_SPRITE_ENABLE
sta VIC_SPRITE_ENABLE

;initialise enemy values


lda #SPRITE_PLAYER
sta SPRITE_POINTER_BASE
lda #0
sta PLAYER_FAST_RELOAD
sta PLAYER_INVINCIBLE
sta SPRITE_STATE

;look right per default


lda #0
sta SPRITE_DIRECTION
lda #0
sta SPRITE_JUMP_POS
sta SPRITE_FALLING

<< Content
rts

PlayerControl
lda SPRITE_STATE,x
cmp #129
bne .NotDying
jmp .PlayerIsDying

.NotDying

Now that the whole dying sequence is handled by PlayerControl directly the
full routine DeadControl can be removed. This change also helps a lot in the
next step ;-)

Step 38 - Second Player


Now this is one huge step that took me a week to implement: A second
player. Can't have Supernatural with only Dean, now there's Sam as well.

In the title screen, press up to toggle game modes(single Dean, single Sam,
coop). Note that Sam does not use a shot gun, he uses his dark powers.

We start by adding the game modes:

;game mode types


GT_SINGLE_PLAYER_DEAN = 0
GT_SINGLE_PLAYER_SAM = 1
GT_COOP = 2

Of course the current mode needs to be displayed in the main menu. We use
GAME_MODE as index into a text mode description table.

ldx GAME_MODE
lda TEXT_GAME_MODE_LO,x
sta ZEROPAGE_POINTER_1
lda TEXT_GAME_MODE_HI,x
sta ZEROPAGE_POINTER_1 + 1
lda #11
sta PARAM1
lda #21
sta PARAM2
jsr DisplayText

...and let the player toggle through the game modes with a joystick up

<< Content
movement:

lda #$01
bit JOYSTICK_PORT_II
bne .NotUpPressed
lda UP_RELEASED
beq .UpPressed
inc GAME_MODE
lda GAME_MODE
cmp #3
bne .NoGameModeWrap

lda #0
sta GAME_MODE

.NoGameModeWrap
;redisplay game mode
ldx GAME_MODE
lda TEXT_GAME_MODE_LO,x
sta ZEROPAGE_POINTER_1
lda TEXT_GAME_MODE_HI,x
sta ZEROPAGE_POINTER_1 + 1
lda #11
sta PARAM1
lda #21
sta PARAM2
jsr DisplayText

lda #0
jmp .UpPressed

.NotUpPressed
lda #1

.UpPressed
sta UP_RELEASED

The game mode incurs a few changes in the score display:

;score display according to game mode


lda GAME_MODE
cmp #GT_SINGLE_PLAYER_DEAN
beq .DeanOnly
cmp #GT_SINGLE_PLAYER_SAM
beq .SamOnly
lda #<TEXT_DISPLAY_DEAN_AND_SAM
sta ZEROPAGE_POINTER_1
lda #>TEXT_DISPLAY_DEAN_AND_SAM
sta ZEROPAGE_POINTER_1 + 1
jmp .DisplayDisplay

<< Content
.DeanOnly
lda #0
sta PLAYER_LIVES + 1
lda #<TEXT_DISPLAY_DEAN_ONLY
sta ZEROPAGE_POINTER_1
lda #>TEXT_DISPLAY_DEAN_ONLY
sta ZEROPAGE_POINTER_1 + 1
jmp .DisplayDisplay

.SamOnly
lda #0
sta PLAYER_LIVES
lda #<TEXT_DISPLAY_SAM_ONLY
sta ZEROPAGE_POINTER_1
lda #>TEXT_DISPLAY_SAM_ONLY
sta ZEROPAGE_POINTER_1 + 1

.DisplayDisplay

..and the joystick port a player uses. This snippet makes sure that for single
player modes the used joystick port is port 2.

;settings per game mode


;default ports
lda #0
sta PLAYER_JOYSTICK_PORT
lda #1
sta PLAYER_JOYSTICK_PORT + 1
lda GAME_MODE
cmp #GT_SINGLE_PLAYER_SAM
bne .NoPortChange

lda #0
sta PLAYER_JOYSTICK_PORT + 1

.NoPortChange

A lot of changes from Dean to Same are quite simple to implement. Reuse
Dean's code, but add an index to the sprite tables. Since Dean is always in slot
0, the ",x" was omitted to speed code up. Now we just put ,x for every sprite
table access (Sam is always in slot 1), and the basic code just works.

Some pieces had to be written anew from scratch; for example the fire code
for Sam. Sam does not use a shotgun, he uses his demonic forces instead.
Sam needs to look at an enemy, and keep fire pressed. Doing that a enemy
will get frozen and take damage. However Sam cannot move during that
phase.

<< Content
.FireSam
ldy PLAYER_JOYSTICK_PORT,x
lda JOYSTICK_PORT_II,y
and #$10
bne .SamNotFirePushed

lda #1
sta PLAYER_FIRE_PRESSED_TIME,x
stx PARAM6

jsr SamUseForce
beq .NoEnemyHeld

;Sam needs to keep pressed


inc PLAYER_SHOT_PAUSE,x
lda PLAYER_SHOT_PAUSE,x
cmp #40
beq .EnemyHurtBySam

ldy SPRITE_HELD
dey
lda #2
sta VIC_SPRITE_COLOR,y

.NoEnemyHeld
.EnemyWasHurt
;restore sprite index
ldx PARAM6
jmp .NotFirePushed

.EnemyHurtBySam
lda #0
sta PLAYER_SHOT_PAUSE,x
ldx SPRITE_HELD
dex
lda #0
sta VIC_SPRITE_COLOR,x
dec SPRITE_HP,x
bne .EnemyWasHurt

.EnemyKilledBySam
lda #5
jsr IncreaseScore
ldx SPRITE_HELD
dex
jsr KillEnemy
ldx PARAM6
lda #0
sta SPRITE_HELD
jmp .NotFirePushed

<< Content
.SamNotFirePushed
lda #0
sta SPRITE_HELD
sta PLAYER_SHOT_PAUSE,x
sta PLAYER_FIRE_PRESSED_TIME,x
jmp .NotFirePushed

Step 39 - MUSIC!
And now technically a rather small step, but with huge impact: Music.

For now we're using a demo song from GoatTracker. GoatTracker is a PC


tracker tool that lets you compose songs and provides the player source. It's
most common for music players to be stored at $1000. Since musicians on the
C64 usually need to be programmers as well the music mostly comes with the
player code.

At the beginning we initialise the player.

;set volume to max


lda #15
sta 53248

;initialise music player (play song #0)


lda #0
jsr MUSIC_PLAYER

The song and player code can be exported to a binary blob, this is simply
included in the source:

* = $3000
MUSIC_PLAYER
!binary "gt2music.bin"

Once a frame we call the play music routine to advance the music:

;------------------------------------------------------------
;wait for the raster to reach line $f8
;this is keeping our timing stable
;------------------------------------------------------------
!zone WaitFrame
WaitFrame
;are we on line $F8 already? if so, wait for the next full screen

<< Content
;prevents mistimings if called too fast
lda $d012
cmp #$F8
beq WaitFrame

;wait for the raster to reach line $f8 (should be closer to the
start of this line this way)

.WaitStep2
lda $d012
cmp #$F8
bne .WaitStep2

;play music
jsr MUSIC_PLAYER + 3
rts

That's it. Easy as pie.

Generally you may have to look out as the music player can use other code
addresses (usually some zero page bytes) which may interfere with the main
game code. This especially applies if you've got interrupt code running.

Have fun!

Step 40 - Spawn Spots


And onwards we go. This time we add spawn spots. These are a different kind
of object which spawn a number of one specific enemy type. The level is clear
once all enemies are killed and any existing spawn spots are empty.

In case you wonder about the .C64 file inside there: It's a project file for
C64Studio, an .NET based IDE I've written for development and debugging
alongside with mostly WinVICE.

Spawn spots are handled pretty similar to objects, tables with its data with
code to iterate and process them.

Note that for the spawn spot tables we use the !fill macro. It fills memory
with n bytes of value x.

;number of possible spawn spots

<< Content
SPAWN_SPOT_COUNT = 8
SPAWN_SPOT_X
!fill SPAWN_SPOT_COUNT,0

SPAWN_SPOT_Y
!fill SPAWN_SPOT_COUNT,0

SPAWN_SPOT_ACTIVE
!fill SPAWN_SPOT_COUNT,0

SPAWN_SPOT_TYPE
!fill SPAWN_SPOT_COUNT,0

SPAWN_SPOT_SPAWN_COUNT
!fill SPAWN_SPOT_COUNT,0

SPAWN_SPOT_DELAY
!fill SPAWN_SPOT_COUNT,0

NUMBER_SPAWN_SPOTS_ALIVE
!byte 0

In GameFlowControl we add the call to handle the spawn spots, and modify
the level-done check:

jsr ProcessSpawnSpots
;------------------------
;slow events
inc DELAYED_GENERIC_COUNTER
lda DELAYED_GENERIC_COUNTER
cmp #8
bne .NoTimedActionYet

lda #0
sta DELAYED_GENERIC_COUNTER

;level done delay


lda NUMBER_ENEMIES_ALIVE
bne .NotDoneYet

lda NUMBER_SPAWN_SPOTS_ALIVE
bne .NotDoneYet

Handling a spawn spot is easy. If a spawn spots delay counter reaches zero we
check if the number of alive enemies is below 4. If it is, spawn an enemy, if it
isn't reset the delay counter. Once the spawn spot object counter reaches
zero the active spawn spot is removed.

;------------------------------------------------------------
;handle spawn spots

<< Content
;------------------------------------------------------------
!zone ProcessSpawnSpots
ProcessSpawnSpots
ldx #0

.SpawnSpotLoop
lda SPAWN_SPOT_ACTIVE,x
beq .NextSpawnSpot
lda SPAWN_SPOT_DELAY,x
beq .TryToSpawn

dec SPAWN_SPOT_DELAY,x
jmp .NextSpawnSpot

.RemoveSpawnSpot
lda #0
sta SPAWN_SPOT_ACTIVE,x
dec NUMBER_SPAWN_SPOTS_ALIVE

.NextSpawnSpot
inx
cpx #SPAWN_SPOT_COUNT
bne .SpawnSpotLoop
rts

.TryToSpawn
lda #128
sta SPAWN_SPOT_DELAY,x
lda NUMBER_ENEMIES_ALIVE
cmp #3
bpl .DoNotSpawn
stx PARAM4
lda SPAWN_SPOT_TYPE,x
sta PARAM3
lda SPAWN_SPOT_X,x
sta PARAM1
lda SPAWN_SPOT_Y,x
sta PARAM2

;spawn object
jsr FindEmptySpriteSlot
beq .DoNotSpawn

;x is sprite slot
;PARAM1 is X
;PARAM2 is Y
;PARAM3 is object type
jsr SpawnObject

;restore x
ldx PARAM4

<< Content
dec SPAWN_SPOT_SPAWN_COUNT,x
beq .RemoveSpawnSpot

.DoNotSpawn
jmp .NextSpawnSpot

A new level element LD_SPAWN_SPOT is added to add spawn spots to the


stage table. The code simply stores the data in the next free spawn spot slot.

!zone LevelSpawnSpot
LevelSpawnSpot
;find free spot
ldx #0

.ExamineNextSpot
lda SPAWN_SPOT_ACTIVE,x
beq .EmptySpotFound
inx
cpx SPAWN_SPOT_COUNT
bne .ExamineNextSpot
jmp NextLevelData

.EmptySpotFound
inc NUMBER_SPAWN_SPOTS_ALIVE
lda #1
sta SPAWN_SPOT_ACTIVE,x

;X pos
iny
lda (ZEROPAGE_POINTER_1),y
sta SPAWN_SPOT_X,x

;Y pos
iny
lda (ZEROPAGE_POINTER_1),y
sta SPAWN_SPOT_Y,x

;type
iny
lda (ZEROPAGE_POINTER_1),y
sta SPAWN_SPOT_TYPE,x

;count
iny
lda (ZEROPAGE_POINTER_1),y
sta SPAWN_SPOT_SPAWN_COUNT,x
tya
pha
jmp NextLevelData

<< Content
<< Content
Step 41 - More Sprites
Finally: A very talented chap volunteered to create imagery for the game. Lots
of decent sprites and chars were created.

To celebrate there's also a slew of new enemies:

Wolfman
Eye
Jumping toad
Ghost skeleton

Code wise this is mainly add the sprites to the binary include and the new
behaviours. As you can see, the ghost skeleton doesn't do anything
interesting (yet). The jumping toad however is quite complex. It's movement
consists of ducking, jumping and waiting. Still, for now, it's quite predictable.

;------------------------------------------------------------
;ghost skeleton
;------------------------------------------------------------
!zone BehaviourGhostSkeleton
BehaviourGhostSkeleton
lda DELAYED_GENERIC_COUNTER
and #$03
bne .NoAnimUpdate

inc SPRITE_ANIM_POS,x
lda SPRITE_ANIM_POS,x
and #$03
sta SPRITE_ANIM_POS,x
tay
lda GHOST_SKELETON_ANIMATION_TABLE,y
sta SPRITE_POINTER_BASE,x

.NoAnimUpdate
.NormalUpdate

;does not do anything yet


rts

;------------------------------------------------------------
;jumping toad
;------------------------------------------------------------
!zone BehaviourJumpingToad
BehaviourJumpingToad
lda SPRITE_HITBACK,x
beq .NoHitBack

<< Content
dec SPRITE_HITBACK,x
lda SPRITE_HITBACK_DIRECTION,x
beq .HitBackRight

;move left
jsr ObjectMoveLeftBlocking
rts

.HitBackRight
jsr ObjectMoveRightBlocking
rts

.NoHitBack
lda SPRITE_STATE,x
beq .NotDucking

inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #6
bne .StillDucking
lda #0
sta SPRITE_ANIM_DELAY,x
ldy SPRITE_ANIM_POS,x
inc SPRITE_ANIM_POS,x
lda TOAD_JUMP_ANIMATION_TABLE,y
sta SPRITE_POINTER_BASE,x
cmp #SPRITE_JUMPING_TOAD_1
bne .StillDucking

;start jump
lda #0
sta SPRITE_STATE,x
inc SPRITE_JUMP_POS,x

.StillDucking
rts

.NotDucking
lda SPRITE_JUMP_POS,x
beq .FallIfPossible

;toad is jumping
lda SPRITE_JUMP_POS,x
cmp #TOAD_JUMP_TABLE_SIZE
bne .JumpOn

;jump done
jmp .JumpBlocked

.JumpOn
ldy SPRITE_JUMP_POS,x

<< Content
inc SPRITE_JUMP_POS,x
lda TOAD_JUMP_TABLE,y
bne .KeepJumping

;no jump movement needed


jmp .ToadMove

.KeepJumping
sta PARAM5

.JumpContinue
jsr ObjectMoveUpBlocking
beq .JumpBlocked
dec PARAM5
bne .JumpContinue

jmp .ToadMove

.JumpBlocked
lda #0
sta SPRITE_JUMP_POS,x
jmp .ToadMove

.FallIfPossible
jsr UpdateSpriteFall
beq .CanJump
jmp .ToadMove

.CanJump
inc SPRITE_STATE,x
lda #0
sta SPRITE_ANIM_DELAY,x
sta SPRITE_ANIM_POS,x
lda #SPRITE_JUMPING_TOAD_2
sta SPRITE_POINTER_BASE,x
rts

;simple move left/right

.ToadMove
lda SPRITE_DIRECTION,x
beq .MoveRight
jsr ObjectMoveLeftBlocking
beq .ToggleDirection
rts

.MoveRight
jsr ObjectMoveRightBlocking
beq .ToggleDirection
rts

<< Content
.ToggleDirection
lda SPRITE_DIRECTION,x
eor #1
sta SPRITE_DIRECTION,x
rts

Then we have the wolfman. Running about the level and randomly jumping.
What's worse, the angrier it gets, the faster it runs.

!zone BehaviourWolf
BehaviourWolf
lda SPRITE_HITBACK,x
beq .NoHitBack
dec SPRITE_HITBACK,x
lda SPRITE_HITBACK_DIRECTION,x
beq .HitBackRight

;move left
jsr ObjectMoveLeftBlocking
rts

.HitBackRight
jsr ObjectMoveRightBlocking
rts

.NoHitBack
;animate wolf
lda SPRITE_JUMP_POS,x
bne .NoAnimUpdate

inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #4
bne .NoAnimUpdate

lda #0
sta SPRITE_ANIM_DELAY,x
inc SPRITE_ANIM_POS,x
lda SPRITE_ANIM_POS,x
and #$03
sta SPRITE_ANIM_POS,x
tay
lda SPRITE_DIRECTION,x
beq .FacingLeft

lda WOLF_ANIMATION_TABLE,y
sta SPRITE_POINTER_BASE,x
jmp .NoAnimUpdate

.FacingLeft
lda WOLF_ANIMATION_TABLE,y

<< Content
clc
adc #( SPRITE_WOLF_WALK_R_1 - SPRITE_WOLF_WALK_L_1 )
sta SPRITE_POINTER_BASE,x

.NoAnimUpdate
lda SPRITE_JUMP_POS,x
bne .NoFallHandling

jsr UpdateSpriteFall
sta SPRITE_FALLING,x
bne .IsFalling

;neither jumping nor falling


jsr GenerateRandomNumber
and #$0f
cmp SPRITE_ANNOYED,x
bpl .IsFalling

;random jump
jmp .Jumping

.IsFalling
.NoFallHandling
lda SPRITE_ANNOYED,x
clc
adc #2
sta PARAM6

.MoveStep
dec PARAM6
beq .MoveDone

lda SPRITE_DIRECTION,x
beq .MoveRight

;move left
lda SPRITE_JUMP_POS,x
ora SPRITE_FALLING,x
bne .OnlyMoveLeft

jsr ObjectWalkOrJumpLeft
beq .ToggleDirection

jmp .MoveStep

.MoveDone
lda SPRITE_JUMP_POS,x
beq .NotJumping

.Jumping
jsr UpdateSpriteJump

<< Content
.NotJumping
rts

.OnlyMoveLeft
jsr ObjectMoveLeftBlocking
beq .ToggleDirection
jmp .MoveStep

.MoveRight
lda SPRITE_JUMP_POS,x
ora SPRITE_FALLING,x
bne .OnlyMoveRight

jsr ObjectWalkOrJumpRight
beq .ToggleDirection

jmp .MoveStep

.OnlyMoveRight
jsr ObjectMoveRightBlocking
beq .ToggleDirection

jmp .MoveStep

.ToggleDirection
lda SPRITE_DIRECTION,x
eor #1
sta SPRITE_DIRECTION,x
jmp .MoveStep

The eye is rather easy again. It moves diagonally, but stops every now and
then to take a look.

!zone BehaviourEye
BehaviourEye
lda DELAYED_GENERIC_COUNTER
and #$03
bne .NoAnimUpdate

;inc SPRITE_ANIM_POS,x
;lda SPRITE_ANIM_POS,x
;and #$03
;sta SPRITE_ANIM_POS,x

;tay
;lda BAT_ANIMATION,y
;sta SPRITE_POINTER_BASE,x

.NoAnimUpdate
lda SPRITE_STATE,x

<< Content
beq .Move
cmp #1
beq .EyeOpen
cmp #2
beq .EyeIsOpen

;eye closes
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #3
bne .NoActionNow
lda #0
sta SPRITE_ANIM_DELAY,x

;close animation
dec SPRITE_ANIM_POS,x
dec SPRITE_POINTER_BASE,x

ldy SPRITE_ANIM_POS,x
lda EYE_COLOR_TABLE,y
sta VIC_SPRITE_COLOR,x

cpy #0
bne .NoActionNow

;can move again


lda #0
sta SPRITE_STATE,x
rts

.EyeOpen
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #3
bne .NoActionNow
lda #0
sta SPRITE_ANIM_DELAY,x
;open animation
inc SPRITE_ANIM_POS,x
inc SPRITE_POINTER_BASE,x

ldy SPRITE_ANIM_POS,x
lda EYE_COLOR_TABLE,y
sta VIC_SPRITE_COLOR,x

cpy #3
bne .NoActionNow

;now wait
inc SPRITE_STATE,x

<< Content
rts

.EyeIsOpen
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #30
beq .EyeCloseNow

.NoActionNow
rts

.EyeCloseNow
lda #3
sta SPRITE_STATE,x
lda #0
sta SPRITE_ANIM_DELAY,x
rts

.Move
jsr GenerateRandomNumber
cmp #7
bne .MoveNow

;start blinking
lda #1
sta SPRITE_STATE,x
rts
.MoveNow
lda SPRITE_DIRECTION,x
beq .MoveRight

;move left
jsr ObjectMoveLeftBlocking
beq .ToggleDirection
jmp .MoveY

.MoveRight
jsr ObjectMoveRightBlocking
beq .ToggleDirection
jmp .MoveY

.ToggleDirection
lda SPRITE_DIRECTION,x
eor #1
sta SPRITE_DIRECTION,x

.MoveY
lda SPRITE_DIRECTION_Y,x
beq .MoveDown

;move up

<< Content
jsr ObjectMoveUpBlocking
beq .ToggleDirectionY
rts

.MoveDown
jsr ObjectMoveDownBlocking
beq .ToggleDirectionY
rts

.ToggleDirectionY
lda SPRITE_DIRECTION_Y,x
eor #1
sta SPRITE_DIRECTION_Y,x
rts

TL/DR: Have fun!

<< Content
Step 42 - Sprites (Ghost, Fly)
So after a break last week we carry on. There were even more enemy sprites
created, so here we go. New are the floating ghost and the fly.

The floating ghost is homing in on the player, but currently blocked by


platforms downwards. To amend this we add a new routine. It's a clone of
ObjectMoveDownBlocking, the only difference is the call to IsCharBlocking.

;------------------------------------------------------------
;move object down if not blocked
;x = object index
;------------------------------------------------------------
!zone ObjectMoveDownBlockingNoPlatform
ObjectMoveDownBlockingNoPlatform

lda SPRITE_CHAR_POS_Y_DELTA,x
beq .CheckCanMoveDown

.CanMoveDown
inc SPRITE_CHAR_POS_Y_DELTA,x

lda SPRITE_CHAR_POS_Y_DELTA,x
cmp #8
bne .NoCharStep

lda #0
sta SPRITE_CHAR_POS_Y_DELTA,x
inc SPRITE_CHAR_POS_Y,x

.NoCharStep
jsr MoveSpriteDown
lda #1
rts

.CheckCanMoveDown
lda SPRITE_CHAR_POS_X_DELTA,x
beq .NoSecondCharCheckNeeded

ldy SPRITE_CHAR_POS_Y,x
iny
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1
ldy SPRITE_CHAR_POS_X,x
iny
lda (ZEROPAGE_POINTER_1),y

<< Content
jsr IsCharBlocking
bne .BlockedDown

.NoSecondCharCheckNeeded
ldy SPRITE_CHAR_POS_Y,x
iny
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1

ldy SPRITE_CHAR_POS_X,x

lda (ZEROPAGE_POINTER_1),y

jsr IsCharBlocking
bne .BlockedDown

jmp .CanMoveDown

.BlockedDown
lda #0
rts

We had code for the ghost skeleton last week, but it was only animating. Now
here comes the homing in part. The most annoying part is actually to decide
which player to follow. For now it settles on one of the two, that may be
subject to change later.

;------------------------------------------------------------
;ghost skeleton
;------------------------------------------------------------
!zone BehaviourGhostSkeleton
BehaviourGhostSkeleton
GHOST_MOVE_SPEED = 1
lda DELAYED_GENERIC_COUNTER
and #$03
bne .NoAnimUpdate

inc SPRITE_ANIM_POS,x
lda SPRITE_ANIM_POS,x
and #$03
sta SPRITE_ANIM_POS,x

tay
lda GHOST_SKELETON_ANIMATION_TABLE,y
sta SPRITE_POINTER_BASE,x

.NoAnimUpdate
inc SPRITE_ANIM_DELAY,x

<< Content
lda SPRITE_ANIM_DELAY,x
cmp #10
beq .DoCheckMove
jmp .DoGhostMove

.DoCheckMove
lda #0
sta SPRITE_ANIM_DELAY,x

txa
and #$01
tay
lda SPRITE_ACTIVE,y
cmp #TYPE_PLAYER_DEAN
beq .FoundPlayer
cmp #TYPE_PLAYER_SAM
beq .FoundPlayer

;check other player


tya
eor #1
tay
lda SPRITE_ACTIVE,y
cmp #TYPE_PLAYER_DEAN
beq .FoundPlayer
cmp #TYPE_PLAYER_SAM
beq .FoundPlayer

;no player to hunt


rts

.FoundPlayer
;player index in y
lda SPRITE_CHAR_POS_X,y
cmp SPRITE_CHAR_POS_X,x
bpl .MoveRight

;move left
lda SPRITE_DIRECTION,x
bne .AlreadyLookingLeft
lda SPRITE_MOVE_POS,x
beq .TurnLNow
dec SPRITE_MOVE_POS,x
bne .CheckYNow

.TurnLNow
;turning now
lda #1
sta SPRITE_DIRECTION,x
jmp .CheckYNow

<< Content
.AlreadyLookingLeft
lda SPRITE_MOVE_POS,x
cmp #GHOST_MOVE_SPEED
beq .CheckYNow
inc SPRITE_MOVE_POS,x
jmp .CheckYNow

.MoveRight
lda SPRITE_DIRECTION,x
beq .AlreadyLookingRight

lda SPRITE_MOVE_POS,x
beq .TurnRNow
dec SPRITE_MOVE_POS,x
bne .CheckYNow

;turning now
.TurnRNow
lda #0
sta SPRITE_DIRECTION,x
jmp .CheckYNow

.AlreadyLookingRight
lda SPRITE_MOVE_POS,x
cmp #GHOST_MOVE_SPEED
beq .CheckYNow
inc SPRITE_MOVE_POS,x
jmp .CheckYNow

.CheckYNow
;player index in y
lda SPRITE_CHAR_POS_Y,y
cmp SPRITE_CHAR_POS_Y,x
bpl .MoveDown

;move left
lda SPRITE_DIRECTION_Y,x
bne .AlreadyLookingUp
lda SPRITE_MOVE_POS_Y,x
beq .TurnUNow
dec SPRITE_MOVE_POS_Y,x
bne .DoGhostMove

.TurnUNow
;turning now
lda #1
sta SPRITE_DIRECTION_Y,x
jmp .DoGhostMove

.AlreadyLookingUp
lda SPRITE_MOVE_POS_Y,x

<< Content
cmp #GHOST_MOVE_SPEED
beq .DoGhostMove
inc SPRITE_MOVE_POS_Y,x
jmp .DoGhostMove

.MoveDown
lda SPRITE_DIRECTION_Y,x
beq .AlreadyLookingDown

lda SPRITE_MOVE_POS_Y,x
beq .TurnDNow
dec SPRITE_MOVE_POS_Y,x
bne .DoGhostMove

;turning now
.TurnDNow
lda #0
sta SPRITE_DIRECTION_Y,x
jmp .DoGhostMove

.AlreadyLookingDown
lda SPRITE_MOVE_POS_Y,x
cmp #GHOST_MOVE_SPEED
beq .DoGhostMove
inc SPRITE_MOVE_POS_Y,x
jmp .DoGhostMove

.DoGhostMove
;move X times
ldy SPRITE_MOVE_POS,x
sty PARAM4
beq .DoY

lda SPRITE_DIRECTION,x
beq .DoRight
.MoveLoopL
jsr ObjectMoveLeftBlocking
dec PARAM4
bne .MoveLoopL
jmp .DoY

.DoRight
.MoveLoopR
jsr ObjectMoveRightBlocking
dec PARAM4
bne .MoveLoopR

.DoY
;move X times
ldy SPRITE_MOVE_POS_Y,x
sty PARAM4

<< Content
beq .MoveDone

lda SPRITE_DIRECTION_Y,x
beq .DoDown
.MoveLoopU
jsr ObjectMoveUpBlocking
dec PARAM4
bne .MoveLoopU
jmp .MoveDone

.DoDown
.MoveLoopD
jsr ObjectMoveDownBlockingNoPlatform
dec PARAM4
bne .MoveLoopD
.MoveDone
rts

And last but not least, the annoying fly. Moving randomly about it surely
annoys the heck out of you.

;------------------------------------------------------------
;move randomly diagonal
;------------------------------------------------------------
!zone BehaviourFly
BehaviourFly
lda DELAYED_GENERIC_COUNTER
and #$01
bne .NoAnimUpdate

lda SPRITE_ANIM_POS,x
eor #1
sta SPRITE_ANIM_POS,x

clc
adc #SPRITE_FLY_1
sta SPRITE_POINTER_BASE,x

.NoAnimUpdate
lda SPRITE_STATE,x
beq .Move
dec SPRITE_MOVE_POS,x
bne .NoAction

;can move again


dec SPRITE_STATE,x

jsr GenerateRandomNumber
sta SPRITE_MOVE_POS,x

<< Content
jsr GenerateRandomNumber
and #$03
cmp #3
bne .ValueOK

lda #2
.ValueOK
sta SPRITE_DIRECTION,x
jsr GenerateRandomNumber
and #$03
cmp #3
bne .ValueOK2

lda #2
.ValueOK2
sta SPRITE_DIRECTION_Y,x
.NoAction
rts

.Move
dec SPRITE_MOVE_POS,x
bne .CanMove

;wait
jsr GenerateRandomNumber
sta SPRITE_MOVE_POS,x

inc SPRITE_STATE,x
rts

.CanMove
lda SPRITE_DIRECTION,x
beq .MoveRight
cmp #2
beq .MoveY

;move left
jsr ObjectMoveLeftBlocking
beq .ToggleDirection
jmp .MoveY

.MoveRight
jsr ObjectMoveRightBlocking
beq .ToggleDirection
jmp .MoveY

.ToggleDirection
lda SPRITE_DIRECTION,x
eor #1
sta SPRITE_DIRECTION,x

<< Content
.MoveY
lda SPRITE_DIRECTION_Y,x
beq .MoveDown
cmp #2
beq .NoYMovement

;move up
jsr ObjectMoveUpBlocking
beq .ToggleDirectionY
rts

.MoveDown
jsr ObjectMoveDownBlockingNoPlatform
beq .ToggleDirectionY

.NoYMovement
rts

.ToggleDirectionY
lda SPRITE_DIRECTION_Y,x
eor #1
sta SPRITE_DIRECTION_Y,x
rts

Step 43 - Sprites (Frank & Slime)


And another two new enemies, Frankenstein's monster and a slime.
Frankenstein's monster behaves very much like zombies, but is a bit stronger.

;------------------------------------------------------------
;simply walk left/right, do not fall off
;state 128 = invisible
; 1 = rising
; 0 = moving
; 2 = collapsing
;------------------------------------------------------------
!zone BehaviourFrankenstein
BehaviourFrankenstein
lda SPRITE_HITBACK,x
beq .NoHitBack
dec SPRITE_HITBACK,x
lda SPRITE_HITBACK_DIRECTION,x
beq .HitBackRight

;move left
jsr ObjectMoveLeftBlocking
rts

<< Content
.HitBackRight
jsr ObjectMoveRightBlocking
rts

.NoHitBack
lda SPRITE_JUMP_POS,x
bne .IsJumping
jsr ObjectMoveDownBlocking
bne .Falling

.IsJumping
lda DELAYED_GENERIC_COUNTER
and #$03
beq .MovementUpdate
.NoMovement
rts

.Falling
lda DELAYED_GENERIC_COUNTER
and #$03
bne .NoMovement
jmp .WalkWithoutAnimation

.MovementUpdate
lda SPRITE_JUMP_POS,x
bne .UpdateJump
lda SPRITE_STATE,x
bne .OtherStates
;moving
jsr GenerateRandomNumber
cmp #17
beq .Jump
jmp .NormalWalk

.OtherStates
;collapsing?
cmp #2
beq .Collapsing
cmp #1
beq .Rising
cmp #128
bne .NotHidden
jmp .Hidden

.NotHidden
rts

.Jump
;start jump
lda #SPRITE_FRANKIE_JUMP_R
clc

<< Content
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x

.UpdateJump
jsr UpdateSpriteJump

;still move
jmp .WalkWithoutAnimation

.NoUpdate
rts

.Collapsing
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #3
bne .NoUpdate

lda #0
sta SPRITE_ANIM_DELAY,x

lda SPRITE_ANIM_POS,x
beq .CollapseDone

dec SPRITE_ANIM_POS,x

lda SPRITE_ANIM_POS,x
clc
asl
adc #SPRITE_FRANKIE_RISE_R_1
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x
rts

.CollapseDone
;on to hidden state
lda #128
sta SPRITE_STATE,x

lda #SPRITE_INVISIBLE
sta SPRITE_POINTER_BASE,x
;generate hidden time
jsr GenerateRandomNumber
and #$31
clc
adc #25
sta SPRITE_MOVE_POS,x

;normalise position on full char


ldy SPRITE_CHAR_POS_X_DELTA,x
sty PARAM5

<< Content
.CheckXPos
beq .XPosClear
jsr ObjectMoveLeft
dec PARAM5
jmp .CheckXPos

.XPosClear
ldy SPRITE_CHAR_POS_Y_DELTA,x
sty PARAM5
.CheckYPos
beq .YPosClear
jsr ObjectMoveUp
dec PARAM5
jmp .CheckYPos
.YPosClear
rts

.Rising
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #3
bne .NoUpdate

lda #0
sta SPRITE_ANIM_DELAY,x

inc SPRITE_ANIM_POS,x
lda SPRITE_ANIM_POS,x
cmp #3
beq .RiseDone
clc
asl
adc #SPRITE_FRANKIE_RISE_R_1
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x
rts

.RiseDone
lda #SPRITE_FRANKIE_WALK_R_1
clc
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x

lda #0
sta SPRITE_MOVE_POS,x
sta SPRITE_ANIM_DELAY,x
sta SPRITE_ANIM_POS,x
sta SPRITE_STATE,x
rts

.NormalWalk

<< Content
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #3
bne .NoAnimUpdate

lda #0
sta SPRITE_ANIM_DELAY,x

inc SPRITE_MOVE_POS,x

.NoAnimUpdate
lda SPRITE_MOVE_POS,x
and #$03
sta SPRITE_MOVE_POS,x

clc
asl
adc #SPRITE_FRANKIE_WALK_R_1
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x

.WalkWithoutAnimation
lda SPRITE_DIRECTION,x
beq .MoveRight

;move left
jsr ObjectMoveLeftBlocking
beq .ToggleDirection

lda SPRITE_ANNOYED,x
beq .NotAnnoyed
jsr ObjectMoveLeftBlocking
beq .ToggleDirection
.NotAnnoyed
rts

.MoveRight
jsr ObjectMoveRightBlocking
beq .ToggleDirection
lda SPRITE_ANNOYED,x
beq .NotAnnoyed
jsr ObjectMoveRightBlocking
beq .ToggleDirection
rts

.ToggleDirection
lda SPRITE_DIRECTION,x
eor #1
sta SPRITE_DIRECTION,x
rts

<< Content
.Hidden
;are we apt to wake up?
dec SPRITE_MOVE_POS,x
bne .RandomMove

;wake up
lda #1
sta SPRITE_STATE,x
lda #SPRITE_FRANKIE_RISE_R_1
clc
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x
rts

.RandomMove
;move randomly left/right
jsr GenerateRandomNumber
and #$01
beq .MoveLeft
;move right if possible
jsr CanWalkRight
beq .Blocked

inc SPRITE_CHAR_POS_X,x

ldy #8
sty PARAM5

.MoveSpriteRight
jsr MoveSpriteRight
dec PARAM5
bne .MoveSpriteRight
rts

.MoveLeft
jsr CanWalkLeft
beq .Blocked

dec SPRITE_CHAR_POS_X,x

ldy #8
sty PARAM5

.MoveSpriteLeft
jsr MoveSpriteLeft
dec PARAM5
bne .MoveSpriteLeft
rts

.Blocked
rts

<< Content
The slime sports new behaviour. Ducking and jumping through the stage
trying to slime the player.

;------------------------------------------------------------
;slime
;------------------------------------------------------------
!zone BehaviourSlime
BehaviourSlime
lda SPRITE_HITBACK,x
beq .NoHitBack
dec SPRITE_HITBACK,x
lda SPRITE_HITBACK_DIRECTION,x
beq .HitBackRight

;move left
jsr ObjectMoveLeftBlocking
rts

.HitBackRight
jsr ObjectMoveRightBlocking
rts

.NoHitBack
;state 0 = jumping
;state 1 = ducking
;state 2 = ducked
;state 3 = unducking

lda SPRITE_STATE,x
beq .SlimeJumping
cmp #2
beq .SlimeDucked

inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #6
bne .AnimPause
lda #0
sta SPRITE_ANIM_DELAY,x

ldy SPRITE_ANIM_POS,x
inc SPRITE_ANIM_POS,x

lda SPRITE_STATE,x
cmp #3
beq .SlimeUnducking

cpy #3
beq .DuckDone

<< Content
lda SLIME_DUCK_ANIMATION_TABLE,y
clc
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x
rts

.DuckDone
;start ducked state
lda #0
sta SPRITE_ANIM_POS,x
lda #2
sta SPRITE_STATE,x

jsr GenerateRandomNumber
sta SPRITE_MOVE_POS,x

.AnimPause
rts

.SlimeUnducking
cpy #3
beq .UnduckDone

lda SLIME_UNDUCK_ANIMATION_TABLE,y
clc
adc SPRITE_DIRECTION,x
sta SPRITE_POINTER_BASE,x
rts

.UnduckDone
;start jump
lda #0
sta SPRITE_ANIM_POS,x
sta SPRITE_STATE,x
inc SPRITE_JUMP_POS,x
rts

.SlimeDucked
dec SPRITE_MOVE_POS,x
bne .StayDucked

inc SPRITE_STATE,x
.StayDucked
rts

.SlimeJumping
lda SPRITE_JUMP_POS,x
beq .FallIfPossible

;toad is jumping

<< Content
lda SPRITE_JUMP_POS,x
cmp #TOAD_JUMP_TABLE_SIZE
bne .JumpOn

;jump done
jmp .JumpBlocked

.JumpOn
ldy SPRITE_JUMP_POS,x
inc SPRITE_JUMP_POS,x
lda TOAD_JUMP_TABLE,y
bne .KeepJumping

;no jump movement needed


jmp .SlimeMove

.KeepJumping
sta PARAM5

.JumpContinue
jsr ObjectMoveUpBlocking
beq .JumpBlocked

dec PARAM5
bne .JumpContinue
jmp .SlimeMove

.JumpBlocked
lda #0
sta SPRITE_JUMP_POS,x
jmp .SlimeMove

.FallIfPossible
jsr UpdateSpriteFall
beq .CanJump
jmp .SlimeMove

.CanJump
inc SPRITE_STATE,x
lda #0
sta SPRITE_ANIM_DELAY,x
sta SPRITE_ANIM_POS,x

lda SPRITE_DIRECTION,x
beq .LookingRight
lda #SPRITE_SLIME_L_1
sta SPRITE_POINTER_BASE,x
rts

.LookingRight
lda #SPRITE_SLIME_R_1

<< Content
sta SPRITE_POINTER_BASE,x
rts

;simple move left/right


.SlimeMove
lda SPRITE_DIRECTION,x
beq .MoveRight

jsr ObjectMoveLeftBlocking
beq .ToggleDirection
rts

.MoveRight
jsr ObjectMoveRightBlocking
beq .ToggleDirection
rts

.ToggleDirection
lda SPRITE_DIRECTION,x
eor #1
sta SPRITE_DIRECTION,x
clc
adc #SPRITE_SLIME_R_1
sta SPRITE_POINTER_BASE,x
rts

Beside the new monsters a few fixes and features are added as well. For one
the LookingAtPlayer function would only notice player 1. Now it works for
both players:

;------------------------------------------------------------
;determins if object is looking at player
;X = sprite index
;returns 1 if looking at player, 0 if not
;------------------------------------------------------------
!zone LookingAtPlayer
LookingAtPlayer
lda SPRITE_DIRECTION,x
beq .LookingRight

lda SPRITE_ACTIVE
cmp #TYPE_PLAYER_DEAN
bne .NotDean

lda SPRITE_CHAR_POS_X,x
cmp SPRITE_CHAR_POS_X
bpl .LookingAtPlayer

.NotDean
lda SPRITE_ACTIVE + 1
cmp #TYPE_PLAYER_SAM

<< Content
bne .NoPlayerInSight

lda SPRITE_CHAR_POS_X,x
cmp SPRITE_CHAR_POS_X + 1
bpl .LookingAtPlayer
jmp .NoPlayerInSight

.LookingRight
lda SPRITE_ACTIVE
cmp #TYPE_PLAYER_DEAN
bne .NotDeanR

lda SPRITE_CHAR_POS_X,x
cmp SPRITE_CHAR_POS_X
bmi .LookingAtPlayer

.NotDeanR
lda SPRITE_ACTIVE + 1
cmp #TYPE_PLAYER_SAM
bne .NoPlayerInSight
lda SPRITE_CHAR_POS_X,x
cmp SPRITE_CHAR_POS_X + 1
bmi .LookingAtPlayer
jmp .NoPlayerInSight

.LookingAtPlayer
lda #1
rts

.NoPlayerInSight
lda #0
rts

And a little usability thing. Previously you could only change the game mode
by pressing up in the title screen. Now it also works when pressing down:

lda #$02
bit JOYSTICK_PORT_II
bne .NotDownPressed

lda DOWN_RELEASED
beq .DownPressed

lda GAME_MODE
bne .NoGameModeWrap2
lda #3
sta GAME_MODE
.NoGameModeWrap2
dec GAME_MODE

<< Content
;redisplay game mode
ldx GAME_MODE
lda TEXT_GAME_MODE_LO,x
sta ZEROPAGE_POINTER_1
lda TEXT_GAME_MODE_HI,x
sta ZEROPAGE_POINTER_1 + 1
lda #11
sta PARAM1
lda #21
sta PARAM2
jsr DisplayText

lda #0
jmp .DownPressed

.NotDownPressed
lda #1
.DownPressed
sta DOWN_RELEASED

<< Content
Step 44 - Sprites (Hand & Skeleton)
And yet more enemies, an underground hand and a devil skeleton. Nothing
spectacularely new.

The devil skeleton is behaving mostly like the mummy. Walk back and forth, if
a player is seen, speed towards him.

!zone BehaviourDevil
BehaviourDevil
lda SPRITE_HITBACK,x
beq .NoHitBack
dec SPRITE_HITBACK,x
lda SPRITE_HITBACK_DIRECTION,x
beq .HitBackRight

;move left
jsr ObjectMoveLeftBlocking
rts

.HitBackRight
jsr ObjectMoveRightBlocking
rts

.NoHitBack
jsr ObjectMoveDownBlocking
beq .NotFalling
rts

.NotFalling
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #8
bne .NoAnimUpdate

lda #0
sta SPRITE_ANIM_DELAY,x

inc SPRITE_ANIM_POS,x
lda SPRITE_ANIM_POS,x
cmp #3
bne .NoWrap
lda #0
.NoWrap
sta SPRITE_ANIM_POS,x

clc
asl

<< Content
adc SPRITE_DIRECTION,x
adc #SPRITE_DEVIL_WALK_R_1
sta SPRITE_POINTER_BASE,x
.NoAnimUpdate
lda SPRITE_CHAR_POS_Y,x
cmp SPRITE_CHAR_POS_Y
bne .NoPlayerInSight

;player on same height


;looking at the player?
jsr LookingAtPlayer
beq .NoPlayerInSight
lda SPRITE_DIRECTION,x
beq .AttackRight

;attack to left
jsr ObjectMoveLeftBlocking
jsr ObjectMoveLeftBlocking
beq .ToggleDirection
rts

.AttackRight
;attack to left
jsr ObjectMoveRightBlocking
jsr ObjectMoveRightBlocking
beq .ToggleDirection
rts

.NoPlayerInSight
lda DELAYED_GENERIC_COUNTER
and #$03
beq .MovementUpdate
rts

.MovementUpdate
inc SPRITE_MOVE_POS,x
lda SPRITE_MOVE_POS,x
and #$03
sta SPRITE_MOVE_POS,x

lda SPRITE_DIRECTION,x
beq .MoveRight

;move left
jsr ObjectWalkLeft
beq .ToggleDirection
rts

.MoveRight
jsr ObjectWalkRight
beq .ToggleDirection

<< Content
rts

.ToggleDirection
lda SPRITE_DIRECTION,x
eor #1
sta SPRITE_DIRECTION,x
clc
adc #SPRITE_DEVIL_WALK_R_1
sta SPRITE_POINTER_BASE,x
rts

The hand is a bit more interesting. It is stationary, but usually stays


underground. In intervals the hand rises and tries to grab at the player.

;------------------------------------------------------------
;simply appear and hide again
;state 128 = invisible
; 0 = rising/hiding
;------------------------------------------------------------
!zone BehaviourHand
BehaviourHand
lda DELAYED_GENERIC_COUNTER
and #$03
beq .MovementUpdate
.NoMovement
rts

.MovementUpdate
lda SPRITE_STATE,x
bne .HiddenState
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #3
bne .NoMovement

lda #0
sta SPRITE_ANIM_DELAY,x

inc SPRITE_ANIM_POS,x
lda SPRITE_ANIM_POS,x
cmp #6
beq .EnterHiddenState

.UpdateHandSprite
ldy SPRITE_ANIM_POS,x
lda HAND_ANIM_TABLE,y
sta SPRITE_POINTER_BASE,x
lda HAND_COLOR_TABLE,y
sta VIC_SPRITE_COLOR,x

<< Content
rts

.EnterHiddenState
lda #SPRITE_INVISIBLE
sta SPRITE_POINTER_BASE,x

jsr GenerateRandomNumber
sta SPRITE_MOVE_POS,x

lda #128
sta SPRITE_STATE,x
.StillHidden
rts

.HiddenState
dec SPRITE_MOVE_POS,x
bne .StillHidden

;unhiding
lda #0
sta SPRITE_STATE,x
sta SPRITE_ANIM_DELAY,x
sta SPRITE_ANIM_POS,x
jmp .UpdateHandSprite

Note that also for the hand the empty sprite comes to use. It's basically an
empty image to avoid disabling and enabling the sprite. This however comes
at the cost of 63 bytes for the image.

Step 45 - Ext. Level Editor


In this version nothing much is added to the code. However an external level
editor was added (Windows executable) which helps a lot in churning out
pretty levels faster.

Most prominent addition are level elements. Before the level was built mostly
of simple primitives (line of an character, etc.). With the editor so called
Elements are added. These consist of a variable sized character and color
block. Elements can be arranged as single object, lines or areas. This helps a
lot in reusing bigger level parts and keeping memory usage down.

The elements are stored in several tables. A major lookup table that points to
a elements character and color tables, and two lookup tables holding an
elements width and height. The editor tries to fold element tables into each
other to save memory. For example if there's a big brick sized 4x2 characters,
and you have two smaller elements showing the left and right halfs of the
brick, the element data is reused.

<< Content
Note that the element area code is not implemented in this step.

Moral of the story:


As soon as you see you're going ahead with a project having an easy to use
editor is quite important. It aids you in faster content creation, faster testing
and overall usually prettier results. Nothing crushes productivity better than
annoying tools (or manual boring work without tools).

;------------------------------------------------------------
;draws a level element
;PARAM1 = X
;PARAM2 = Y
;PARAM3 = TYPE
;returns element width in PARAM4
;returns element height in PARAM5
;------------------------------------------------------------
!zone DrawLevelElement
DrawLevelElement
ldy PARAM3
lda SNELEMENT_TABLE_LO,y
sta .LoadCode + 1
lda SNELEMENT_TABLE_HI,y
sta .LoadCode + 2
lda SNELEMENT_COLOR_TABLE_LO,y
sta .LoadCodeColor + 1
lda SNELEMENT_COLOR_TABLE_HI,y
sta .LoadCodeColor + 2
lda SNELEMENT_WIDTH_TABLE,y
sta PARAM4
lda SNELEMENT_HEIGHT_TABLE,y
sta PARAM5
sta PARAM6

ldy PARAM2
lda SCREEN_LINE_OFFSET_TABLE_LO,y
clc
adc PARAM1
sta .StoreCode + 1
sta .StoreCodeColor + 1
sta ZEROPAGE_POINTER_4
lda SCREEN_LINE_OFFSET_TABLE_HI,y
adc #0
sta .StoreCode + 2
adc #( ( >SCREEN_COLOR ) - ( >SCREEN_CHAR ) )
sta .StoreCodeColor + 2

.NextRow
ldx #0

<< Content
;display a row
.Row

.LoadCode
lda $8000,x
.StoreCode
sta $8000,x

.LoadCodeColor
lda $8000,x
.StoreCodeColor
sta $8000,x

inx
cpx PARAM4
bne .Row

;eine zeile nach unten


dec PARAM6
beq .ElementDone
;should be faster?
lda .LoadCode + 1
clc
adc PARAM4
sta .LoadCode + 1
lda .LoadCode + 2
adc #0
sta .LoadCode + 2

lda .LoadCodeColor + 1
clc
adc PARAM4
sta .LoadCodeColor + 1
lda .LoadCodeColor + 2
adc #0
sta .LoadCodeColor + 2
lda .StoreCode + 1
clc
adc #40
sta .StoreCode + 1
lda .StoreCode + 2
adc #0
sta .StoreCode + 2
lda .StoreCodeColor + 1
clc
adc #40
sta .StoreCodeColor + 1
lda .StoreCodeColor + 2
adc #0
sta .StoreCodeColor + 2
jmp .NextRow

<< Content
.ElementDone
rts

!zone LevelElement
LevelElement
LevelElementArea

; !byte LD_ELEMENT,0,0,EL_BLUE_BRICK_4x3
;X pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM1

;Y pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM2

;type
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM3
;store y for later
tya
pha

jsr DrawLevelElement

jmp NextLevelData

The element line primitives are very similar, they just loop over the element
draw routine:

!zone LevelElementH
LevelElementH
; !byte LD_ELEMENT_LINE_H,x,y,width,element
;X pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM1

;Y pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM2
;x count
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM7
;type

<< Content
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM3
;store y for later
tya
pha

.NextElement
jsr DrawLevelElement

dec PARAM7
beq .Done

lda PARAM1
clc
adc PARAM4
sta PARAM1
jmp .NextElement

.Done
jmp NextLevelData

!zone LevelElementV
LevelElementV
; !byte LD_ELEMENT_LINE_V,x,y,num,element
;X pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM1

;Y pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM2
;y count
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM7
;type
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM3
;store y for later
tya
pha

.NextElement
jsr DrawLevelElement

<< Content
dec PARAM7
beq .Done

lda PARAM2
clc
adc PARAM5
sta PARAM2
jmp .NextElement

.Done
jmp NextLevelData

The editor exports the level structure to a separate file, this is then included
in the main file via the !source macro.

<< Content
Step 46 - Chapter Intro
This time we'll add a chapter intro. The game is supposed to be separated in
themed chapters with an intro for each. There's nothing much to it, we'll
display a short text while the boys drive to their new target.

For now we simply show the first page after starting the game:

lda #0
jsr ShowStory

Most of the code is spent to actually setup the impala and driver sprites. The
text itself is displayed by our trusty DisplayText routine. Followed by the
obligatory wait for button press and release.

;------------------------------------------------------------
;story pages
;------------------------------------------------------------
!zone ShowStory
ShowStory
;clear screen
lda #32
ldy #1
jsr ClearScreen

lda # sta ZEROPAGE_POINTER_1


lda #>TEXT_STORY_1
sta ZEROPAGE_POINTER_1 + 1
lda #1
sta PARAM1
lda #1
sta PARAM2
jsr DisplayText

lda #41
sta PARAM1
lda #20
sta PARAM2
lda #TYPE_IMPALA_1
sta PARAM3
jsr FindEmptySpriteSlot
jsr SpawnObject
lda #44
sta PARAM1
lda #TYPE_IMPALA_DRIVER
sta PARAM3
jsr FindEmptySpriteSlot
jsr SpawnObject

<< Content
lda #44
sta PARAM1
lda #TYPE_IMPALA_2
sta PARAM3
jsr FindEmptySpriteSlot
jsr SpawnObject
lda #47
sta PARAM1
lda #TYPE_IMPALA_3
sta PARAM3
jsr FindEmptySpriteSlot
jsr SpawnObject
lda #48
sta PARAM1
lda #TYPE_IMPALA_DEBRIS
sta PARAM3
jsr FindEmptySpriteSlot
jsr SpawnObject

lda #12
sta VIC_SPRITE_MULTICOLOR_1
lda #11
sta VIC_SPRITE_MULTICOLOR_2

lda #0
sta BUTTON_RELEASED

.StoryLoop
jsr WaitFrame

jsr ObjectControl
;ldx #0
;jsr MoveSpriteLeft
;inx
;jsr MoveSpriteLeft
;inx
;jsr MoveSpriteLeft
;inx
;jsr MoveSpriteLeft
;inx
;jsr MoveSpriteLeft

lda #$10
bit JOYSTICK_PORT_II
bne .ButtonNotPressed

;button pushed
lda BUTTON_RELEASED
beq .StoryLoop

lda #0

<< Content
sta VIC_SPRITE_ENABLE
lda #11
sta VIC_SPRITE_MULTICOLOR_1
lda #1
sta VIC_SPRITE_MULTICOLOR_2
rts

.ButtonNotPressed
lda #1
sta BUTTON_RELEASED
jmp .StoryLoop

The impala objects are simple game objects with the same behaviour. Move
to the center, wait for a while and then move off the left side.

;------------------------------------------------------------
;drive left/pause/drive off left
;------------------------------------------------------------
!zone BehaviourImpala
BehaviourImpalaDebris
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
and #$04
lsr
lsr
clc
adc #SPRITE_DEBRIS_1
sta SPRITE_POINTER_BASE,x
BehaviourImpala
lda SPRITE_STATE,x
beq .DriveFirstHalf
cmp #1
beq .HandlePause

;drive off
jsr MoveSpriteLeft
lda SPRITE_POS_X,x
beq .DriveDone
rts

.DriveDone
jsr RemoveObject
rts

.DriveFirstHalf
jsr MoveSpriteLeft
inc SPRITE_MOVE_POS,x
lda SPRITE_MOVE_POS,x
cmp #200
beq .NextState

<< Content
rts

.NextState
inc SPRITE_STATE,x
lda #0
sta SPRITE_MOVE_POS,x
rts

.HandlePause
inc SPRITE_MOVE_POS,x
beq .NextState
rts

The text is stored as usual. Nice to see, a - acts as CR, * as end of text.

TEXT_STORY_1
!text "A LOCAL NEWSPAPER MENTIONS SEVERAL-"
!text "MISSING PEOPLE",59," THIS SEEMS TO BE A-"
!text "RECURRING PATTERN EVERY 44 YEARS",59,"-"
!text "WE SHOULD INVESTIGATE THE TOWN-"
!text "CEMETARY",59,"*"

<< Content
Step 47 - PowerUps
Poor Sam got nerfed. Now his force range starts out really low (about 5
characters), but can be improved by picking the proper extra. And finally
cheat keys appear. Press 1 to advance to the next stage.

We modify the existing SamUseForce routine to check for a max range (stored
in PLAYER_FORCE_RANGE):

;------------------------------------------------------------
;sam uses power
;returns 1 when holding an enemy
;------------------------------------------------------------
!zone SamUseForce
SamUseForce
lda SPRITE_HELD
beq .NoSpriteHeldNow

lda #1
rts

.NoSpriteHeldNow
stx PARAM6

ldy SPRITE_CHAR_POS_Y,x
dey
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1

ldy SPRITE_CHAR_POS_X,x
lda #0
sta PARAM7

.ShotContinue
lda PARAM7
cmp PLAYER_FORCE_RANGE
beq .OutOfRange
inc PARAM7
;y contains shot X pos
;PARAM6 contains x sprite index of player
ldx PARAM6
lda SPRITE_DIRECTION,x
beq .ShootRight
;shooting left
dey

<< Content
lda (ZEROPAGE_POINTER_1),y
jsr IsCharBlocking
beq .CheckHitEnemy
ldx PARAM6
.ShotDoneMiss
.OutOfRange
lda #0
.ShotDoneHit
rts

.ShootRight
iny

lda (ZEROPAGE_POINTER_1),y
jsr IsCharBlocking
bne .ShotDoneMiss

.CheckHitEnemy
;hit an enemy?
ldx #0

.CheckEnemy
stx PARAM2
sty PARAM1
lda SPRITE_ACTIVE,x
beq .CheckNextEnemy
tax
lda IS_TYPE_ENEMY,x
beq .CheckNextEnemy
ldx PARAM2
;is vulnerable?
lda SPRITE_STATE,x
cmp #128
bpl .CheckNextEnemy
;sprite pos matches on x?
lda SPRITE_CHAR_POS_X,x
cmp PARAM1
bne .CheckNextEnemy

;sprite pos matches on y?


ldy PARAM6
lda SPRITE_CHAR_POS_Y,x
cmp SPRITE_CHAR_POS_Y,y
beq .EnemyHit
;sprite pos matches on y + 1?
clc
adc #1
cmp SPRITE_CHAR_POS_Y,y
beq .EnemyHit
;sprite pos matches on y - 1?
sec

<< Content
sbc #2
cmp SPRITE_CHAR_POS_Y,y
bne .CheckNextEnemy

.EnemyHit
;enemy hit!
stx SPRITE_HELD
inc SPRITE_HELD

;call enemy hit behaviour


ldy SPRITE_ACTIVE,x
;enemy is active
dey
dey
lda ENEMY_HIT_BEHAVIOUR_TABLE_LO,y
sta ZEROPAGE_POINTER_1
lda ENEMY_HIT_BEHAVIOUR_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1

;set up return address for rts


lda #>( .ShotDoneHit - 1 )
pha
lda #<( .ShotDoneHit - 1 )
pha

;1 as return value
lda #1
jmp (ZEROPAGE_POINTER_1)

.CheckNextEnemy
ldx PARAM2
ldy PARAM1
inx
cpx #8
beq .NoEnemyHit

jmp .CheckEnemy
.NoEnemyHit
jmp .ShotContinue

If the extra is picked up the range is simply increased to a max of 38


(Remember, screen width is 40 characters):

.EffectIncForceRange
cpx #0
beq .DeanDoesNotUseForce

lda PLAYER_FORCE_RANGE
clc
adc #2
sta PLAYER_FORCE_RANGE

<< Content
cmp #38
bcs .NotTooLong

lda #38
sta PLAYER_FORCE_RANGE

.NotTooLong
jmp .RemoveItem

Adding cheat keys is way easier. Since the Kernal (yes, with an 'a') comes with
a keyboard check routine we just call that. Notice that for this to work you
must not have the Kernal disabled (remember the memory layout in the
beginning):

JSR $FFE4 ;GETIN


BEQ .NOCHEAT
CMP #49
bne .NOCHEAT

;jump to next level


jsr StartLevel

inc LEVEL_NR
jsr BuildScreen

jsr CopyLevelToBackBuffer
jsr DisplayGetReady
.NOCHEAT

And still something new: Element Areas. A new primitive that fills an area
with m/n repeats of an element:

!zone LevelElementArea
LevelElementArea
;!byte LD_ELEMENT_AREA,24,16,5,1,EL_SN_BROWN_ROCK

;X pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM1
sta PARAM10

;Y pos
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM2
;x count

<< Content
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM7
sta PARAM9
;y count
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM8
;type
iny
lda (ZEROPAGE_POINTER_1),y
sta PARAM3
;store y for later
tya
pha

.NextElementRow
jsr DrawLevelElement

dec PARAM7
beq .RowDone

lda PARAM1
clc
adc PARAM4
sta PARAM1
jmp .NextElementRow

.RowDone
lda PARAM2
clc
adc PARAM5
sta PARAM2

lda PARAM9
sta PARAM7

lda PARAM10
sta PARAM1

dec PARAM8
bne .NextElementRow
jmp NextLevelData

<< Content
Step 48 - More Levels (>>)
see ​www.gamedev.net/blogs/entry/step-48

<< Content
Step 49 - Bug Hunt
Bug fixing time. Don't ever let the bug count get out of hand.

Most enemies wouldn't have a proper hit back effect. Also, some sprites are
moving inside the floor visually. We'll fix that in this step.

Hit back works the same for all enemies, so let's make it a new sub routine.
The routine is to be called at the start of an enemies custom behaviour code:

;------------------------------------------------------------
;handles simple hitback
;------------------------------------------------------------
!zone HandleHitBack
HandleHitBack
lda SPRITE_HITBACK,x
beq .NoHitBack
dec SPRITE_HITBACK,x
lda SPRITE_HITBACK_DIRECTION,x
beq .HitBackRight

;move left
jsr ObjectMoveLeftBlocking
lda #1
rts

.HitBackRight
jsr ObjectMoveRightBlocking
lda #1
rts

.NoHitBack
lda #0
rts

For every behaviour routine we add this in:


jsr HandleHitBack
beq .NoHitBack
rts

.NoHitBack

Fortunately, to fix the sprite offset problem we already have the solution in
form of a start offset table. We just amend some values (note all the new
entries with value 2):

TYPE_START_DELTA_Y

<< Content
!byte 0 ;dummy
!byte 0 ;player dean
!byte 0 ;bat 1
!byte 0 ;bat 1
!byte 0 ;bat 2
!byte 0 ;mummy
!byte 0 ;zombie
!byte 0 ;nasty bat
!byte 0 ;spider
!byte 0 ;explosion
!byte 0 ;player sam
!byte 2 ;wolf
!byte 0 ;ghost skeleton
!byte 2 ;jumping toad
!byte 0 ;eye
!byte 0 ;floating ghost
!byte 0 ;fly
!byte 2 ;slime
!byte 2 ;frankenstein
!byte 2 ;hand
!byte 2 ;devil
!byte 0 ;impala 1
!byte 0 ;impala 2
!byte 0 ;impala 3
!byte 0 ;impala driver
!byte 0 ;impala debris

<< Content
Step 50 - PowerUps II
In this step the powerups for range increase and decrease reload delay are
made permanent. They won't fade with time. You can also collect up to five
extras to reach the maximum. And the powerup stays even when you get
killed. (Don't you hate it when you die in Bubble Bobble and you're slow
again).

Previously we had a flag that stored the time left of a faster reload. The max
times are now kept in a table, and a faster reload step value is stored instead.

The speed table RELOAD_SPEED_MAX and other counters for this update:

PLAYER_RELOAD_SPEED
!byte 0
RELOAD_SPEED
!byte 1,1,1,1,1
RELOAD_SPEED_MAX
!byte 40,35,30,25,20

Initialising on game restart:


lda #0
sta PLAYER_RELOAD_SPEED

During standing still the reload speed value is now used to count down the
time.

ldy PLAYER_RELOAD_SPEED
lda PLAYER_STAND_STILL_TIME
clc
adc RELOAD_SPEED,y
cmp RELOAD_SPEED_MAX,y
bcs .ReloadTimeDone

sta PLAYER_STAND_STILL_TIME
jmp .HandleFire

.ReloadTimeDone
lda #0
sta PLAYER_STAND_STILL_TIME

During pickup of the increase reload speed the counter needs to be updated:

lda PLAYER_RELOAD_SPEED
cmp #4
beq .SpeedHighestAlready
inc PLAYER_RELOAD_SPEED

<< Content
.SpeedHighestAlready

Making the force range increase permanent is even easier, we simply remove
all instances where it was reset to the start value on respawning and starting
the next level.

Step 51 - Coop Mode


More cooperative hodge podge. An experimental feature, both players are
needed to kill an enemy. Sam must hold the enemy, so Dean can shoot it.

To accomodate this the changes are quite simple:

First block the hurting of enemies when they are held by Sam:

ldy SPRITE_HELD
dey
ldx SPRITE_ACTIVE,y
lda IS_TYPE_ENEMY,x
cmp #2
bne .NormalHurtByForce

;in 2p mode?
;TODO - if only one player is left?
lda GAME_MODE
cmp #2
bne .NormalHurtByForce

;no further action


jmp .NoEnemyHeld

.NormalHurtByForce
ldx PARAM6

Then, when Dean's bullet hits, check if the play mode is 2 player, and the
enemy is actually held by Sam:

;is two player enemy?


ldy SPRITE_ACTIVE,x
lda IS_TYPE_ENEMY,y
cmp #2

<< Content
bne .HitEnemy

;in 2p mode?
;TODO - if only one player is left?
lda GAME_MODE
cmp #2
bne .HitEnemy

ldy SPRITE_HELD
dey
sty PARAM1
cpx PARAM1
beq .HitEnemy

;enemy would be hit, but is not held


jmp .ShotDone

.HitEnemy

And obviously, when an enemy is hurt, release Sam's lock:

lda SPRITE_HELD
sta PARAM1
dec PARAM1
cpx PARAM1
bne .NotHeldEnemy

lda #0
sta SPRITE_HELD

.NotHeldEnemy

As you can see, there're still TODOs left. Also, if 2 player mode makes this
behaviour general or only for special enemies is yet to be decided.

<< Content
Step 52 - Visible Spawnpoints
To make spawning enemies a bit more player friendly spawns are now shown
before the enemy actually appears. The way to implement this is quite simple.
We've got a neat object system running already, so we make the spawn
animation just another type. Once the spawn life time is up the object is
replaced in spot with the proper object type.

During a spawn point process we store the target type in SPRITE_ANNOYED


(since spawns do not get annoyed):

;store spawn type in SPRITE_ANNOYED


ldx PARAM7
lda PARAM5
sta SPRITE_ANNOYED,x

The spawns behaviour is straight forward, animate, count life time down and
finally spawn the final object:

;------------------------------------------------------------
;Spawn
;------------------------------------------------------------
!zone BehaviourSpawn
BehaviourSpawn
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #3
beq .UpdateAnimation
rts

.UpdateAnimation
lda #0
sta SPRITE_ANIM_DELAY,x

lda SPRITE_POINTER_BASE,x
eor #$01
sta SPRITE_POINTER_BASE,x

inc SPRITE_MOVE_POS,x
lda SPRITE_MOVE_POS,x
cmp #20
beq .SpawnNow
rts

.SpawnNow
lda SPRITE_ANNOYED,x

<< Content
sta PARAM3
lda SPRITE_CHAR_POS_X,x
sta PARAM1
lda SPRITE_CHAR_POS_Y,x
sta PARAM2
stx PARAM7
lda #1
jsr SpawnObject
ldx PARAM7
rts

The rest is too simple to show it here line by line, add new constants for the
sprite, add the entry to the behaviour and hurt tables, add entries to the type
start tables (color, sprite, etc.) and we're done.

And now the player is not killed by suddenly appearing enemies.

Step 53 - Boss Fight


Here comes our first boss. Nothing too difficult, but different. Let's see what
this works out to ;-)

The boss moves quite similar to the ghost however he's got a special attack. If
nothing else happens the boss is homing in on you. If you shoot him two
times he goes into attack mode (and you better step back).

For performance reason the beams are made of characters, a horizontal and
vertical line at the boss position.

;------------------------------------------------------------
;boss
;------------------------------------------------------------
!zone BehaviourBoss
BehaviourBoss

BOSS_MOVE_SPEED = 1
lda SPRITE_HITBACK,x
beq .NoHitBack
dec SPRITE_HITBACK,x

ldy SPRITE_HITBACK,x
lda BOSS_FLASH_TABLE,y
sta VIC_SPRITE_COLOR,x

<< Content
cpy #0
bne .NoHitBack

;make vulnerable again


lda SPRITE_STATE,x
cmp #128
bne .NoHitBack

lda #0
sta SPRITE_STATE,x

.NoHitBack
lda DELAYED_GENERIC_COUNTER
and #$03
bne .NoAnimUpdate

lda SPRITE_POINTER_BASE,x
eor #$01
sta SPRITE_POINTER_BASE,x

.NoAnimUpdate
lda SPRITE_STATE,x
and #$7f
bne .NotFollowPlayer
jmp .FollowPlayer

.NotFollowPlayer
cmp #1
beq .AttackMode
rts

.AttackMode
;Attack modes (more modes?)
inc SPRITE_MOVE_POS,x
lda SPRITE_MOVE_POS,x
cmp #4
beq .NextAttackStep
rts

.NextAttackStep
lda #0
sta SPRITE_MOVE_POS,x
inc SPRITE_MODE_POS,x

lda SPRITE_MODE_POS,x
cmp #11
bcc .BeamNotDangerous
cmp #29
bcs .BeamNotDangerous

<< Content
;does player hit beam?
ldy #0
jsr CheckIsPlayerCollidingWithBeam
ldy #1
jsr CheckIsPlayerCollidingWithBeam

.BeamNotDangerous
lda SPRITE_MODE_POS,x
cmp #11
beq .BeamStep1
cmp #12
beq .BeamStep2
cmp #13
beq .BeamStep3
cmp #16
beq .BeamStep4
cmp #17
beq .BeamStep3
cmp #18
beq .BeamStep4
cmp #19
beq .BeamStep3
cmp #20
beq .BeamStep4
cmp #21
beq .BeamStep3
cmp #22
beq .BeamStep4
cmp #23
beq .BeamStep3
cmp #24
beq .BeamStep4
cmp #25
beq .BeamStep3
cmp #26
beq .BeamStep4
cmp #27
beq .BeamStep3
cmp #28
beq .BeamStep4
cmp #29
beq .BeamStep3
cmp #30
beq .BeamEnd
rts

.BeamStep1
;beam
lda #BEAM_TYPE_DARK
jsr .DrawBeam
rts

<< Content
.BeamStep2
;beam
lda #BEAM_TYPE_MEDIUM
jsr .DrawBeam
rts

.BeamStep3
;beam
lda #BEAM_TYPE_LIGHT
jsr .DrawBeam
rts

.BeamStep4
;beam
lda #BEAM_TYPE_LIGHT2
jsr .DrawBeam
rts

.BeamEnd
jsr .RestoreBeam

lda #0
sta SPRITE_STATE,x
rts

.DrawBeam
tay
lda BEAM_CHAR_H,y
sta PARAM1
lda BEAM_CHAR_V,y
sta PARAM2
lda BEAM_COLOR,y
sta PARAM3

ldy SPRITE_CHAR_POS_Y,x

lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
sta ZEROPAGE_POINTER_2
lda SCREEN_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 )
sta ZEROPAGE_POINTER_2 + 1

stx PARAM6

ldy #1
.HLoop
lda PARAM1

<< Content
sta (ZEROPAGE_POINTER_1),y
lda PARAM3
sta (ZEROPAGE_POINTER_2),y
iny
cpy #39
bne .HLoop
;vertical beam
ldy SPRITE_CHAR_POS_X,x
ldx #1

.NextLine
lda SCREEN_LINE_OFFSET_TABLE_LO,x
sta ZEROPAGE_POINTER_1
sta ZEROPAGE_POINTER_2
lda SCREEN_LINE_OFFSET_TABLE_HI,x
sta ZEROPAGE_POINTER_1 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 )
sta ZEROPAGE_POINTER_2 + 1

lda PARAM2
sta (ZEROPAGE_POINTER_1),y
lda PARAM3
sta (ZEROPAGE_POINTER_2),y

inx
cpx #22
bne .NextLine
ldx PARAM6
rts

.RestoreBeam
ldy SPRITE_CHAR_POS_Y,x

lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
sta ZEROPAGE_POINTER_2
sta ZEROPAGE_POINTER_3
sta ZEROPAGE_POINTER_4
lda SCREEN_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1
sec
sbc #( ( SCREEN_CHAR - SCREEN_BACK_CHAR ) >> 8 )
sta ZEROPAGE_POINTER_2 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) >> 8 )
sta ZEROPAGE_POINTER_3 + 1
sec
sbc #( ( SCREEN_COLOR - SCREEN_BACK_COLOR ) >> 8 )
sta ZEROPAGE_POINTER_4 + 1

<< Content
stx PARAM6

ldy #1

-
lda (ZEROPAGE_POINTER_2),y
sta (ZEROPAGE_POINTER_1),y
lda (ZEROPAGE_POINTER_4),y
sta (ZEROPAGE_POINTER_3),y
iny
cpy #39
bne -
;vertical beam
ldy SPRITE_CHAR_POS_X,x
ldx #1

.NextLineR
lda SCREEN_LINE_OFFSET_TABLE_LO,x
sta ZEROPAGE_POINTER_1
sta ZEROPAGE_POINTER_2
sta ZEROPAGE_POINTER_3
sta ZEROPAGE_POINTER_4
lda SCREEN_LINE_OFFSET_TABLE_HI,x
sta ZEROPAGE_POINTER_1 + 1
clc
adc #( ( SCREEN_BACK_CHAR - SCREEN_CHAR ) >> 8 )
sta ZEROPAGE_POINTER_2 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) >> 8 )
sta ZEROPAGE_POINTER_3 + 1
sec
sbc #( ( SCREEN_COLOR - SCREEN_BACK_COLOR ) >> 8 )
sta ZEROPAGE_POINTER_4 + 1
lda (ZEROPAGE_POINTER_2),y
sta (ZEROPAGE_POINTER_1),y
lda (ZEROPAGE_POINTER_4),y
sta (ZEROPAGE_POINTER_3),y

inx
cpx #22
bne .NextLineR
ldx PARAM6
rts

.FollowPlayer
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x
cmp #10
beq .DoCheckMove
jmp .DoGhostMove
.DoCheckMove

<< Content
lda #0
sta SPRITE_ANIM_DELAY,x

txa
and #$01
tay
lda SPRITE_ACTIVE,y
cmp #TYPE_PLAYER_DEAN
beq .FoundPlayer
cmp #TYPE_PLAYER_SAM
beq .FoundPlayer

;check other player


tya
eor #1
tay
lda SPRITE_ACTIVE,y
cmp #TYPE_PLAYER_DEAN
beq .FoundPlayer
cmp #TYPE_PLAYER_SAM
beq .FoundPlayer

;no player to hunt


rts

.FoundPlayer
;player index in y
lda SPRITE_CHAR_POS_X,y
cmp SPRITE_CHAR_POS_X,x
bpl .MoveRight

;move left
lda SPRITE_DIRECTION,x
bne .AlreadyLookingLeft
lda SPRITE_MOVE_POS,x
beq .TurnLNow
dec SPRITE_MOVE_POS,x
bne .CheckYNow

.TurnLNow
;turning now
lda #1
sta SPRITE_DIRECTION,x
lda #SPRITE_BOSS_L_1
sta SPRITE_POINTER_BASE,x
jmp .CheckYNow

.AlreadyLookingLeft
lda SPRITE_MOVE_POS,x
cmp #BOSS_MOVE_SPEED
beq .CheckYNow

<< Content
inc SPRITE_MOVE_POS,x
jmp .CheckYNow

.MoveRight
lda SPRITE_DIRECTION,x
beq .AlreadyLookingRight

lda SPRITE_MOVE_POS,x
beq .TurnRNow
dec SPRITE_MOVE_POS,x
bne .CheckYNow

;turning now
.TurnRNow
lda #0
sta SPRITE_DIRECTION,x
lda #SPRITE_BOSS_R_1
sta SPRITE_POINTER_BASE,x
jmp .CheckYNow

.AlreadyLookingRight
lda SPRITE_MOVE_POS,x
cmp #BOSS_MOVE_SPEED
beq .CheckYNow
inc SPRITE_MOVE_POS,x
jmp .CheckYNow

.CheckYNow
;player index in y
lda SPRITE_CHAR_POS_Y,y
cmp SPRITE_CHAR_POS_Y,x
bpl .MoveDown

;move left
lda SPRITE_DIRECTION_Y,x
bne .AlreadyLookingUp
lda SPRITE_MOVE_POS_Y,x
beq .TurnUNow
dec SPRITE_MOVE_POS_Y,x
bne .DoGhostMove

.TurnUNow
;turning now
lda #1
sta SPRITE_DIRECTION_Y,x
jmp .DoGhostMove

.AlreadyLookingUp
lda SPRITE_MOVE_POS_Y,x
cmp #BOSS_MOVE_SPEED
beq .DoGhostMove

<< Content
inc SPRITE_MOVE_POS_Y,x
jmp .DoGhostMove

.MoveDown
lda SPRITE_DIRECTION_Y,x
beq .AlreadyLookingDown

lda SPRITE_MOVE_POS_Y,x
beq .TurnDNow
dec SPRITE_MOVE_POS_Y,x
bne .DoGhostMove

;turning now
.TurnDNow
lda #0
sta SPRITE_DIRECTION_Y,x
jmp .DoGhostMove

.AlreadyLookingDown
lda SPRITE_MOVE_POS_Y,x
cmp #BOSS_MOVE_SPEED
beq .DoGhostMove
inc SPRITE_MOVE_POS_Y,x
jmp .DoGhostMove
.DoGhostMove
;move X times
ldy SPRITE_MOVE_POS,x
sty PARAM4
beq .DoY

lda SPRITE_DIRECTION,x
beq .DoRight
.MoveLoopL
jsr ObjectMoveLeftBlocking
dec PARAM4
bne .MoveLoopL
jmp .DoY

.DoRight
.MoveLoopR
jsr ObjectMoveRightBlocking
dec PARAM4
bne .MoveLoopR

.DoY
;move X times
ldy SPRITE_MOVE_POS_Y,x
sty PARAM4
beq .MoveDone

lda SPRITE_DIRECTION_Y,x

<< Content
beq .DoDown
.MoveLoopU
jsr ObjectMoveUpBlocking
dec PARAM4
bne .MoveLoopU
jmp .MoveDone

.DoDown
.MoveLoopD
jsr ObjectMoveDownBlockingNoPlatform
dec PARAM4
bne .MoveLoopD

.MoveDone
rts

Checking the player for collision with the beam is heavily simplified due to the
beam being horizontal/vertical. It's a cheap comparison of position values:

;------------------------------------------------------------
;check player vs. beam
; beam boss index in x
; player index in y
;------------------------------------------------------------
!zone CheckIsPlayerCollidingWithBeam
CheckIsPlayerCollidingWithBeam
lda SPRITE_ACTIVE,y
bne .PlayerIsActive
.PlayerNotActive
rts

.PlayerIsActive
lda SPRITE_STATE,y
cmp #128
bcs .PlayerNotActive

;compare char positions in x


lda SPRITE_CHAR_POS_X,x
cmp SPRITE_CHAR_POS_X,y
beq .PlayerHit

clc
adc #1
cmp SPRITE_CHAR_POS_X,y
beq .PlayerHit

sec
sbc #2
cmp SPRITE_CHAR_POS_X,y
beq .PlayerHit

<< Content
;compare char positions in y
lda SPRITE_CHAR_POS_Y,x
cmp SPRITE_CHAR_POS_Y,y
beq .PlayerHit

clc
adc #1
cmp SPRITE_CHAR_POS_Y,y
beq .PlayerHit

sec
sbc #2
cmp SPRITE_CHAR_POS_Y,y
beq .PlayerHit

;not hit
rts

.PlayerHit
;player killed
lda #129
sta SPRITE_STATE,y

lda #SPRITE_PLAYER_DEAD
sta SPRITE_POINTER_BASE,y

lda #0
sta SPRITE_MOVE_POS,y

lda SPRITE_ACTIVE,y
cmp #TYPE_PLAYER_SAM
bne .PlayerWasDean

;reset Sam specific variables


lda #0
sta SPRITE_HELD

.PlayerWasDean
rts

The boss only enters attack mode for every second hit, therefore he gets a
special treatment in his hit function:

;------------------------------------------------------------
;hit behaviour for boss
;------------------------------------------------------------
!zone HitBehaviourBoss
HitBehaviourBoss
lda #8
sta SPRITE_HITBACK,x

<< Content
;make invincible for a short while
lda SPRITE_STATE,x
ora #$80
sta SPRITE_STATE,x

;boss switches tactic


lda SPRITE_HP,x
and #$01
beq .SwitchToAttack
rts

.SwitchToAttack
lda #129
sta SPRITE_STATE,x

lda #0
sta SPRITE_MODE_POS,x
sta SPRITE_MOVE_POS,x
sta SPRITE_MOVE_POS_Y,x
rts

The less interesting parts are known, add new values for the boss object plus
the color/character tables for the beam.

<< Content
Step 54 - Bug Hunt
A direly needed step, bug fixing. Usually if I encounter a bug and for whatever
reason can't fix it right away I'll note it down (it's just too easy to forget). This
step fixes:

Spawn count bug fixed (a stage would not be detected as done)


Major slowdown when playing with Sam (bad bad bug)
Car sprite error in story page (failure to properly reset)
Left over char N in mode choice (tiny oversight)
Other player spawing although no lives left (failure of proper two player
testing)

Most of these bugs are trivial and so it's not much use going into details here.

It can't be stressed enough. Often I catch myself encountering a bug while


playing and just thinking "I'll fix that later". Well, in a lot of cases I forget. If
you write a game, test play it, encounter a bug: Break the game, fix the bug.
It'll move you forward faster.

see: ​www.gamedev.net/blogs/entry/step-54/

Step 55 - The next 10 Levels


Yet another step without much details. The next 10 stages await you. If you
want to check them out, don't forget, that you can press '1' to advance to the
next stage. Since this step is somewhat short, find the level editor I'm using
attached as well. Open the file "Supernatural.elementeditorproject". The
editor requires the charset and spriteset files in the same folder as the editor
project. The editor is a quick hack job and therefore a bit buggy. Save often. It
worked good enough for Soulless though ;-)

If you add new stages and want to test them, go to the export tab, chose the
proper location for the export file (level_data.asm) and press "Export". A
simple recompile with C64Studio will incorporate the new stages.

Careful, do not have elements going outside the screen boundaries!

see: ​www.gamedev.net/blogs/entry/step-55

<< Content
Step 56 - New Boss
And here's the next boss. A striking similarity to the first boss, and yet a little
bit different.

The main behaviour of this boss is the same as the previous, only the beam is
diagonal this time.

Making a diagonal beam is somewhat more difficult, as you need to check for
both playfield borders. The beams are started from the boss position and
then all 4 directions are tackled after each other.

;------------------------------------------------------------
;boss
;------------------------------------------------------------
!zone BehaviourBoss2
BehaviourBoss2

BOSS_MOVE_SPEED = 1
lda SPRITE_HITBACK,x
beq .NoHitBack
dec SPRITE_HITBACK,x

ldy SPRITE_HITBACK,x
lda BOSS_FLASH_TABLE,y
sta VIC_SPRITE_COLOR,x

cpy #0
bne .NoHitBack

;make vulnerable again


lda SPRITE_STATE,x
cmp #128
bne .NoHitBack

lda #0
sta SPRITE_STATE,x

.NoHitBack
lda DELAYED_GENERIC_COUNTER
and #$03
bne .NoAnimUpdate

;lda SPRITE_POINTER_BASE,x
;eor #$01
;sta SPRITE_POINTER_BASE,x

<< Content
.NoAnimUpdate
lda SPRITE_STATE,x
and #$7f
bne .NotFollowPlayer
jmp BossFollowPlayer

.NotFollowPlayer
cmp #1
beq .AttackMode
rts

.AttackMode
;Attack modes (more modes?)
inc SPRITE_MOVE_POS,x
lda SPRITE_MOVE_POS,x
cmp #4
beq .NextAttackStep
rts

.NextAttackStep
lda #0
sta SPRITE_MOVE_POS,x
inc SPRITE_MODE_POS,x

lda SPRITE_MODE_POS,x
cmp #11
bcc .BeamNotDangerous
cmp #29
bcs .BeamNotDangerous

;does player hit beam?


ldy #0
jsr CheckIsPlayerCollidingWithDiagonalBeam
ldy #1
jsr CheckIsPlayerCollidingWithDiagonalBeam

.BeamNotDangerous
lda SPRITE_MODE_POS,x
cmp #11
beq .BeamStep1
cmp #12
beq .BeamStep2
cmp #13
beq .BeamStep3
cmp #16
beq .BeamStep4
cmp #17
beq .BeamStep3
cmp #18
beq .BeamStep4
cmp #19

<< Content
beq .BeamStep3
cmp #20
beq .BeamStep4
cmp #21
beq .BeamStep3
cmp #22
beq .BeamStep4
cmp #23
beq .BeamStep3
cmp #24
beq .BeamStep4
cmp #25
beq .BeamStep3
cmp #26
beq .BeamStep4
cmp #27
beq .BeamStep3
cmp #28
beq .BeamStep4
cmp #29
beq .BeamStep3
cmp #30
beq .BeamEnd
rts

.BeamStep1
;beam
lda #BEAM_TYPE_DARK
jsr .DrawBeamDiagonal
rts

.BeamStep2
;beam
lda #BEAM_TYPE_MEDIUM
jsr .DrawBeamDiagonal
rts

.BeamStep3
;beam
lda #BEAM_TYPE_LIGHT
jsr .DrawBeamDiagonal
rts

.BeamStep4
;beam
lda #BEAM_TYPE_LIGHT2
jsr .DrawBeamDiagonal
rts

.BeamEnd
jsr RestoreBeamDiagonal

<< Content
lda #0
sta SPRITE_STATE,x
rts

.DrawBeamDiagonal
tay
lda BEAM_CHAR_NWSE,y
sta PARAM1
lda BEAM_CHAR_NESW,y
sta PARAM2
lda BEAM_COLOR,y
sta PARAM3

ldy SPRITE_CHAR_POS_Y,x
sty PARAM9
stx PARAM6

lda SPRITE_CHAR_POS_X,x
sta PARAM7
sta PARAM8

.NextUpperLine
ldy PARAM9
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
sta ZEROPAGE_POINTER_2
lda SCREEN_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 )
sta ZEROPAGE_POINTER_2 + 1

;upper part
;left
ldy PARAM7
beq .NoLeftPart
lda PARAM1
sta (ZEROPAGE_POINTER_1),y
lda PARAM3
sta (ZEROPAGE_POINTER_2),y

.NoLeftPart
;right
ldy PARAM8
beq .NoRightPart
lda PARAM2
sta (ZEROPAGE_POINTER_1),y
lda PARAM3
sta (ZEROPAGE_POINTER_2),y

<< Content
.NoRightPart
dec PARAM9
beq .UpperPartDone

;left border reached?


lda PARAM7
beq .LeftDone
dec PARAM7
.LeftDone
lda PARAM8
beq .RightEndReached
cmp #38
beq .RightEndReached
inc PARAM8
jmp .NextUpperLine

.RightEndReached
lda #0
sta PARAM8

jmp .NextUpperLine

.UpperPartDone

;lower part
ldy SPRITE_CHAR_POS_Y,x
sty PARAM9
stx PARAM6

lda SPRITE_CHAR_POS_X,x
sta PARAM7
sta PARAM8

.NextUpperLineBottom
ldy PARAM9
lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
sta ZEROPAGE_POINTER_2
lda SCREEN_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 )
sta ZEROPAGE_POINTER_2 + 1

;upper part
;left
ldy PARAM7
beq .NoLeftPartBottom
lda PARAM2

<< Content
sta (ZEROPAGE_POINTER_1),y
lda PARAM3
sta (ZEROPAGE_POINTER_2),y

.NoLeftPartBottom
;right
ldy PARAM8
beq .NoRightPartBottom
lda PARAM1
sta (ZEROPAGE_POINTER_1),y
lda PARAM3
sta (ZEROPAGE_POINTER_2),y

.NoRightPartBottom
inc PARAM9
lda PARAM9
cmp #22
beq .LowerPartDone

;left border reached?


lda PARAM7
beq .LeftDoneBottom
dec PARAM7
.LeftDoneBottom
lda PARAM8
beq .RightEndReachedBottom
cmp #38
beq .RightEndReachedBottom
inc PARAM8
jmp .NextUpperLineBottom

.RightEndReachedBottom
lda #0
sta PARAM8

jmp .NextUpperLineBottom

.LowerPartDone
ldx PARAM6
rts

Obviously all the same is repeated for the removal of the beam.

!zone RestoreBeamDiagonal
RestoreBeamDiagonal
ldy SPRITE_CHAR_POS_Y,x

stx PARAM6

ldy SPRITE_CHAR_POS_Y,x
sty PARAM9

<< Content
stx PARAM6

lda SPRITE_CHAR_POS_X,x
sta PARAM7
sta PARAM8

.NextUpperLine
ldy PARAM9

lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
sta ZEROPAGE_POINTER_2
sta ZEROPAGE_POINTER_3
sta ZEROPAGE_POINTER_4
lda SCREEN_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1
sec
sbc #( ( SCREEN_CHAR - SCREEN_BACK_CHAR ) >> 8 )
sta ZEROPAGE_POINTER_2 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) >> 8 )
sta ZEROPAGE_POINTER_3 + 1
sec
sbc #( ( SCREEN_COLOR - SCREEN_BACK_COLOR ) >> 8 )
sta ZEROPAGE_POINTER_4 + 1

;upper part
;left
ldy PARAM7
beq .NoLeftPart
lda (ZEROPAGE_POINTER_2),y
sta (ZEROPAGE_POINTER_1),y
lda (ZEROPAGE_POINTER_4),y
sta (ZEROPAGE_POINTER_3),y

.NoLeftPart
;right
ldy PARAM8
beq .NoRightPart

lda (ZEROPAGE_POINTER_2),y
sta (ZEROPAGE_POINTER_1),y
lda (ZEROPAGE_POINTER_4),y
sta (ZEROPAGE_POINTER_3),y

.NoRightPart
dec PARAM9
beq .UpperPartDone

;left border reached?

<< Content
lda PARAM7
beq .LeftDone
dec PARAM7
.LeftDone
lda PARAM8
beq .RightEndReached
cmp #38
beq .RightEndReached
inc PARAM8
jmp .NextUpperLine

.RightEndReached
lda #0
sta PARAM8

jmp .NextUpperLine

.UpperPartDone

;lower part
ldy SPRITE_CHAR_POS_Y,x
sty PARAM9
stx PARAM6

lda SPRITE_CHAR_POS_X,x
sta PARAM7
sta PARAM8

.NextUpperLineBottom
ldy PARAM9

lda SCREEN_LINE_OFFSET_TABLE_LO,y
sta ZEROPAGE_POINTER_1
sta ZEROPAGE_POINTER_2
sta ZEROPAGE_POINTER_3
sta ZEROPAGE_POINTER_4
lda SCREEN_LINE_OFFSET_TABLE_HI,y
sta ZEROPAGE_POINTER_1 + 1
sec
sbc #( ( SCREEN_CHAR - SCREEN_BACK_CHAR ) >> 8 )
sta ZEROPAGE_POINTER_2 + 1
clc
adc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) >> 8 )
sta ZEROPAGE_POINTER_3 + 1
sec
sbc #( ( SCREEN_COLOR - SCREEN_BACK_COLOR ) >> 8 )
sta ZEROPAGE_POINTER_4 + 1

;upper part
;left
ldy PARAM7

<< Content
beq .NoLeftPartBottom

lda (ZEROPAGE_POINTER_2),y
sta (ZEROPAGE_POINTER_1),y
lda (ZEROPAGE_POINTER_4),y
sta (ZEROPAGE_POINTER_3),y

.NoLeftPartBottom
;right
ldy PARAM8
beq .NoRightPartBottom

lda (ZEROPAGE_POINTER_2),y
sta (ZEROPAGE_POINTER_1),y
lda (ZEROPAGE_POINTER_4),y
sta (ZEROPAGE_POINTER_3),y

.NoRightPartBottom
inc PARAM9
lda PARAM9
cmp #22
beq .LowerPartDone

;left border reached?


lda PARAM7
beq .LeftDoneBottom
dec PARAM7

.LeftDoneBottom
lda PARAM8
beq .RightEndReachedBottom
cmp #38
beq .RightEndReachedBottom
inc PARAM8
jmp .NextUpperLineBottom

.RightEndReachedBottom
lda #0
sta PARAM8

jmp .NextUpperLineBottom

.LowerPartDone
ldx PARAM6
rts

Checking if the player is hit by the beam as a bit different as well. The
previous boss beam was checked simply by comparing X and Y pos. Now we
take the distances between X and Y pos and compare them. They must be
either equal or only differ by one to count as colliding.

<< Content
;------------------------------------------------------------
;check player vs. diagonal beam
; beam boss index in x
; player index in y
;------------------------------------------------------------
!zone CheckIsPlayerCollidingWithDiagonalBeam
CheckIsPlayerCollidingWithDiagonalBeam
lda SPRITE_ACTIVE,y
bne .PlayerIsActive

.PlayerNotActive
rts

.PlayerIsActive
lda SPRITE_STATE,y
cmp #128
bcs .PlayerNotActive

;compare char positions in x


lda SPRITE_CHAR_POS_X,x
sec
sbc SPRITE_CHAR_POS_X,y
bpl .PositiveX

lda SPRITE_CHAR_POS_X,y
sec
sbc SPRITE_CHAR_POS_X,x
.PositiveX
sta PARAM1

lda SPRITE_CHAR_POS_Y,x
sec
sbc SPRITE_CHAR_POS_Y,y
bpl .PositiveY

lda SPRITE_CHAR_POS_Y,y
sec
sbc SPRITE_CHAR_POS_Y,x
.PositiveY
sta PARAM2

lda PARAM1
cmp PARAM2
beq .PlayerHit

lda PARAM1
sec
sbc PARAM2
and #$7f
cmp #1
beq .PlayerHit

<< Content
;not hit
rts

.PlayerHit
;player killed
lda #129
sta SPRITE_STATE,y

lda #SPRITE_PLAYER_DEAD
sta SPRITE_POINTER_BASE,y

lda #0
sta SPRITE_MOVE_POS,y

lda SPRITE_ACTIVE,y
cmp #TYPE_PLAYER_SAM
bne .PlayerWasDean

;reset Sam specific variables


lda #0
sta SPRITE_HELD

.PlayerWasDean
rts

Play the first boss, then stumble upon this one. It's not really difficult as well,
but annoying to get the habit of moving out of the way for the first boss out
of your mind ;-)

Have fun!

<< Content
Step 57 - Borderless Level
This is a technically more sophisticated step. We'll add the possibility of a
borderless level, and also a second charset.

Especially the latter requires a thorough planning of where to put things in


memory. Remember, the VIC (the C64s graphic chip) can only see a 16K range
at once. This means, all visible characters, sprite and screen memory must
reside inside one 16K bank.

The new memory layout looks like this:

;screen back color $BC00 to $BFFF


;charset2 $C000 to $C800
;screen back char $C800 to $CBFF
;screen $CC00 to $CFFF
;sprites $D000 to $F7FF
;charset $F800 to $FFFF

Once that's down (and fits) the code changes are rather simple. A new byte is
added per screen to allow for 8 flags. Bit 0 means no border, bit 1 means
second charset.

Implementing borderless is easy, check the bit, and it set, skip the level
border drawing:

lda LEVEL_CONFIG
and #$01
bne .SkipBorder
;draw level border
lda # sta ZEROPAGE_POINTER_1
lda #>LEVEL_BORDER_DATA
sta ZEROPAGE_POINTER_1 + 1
jsr .BuildLevel

.SkipBorder
sta VIC_MEMORY_CONTROL

Actually, the second char set is quite similar to implement:


;.. snip
lda LEVEL_CONFIG
and #$02
beq .SetCharSet1

;set charset 2

<< Content
lda #$30
-
sta VIC_MEMORY_CONTROL
jsr DisplayLevelNumber

rts

.SetCharSet1
lda #$3e
jmp -

Setting the second char set is as simple as adding another copy routine:

;take source address from CHARSET 2


LDA # STA ZEROPAGE_POINTER_1
LDA #>CHARSET_2
STA ZEROPAGE_POINTER_1 + 1

;now copy
jsr CopyCharSet2

;------------------------------------------------------------
;copies charset from ZEROPAGE_POINTER_1 to ZEROPAGE_POINTER_2
;------------------------------------------------------------
!zone CopyCharSet2
CopyCharSet2
;set target address ($F000)
lda #$00
sta ZEROPAGE_POINTER_2
lda #$C0
sta ZEROPAGE_POINTER_2 + 1
ldx #$00
ldy #$00
lda #0
sta PARAM2
.NextLine
lda (ZEROPAGE_POINTER_1),Y
sta (ZEROPAGE_POINTER_2),Y
inx
iny
cpx #$08
bne .NextLine
cpy #$00
bne .PageBoundaryNotReached

;we have reached the next 256 bytes, inc high byte
inc ZEROPAGE_POINTER_1 + 1
inc ZEROPAGE_POINTER_2 + 1

.PageBoundaryNotReached
;only copy 254 chars to keep irq vectors intact

<< Content
inc PARAM2
lda PARAM2
cmp #254
beq .CopyCharsetDone
ldx #$00
jmp .NextLine

.CopyCharsetDone
rts

The last one is a subtle change, due to the border not always used the
ClearPlayScreen routine is adjusted to clear one more line.

Have fun!

Step 58 - Custom Music (>>)


Another entry with no code change at all. Richard Bayliss created custom
music for the game. Code wise there is no change, just the music player
binary included is a different one.

Enjoy!

see: ​www.gamedev.net/blogs/entry/step-58

<< Content
Step 58 - Boss N​o​ 3
And yet another boss, this time a bit different than the previous two.

;------------------------------------------------------------
;boss 3
;------------------------------------------------------------
!zone BehaviourBoss3
BehaviourBoss3
BOSS_MOVE_SPEED = 1
lda SPRITE_HITBACK,x
beq .NoHitBack
dec SPRITE_HITBACK,x

ldy SPRITE_HITBACK,x
lda BOSS_FLASH_TABLE,y
sta VIC_SPRITE_COLOR,x

cpy #0
bne .NoHitBack

;make vulnerable again


;lda SPRITE_STATE,x
;cmp #128
;bne .NoHitBack

lda #0
sta SPRITE_STATE,x

.NoHitBack
lda DELAYED_GENERIC_COUNTER
and #$01
beq +

rts

+
;y swing
inc SPRITE_MOVE_POS_Y,x
lda SPRITE_MOVE_POS_Y,x
and #15
sta SPRITE_MOVE_POS_Y,x

ldy SPRITE_MOVE_POS_Y,x
lda PATH_DY,y
beq .NoYMoveNeeded
sta PARAM1
and #$80
beq .MoveDown

<< Content
;move up
lda PARAM1
and #$7f
sta PARAM1

.MoveUp
jsr ObjectMoveUp
dec PARAM1
bne .MoveUp

jmp BossFollowPlayerX

.MoveDown
jsr ObjectMoveDown
dec PARAM1
bne .MoveDown

.NoYMoveNeeded
jmp BossFollowPlayerX

The boss is supposed to move quite alike the first boss horizontally. Time to
make that code piece a sub routine:

!zone BossFollowPlayerX
BOSS3_MOVE_SPEED = 2
.DoGhostMove
;move X times
ldy SPRITE_MOVE_POS,x
sty PARAM4
beq +

lda SPRITE_DIRECTION,x
beq .DoRight
.MoveLoopL
jsr ObjectMoveLeftBlocking
dec PARAM4
bne .MoveLoopL
+
rts

.DoRight
.MoveLoopR
jsr ObjectMoveRightBlocking
dec PARAM4
bne .MoveLoopR
rts

BossFollowPlayerX
inc SPRITE_ANIM_DELAY,x
lda SPRITE_ANIM_DELAY,x

<< Content
cmp #10
beq .DoCheckMove
jmp .DoGhostMove
.DoCheckMove
lda #0
sta SPRITE_ANIM_DELAY,x

txa
and #$01
tay
lda SPRITE_ACTIVE,y
cmp #TYPE_PLAYER_DEAN
beq .FoundPlayer
cmp #TYPE_PLAYER_SAM
beq .FoundPlayer

;check other player


tya
eor #1
tay
lda SPRITE_ACTIVE,y
cmp #TYPE_PLAYER_DEAN
beq .FoundPlayer
cmp #TYPE_PLAYER_SAM
beq .FoundPlayer

;no player to hunt


rts

.FoundPlayer
;player index in y
lda SPRITE_CHAR_POS_X,y
cmp SPRITE_CHAR_POS_X,x
bpl .MoveRight

;move left
lda SPRITE_DIRECTION,x
bne .AlreadyLookingLeft
lda SPRITE_MOVE_POS,x
beq .TurnLNow
dec SPRITE_MOVE_POS,x
bne .CheckXDone

.TurnLNow
;turning now
lda #1
sta SPRITE_DIRECTION,x
lda #SPRITE_BOSS_ARM_L
sta SPRITE_POINTER_BASE,x
jmp .CheckXDone

<< Content
.AlreadyLookingLeft
lda SPRITE_MOVE_POS,x
cmp #BOSS3_MOVE_SPEED
beq .CheckXDone
inc SPRITE_MOVE_POS,x
jmp .CheckXDone

.MoveRight
lda SPRITE_DIRECTION,x
beq .AlreadyLookingRight

lda SPRITE_MOVE_POS,x
beq .TurnRNow
dec SPRITE_MOVE_POS,x
bne .CheckXDone

;turning now
.TurnRNow
lda #0
sta SPRITE_DIRECTION,x
lda #SPRITE_BOSS_ARM_R
sta SPRITE_POINTER_BASE,x
jmp .CheckXDone

.AlreadyLookingRight
lda SPRITE_MOVE_POS,x
cmp #BOSS3_MOVE_SPEED
beq .CheckXDone
inc SPRITE_MOVE_POS,x
jmp .CheckXDone

.CheckXDone
rts

When the boss is hit he should be invulnerable for a short while, remember
that we reserved the MSB in SPRITE_STATE for invincibility.

HitBehaviourBoss3
lda #8
sta SPRITE_HITBACK,x

;make invincible for a short while


lda SPRITE_STATE,x
ora #$80
sta SPRITE_STATE,x
rts

The rest boils down to the usual additions for a new object type. Oh, and the
new boss can only be hit if he shoots downwards! ;-)

<< Content
Have fun!

<< Content

You might also like