Escape From New York dissected by Lasse Öörni
---------------------------------------------
This is my second rant at http://covertbitops.c64.org. Here I take my Crap Game
Compo 1999 entry and try to explain how it works, from start to finish.
Because the game had graphical bugs deliberately included for more crappiness,
I wanted to fix them first before beginning the dissection. Naturally, I don't
want to give examples of buggy coding :)
You can get the fixed versions at
http://covertbitops.c64.org/rants/efix.zip (binary)
http://covertbitops.c64.org/rants/efixsrc.zip (source)
First something general about this game. EFNY is a simple platform game/
shoot'em-up, that features scrolling in one direction (from right to left),
maximum 8 sprites (no multiplexing), bullets done with character graphics and
4 different kind of enemies. All sprite position calculations are done in the
"screen" coordinates, not in "world" coordinates like in Metal Warrior games.
EFNY uses 2 raster interrupts: one at the top of the screen to initialize the
game screen/title picture display, and one at the bottom to initialize the
score panel display and to play music.
Let the dissection of EFNY.S begin! Warning: this is going to be a long rant!
Definitions
-----------
The source code begins with memory location defines. Sprites, background
character set, music data, background map data (how blocks are arranged),
background block data and the title screen bitmap picture.
SPRITES = $2000
CHARS = $3800
MUSIC = $4000
MAP = $4800
BLOCKS = $5000
PICTURE = $6000
These are the raster line Y-positions on which raster interrupts happen.
"Raster0" is the bottom interrupt that displays the score panel and plays music.
It also increments a counter called "rastercount" which is used to stabilize
execution speed of the main program. "Raster1" displays the game screen or
title screen bitmap picture.
RASTER0POS = 242
RASTER1POS = 20
Here are the "actor type" defines. "Actors" are the heart of almost all my
games, basically they are all the objects that move onscreen (player, enemies,
explosions). Sometimes I also define the bullets as actors (Metal Warrior 1 & 3)
but in this case it isn't done. For each actor there is a move routine that
will be called each time the game situation is updated. Actors can transform
from one type to another, for example the motorist turns into an explosion
when he is destroyed. This is what makes the system very flexible.
ACT_NONE = 0
ACT_PLISSKEN = 1
ACT_MAN1 = 2
ACT_MAN2 = 3
ACT_MAN3 = 4
ACT_MOTORIST = 5
ACT_EXPLOSION = 6
Here are the screen mode defines for "raster1" interrupt. There are 3 modes;
game graphics, title bitmap, and text display. More of these later.
DISPGAME = 0
DISPTITLE = 1
DISPTEXT = 2
Joystick bit value defines for each direction and the fire button. These
are read from the $dc00 register (joystick port 2)
JOY_UP = 1
JOY_DOWN = 2
JOY_LEFT = 4
JOY_RIGHT = 8
JOY_FIRE = 16
Next come all zero-page variable definitions. The first is the display mode
for "raster1" interrupt.
dispmode = $02
The next is the X-direction hardware-scrolling (0-7) for the game screen.
scrollx = $03
This is the joystick control data that has been read from $dc00 (With bits
inverted so that a 1 bit means direction/fire button active).
joystick = $04
Previous joystick control value. This is used to check that player has released
fire button or up direction (jumping) before a new shot can be fired or new
jump be done.
prevjoy = $05
A 16-bit memory location that points at the current level's background map data.
mapadrlo = $06
mapadrhi = $07
The position in background map data (measured from its left edge in blocks)
that is used to draw new background graphics to the right edge of screen when
the screen scrolls. Each level is 100 blocks wide (10 whole screens) so this
gets values from 0 to 100.
mapx = $08
Position in the background data, within a block. Each block is 4 chars wide so
this gets the values 0-3.
blockx = $09
Various temp variables, used when needed.
temp1 = $0a
temp2 = $0b
temp3 = $0c
temp4 = $0d
temp5 = $0e
The raster interrupt counter incremented by "raster0", used in stabilizing the
game execution speed.
rastercount = $0f
DASM assembler requires the processor to be defined, so here that is taken
care. Program code starts at memory location $0800 (2048)
processor 6502
org $0800
The main program
----------------
First things to do are to clear the processor's decimal flag (just to be sure),
init raster interrupts ("initraster") and initialize screen colors
("initscreen"). The subroutines are described in more detail when they are
actually encountered in the source code.
efnystart: cld
jsr initraster
jsr initscreen
Here we init music. EFNY's music has been done with the SadoTracker that uses
the "standard" convention for music init/play JSR addresses: MUSIC+0 is the
init routine (subtune number passed in accumulator) and MUSIC+3 is the play
routine to be called each frame. Basically, EFNY uses only one subtune and
subtune numbering starts from 0.
lda #$00
jsr MUSIC
Title screen code starts here. We come back here whenever a game ends. The
thing we check here is whether a new hiscore has been made. Both the score
and hiscore are 3 bytes of binary coded decimals, with 2 digits in each
byte. The compare is started from the most significant byte and if no
conclusion is made, then the next most significant byte is checked.
title: lda score+2
cmp hiscore+2
bcc title_nohiscore
beq title_check2
bcs title_hiscore
title_check2: lda score+1
cmp hiscore+1
bcc title_nohiscore
beq title_check3
bcs title_hiscore
title_check3: lda score
cmp hiscore
bcc title_nohiscore
If a new hiscore was made, copy the "score" contents (3 bytes) to "hiscore".
title_hiscore: lda score
sta hiscore
lda score+1
sta hiscore+1
lda score+2
sta hiscore+2
Display the title picture ("showpic") and update the numbers in the score panel
("drawscores")
title_nohiscore:jsr showpic
jsr drawscores
Title screen waiting loop. Wait for the "rastercount" to increase ("waitras"),
get joystick control value ("getjoystick") and check the fire button bit. Loop
until fire button has been pressed.
titleloop: jsr waitras
jsr getjoystick
lda joystick
and #JOY_FIRE
beq titleloop
Game starts. First thing to do is to clear the score (3 bytes), init number of
lives and the level number.
gamestart: lda #$00
sta score
sta score+1
sta score+2
lda #3
sta lives
lda #1
sta level
Code to move onto the next level. All sprites are turned off ($d015), score
panel is updated and scrolling of the level background graphics is initialized
("initscroll")
initlevel: lda #$00
sta $d015
jsr drawscores
jsr initscroll
At the end of a level a "kill" meter activates. Here it is deactivated at first
by storing 0 to the variable "killactive", as well as clearing the meter itself
(consisting of the meter length in chars "killmeter" and the "fine" component
of the meter "killmeterd")
lda #0
sta killactive
sta killmeter
sta killmeterd
Next we scroll the first screen "in" from the right edge, as seen in the
beginning of each level. The scrolling speed (4 pixels) is given to "doscroll"
routine in the accumulator. We loop until the map X-position (in blocks) has
reached 10, which means that the entire screen has been scrolled in.
initlevel2: jsr waitras
lda #4
jsr doscroll
lda mapx
cmp #10
bcs initlevel3
jmp initlevel2
The next thing to do is to set all the 8 "actors" to inactive state, which is
done by setting the type of each actor to the inactive (zero) value. The
actor type is stored in an indexed array called "actt", with the index going
from 0 to 7. All other actor properties are stored in similarly indexed arrays
as well. Note: do not confuse actor types and indexes! The player is actor
index 0 and enemies are indexes 1-7, but they can be of any type.
initlevel3: ldx #7
initlevel4: lda #ACT_NONE
sta actt,x
dex
bpl initlevel4
There's a similar array for the bullets, with "bullett" (bullet type)
indicating if a bullet is active or not. Bullets are character graphics, so
there's no limit for their amount, but 16 seems like a sensible value.
ldx #15
initlevel5: lda #$00
sta bullett,x
dex
bpl initlevel5
Program execution goes back here also whenever a life has been lost. Here
the time counter ("time", binary coded decimal number) is reset to the
maximum 99. Also the time decrement delay counter ("timedl") is reset.
initlife: lda #$99
sta time
lda #$00
sta timedl
The first actor index (index 0) is always the player. So player's position
is reset here. An actor has its X-position stored as a 16-bit value ("actxl"
and "actxh" and the Y-position ("acty") as 8 bits. The origin of this
coordinate system is the first visible pixel in the top left corner of the
screen. Note: The actor position corresponds to the Y-position of the actor's
feet and the center in X-direction.
lda #128
sta actxl
lda #0
sta actxh
lda #20*8
sta acty
Here a number of other actor properties are reset. "Actd" is the actor
direction, 0 is facing right, 1 is facing left. "Actf" is the frame number
(animation). "Actj" indicates whether an actor is jumping (1 = is jumping)
"Actsx" and "actsy" are the X and Y speeds of an actor. "Actyd" is a delay
counter for slowly changing the Y-speed of an actor, for acceleration due
to gravity.
lda #0
sta actd
sta actf
sta actj
sta actsx
sta actsy
sta actyd
This is the player's firing delay counter.
sta firedelay
Initialize player actor type and give an immortality time of 200 game frames-
("actimm") Player has only 1 hitpoint ("acthp") so any hit kills the player.
(enemies have more)
lda #ACT_PLISSKEN
sta actt
lda #200
sta actimm
lda #1
sta acthp
Here begins the game main loop. First we get the joystick controls.
gameloop: jsr getjoystick
Handle movement of all actors ("moveactors")
jsr moveactors
Handle player shooting ("plrshoot")
jsr plrshoot
Generate new enemies at the edges of the screen ("spawnenemies")
jsr spawnenemies
Wait for the raster interrupt
jsr waitras
Erase bullet characters from the screen ("erasebullets")
jsr erasebullets
Move bullets ("movebullets")
jsr movebullets
Update score panel.
jsr drawscores
Activate and display the kill meter at the end of levels ("killmeterr")
jsr killmeterr
Wait for the raster interrupt again, to get as much rastertime as possible
for the screen scrolling (takes quite a lot of time when the screen memory
has to be shifted to the left)
jsr waitras
Check for need of scrolling (player moved far enough to the right?)
"Checkscroll" returns the scrolling speed in accumulator, or 0 if no scrolling
required.
jsr checkscroll
Here the screen is scrolled, with speed indicated by accumulator.
jsr doscroll
Draw all actors ("drawactors")
jsr drawactors
Draw all bullet characters ("drawbullets")
jsr drawbullets
Decrement time counter ("dectime")
jsr dectime
Check player actor death ("checkdeath"). If player actor has died, this routine
does not return but pulls the return address from stack and jumps to the
location "initlife", if lives remain, or to "title" when game is over.
jsr checkdeath
Checks completion of level ("checklevelend"). This routine also does not return
if a level is completed.
jsr checklevelend
Go back to the beginning of the gameloop.
jmp gameloop
The rest of the code consists of the subroutines.
"Checklevelend" subroutine
--------------------------
If the kill meter is active (nonzero value), compare the meter value to the
limit required by each level.
checklevelend: lda killactive
beq cle2
lda killmeter
cmp killlimit
bcc cle2
pla
pla
Counting time bonus at the end of the level. By using binary coded decimal
arithmetic, increase score by 100 points until "time" is zero.
countbonus: lda time
beq countbonus2
sed
lda time
sec
sbc #$01
sta time
lda score+1
clc
adc #$01
sta score+1
bcc noextra
Give an extra life each ten thousand points (when the most significant byte of
the score increments).
inc lives
noextra: lda score+2
adc #$00
sta score+2
cld
Update the score panel and perform some delaying.
jsr drawscores
jsr waitras
jsr waitras
jsr waitras
jsr waitras
jmp countbonus
Move onto the next level, or if we were on level 3, do the endsequence.
countbonus2: lda level
cmp #$03
beq complete
inc level
jmp initlevel
Level not completed: go back to main loop
cle2: rts
Endsequence code. Initialize scrolling again, set the "text mode" to be
displayed and clear sprites at first.
complete: jsr initscroll
lda #DISPTEXT
sta dispmode
ldx #0
stx $d015
Loop to display the ending text (4 rows) on the screen.
complete1: lda ctext1,x
and #$3f
sta $400+4*40+8,x
lda ctext2,x
and #$3f
sta $400+6*40+8,x
lda ctext3,x
and #$3f
sta $400+8*40+8,x
lda ctext4,x
and #$3f
sta $400+10*40+8,x
lda #$01
sta $d800+4*40+8,x
sta $d800+6*40+8,x
sta $d800+8*40+8,x
sta $d800+10*40+8,x
inx
cpx #24
bne complete1
Use temp1 as a delay counter to wait for a "long" time and then return to
title screen.
lda #255
sta temp1
complete2: jsr waitras
jsr waitras
dec temp1
bne complete2
jmp title
ctext1: dc.b " WELL DONE SNAKE "
ctext2: dc.b "YOUR MISSION IS COMPLETE"
ctext3: dc.b "AND YOU HAVE EARNED YOUR"
ctext4: dc.b " FREEDOM. "
"Killmeterr" subroutine
-----------------------
First check if kill meter is yet inactive and we have arrived at the end of the
level (X-position in blocks 100), in which case it must be activated.
killmeterr: lda killactive
bne killmeter2
lda mapx
cmp #100
bcc killmeter2
inc killactive
sta killactive
Draw the word "KILL" on the screen :)
lda #96
sta $400+42
lda #97
sta $400+43
lda #98
sta $400+44
lda #99
sta $400+45
Get the kill meter limit corresponding to the current level.
ldx level
dex
lda killlimittbl,x
sta killlimit
tax
Draw the empty kill meter bar on screen.
drawkillloop: lda #100
sta $400+45,x
dex
bne drawkillloop
killmeter2: rts
killlimittbl: dc.b 8,12,24
"Dectime" subroutine
--------------------
Decrement the time counter by using decimal mode arithmetic each time the
"timedl" variable has counted 50 game frames.
dectime: lda time
beq nodectime
inc timedl
lda timedl
cmp #50
bcc nodectime
lda #$00
sta timedl
sed
lda time
sec
sbc #$01
sta time
cld
bne nodectime
If time has run out, kill the player actor by setting its hitpoints to zero.
lda #0
sta acthp
nodectime: rts
"Checkdeath" subroutine
-----------------------
Frame number 7 of the player actor is displayed when he's dead, so first check
for that.
checkdeath: lda actf
cmp #7
bne cdeath_not
Then check if the dead player actor has reached the bottom of the screen.
lda acty
cmp #240
bcc cdeath_not
If so, pull the return address from the stack and do not return with RTS;
instead decrement lives and jump back to the "initlife" code if lives still
remain, or to "title" when player is out of lives.
pla
pla
dec lives
lda lives
beq cdeath_gameover
jmp initlife
cdeath_gameover:jmp title
cdeath_not: rts
"Plrshoot" subroutine
---------------------
Handle player's shooting attack. First check if the "firedelay" counter is
zero, which means that firing the next bullet is allowed. If it's nonzero,
decrement it and return.
plrshoot: lda firedelay
beq plrshootok
dec firedelay
rts
Check for the death frame of player actor. Don't allow firing when dead.
plrshootok: lda actf
cmp #7
bne plrshootok2
plrshootnot: rts
Check that fire button is pressed, and the fire button has been released
previously (a semi-automatic weapon :)).
plrshootok2: lda joystick
and #JOY_FIRE
beq plrshootnot
lda prevjoy
and #JOY_FIRE
bne plrshootnot
Then start searching for an unused bullet index (zero value in the "bullett"
array). Bullet indexes 0-7 are reserved for player bullets. If no unused
bullet is found, return.
ldx #7
plrshootfind: lda bullett,x
beq plrshootfound
dex
bpl plrshootfind
rts
Copy player location & direction to bullet location & direction ("bulletxl",
"bulletxh", "bullety" and "bulletd"). Y-coord is modified to make the bullets
appear at the height of the player's weapon (there's a table for this,
"actshootymod" (Y-modification) of which we use the first value, which
corresponds to the player actor type, ACT_PLISSKEN)
plrshootfound: lda actxl
sta bulletxl,x
lda actxh
sta bulletxh,x
lda acty
sec
sbc actshootymod
sta bullety,x
lda actd
sta bulletd,x
Set the bullet to active state and set the firing delay.
lda #1
sta bullett,x
lda #2
sta firedelay
rts
"Plr": Player actor move routine
--------------------------------
Called by "moveactors" routine, X register contains now the current actor
index.
Assume that the player is in standing position; modify the values in the
actortype's Y-size ("actsizey") and shooting Y-modification tables.
plr: lda #42
sta actsizey
lda #20
sta actshootymod
Check for hitpoints running out.
lda acthp,x
bne plr_nodeath
If player actor is already in the "death" frame, do not re-init the death
sequence.
lda actf,x
cmp #7
beq plr_nodinit
Set the "death" frame and give upwards Y-speed. Reset Y-speed increment
(gravity) delay counter.
lda #7
sta actf,x
lda #-5
sta actsy,x
lda #0
sta actyd,x
Gravity handling for the dead actor. After the Y-speed delay counter has
increased to 3, increase the Y-speed.
plr_nodinit: inc actyd,x
lda actyd,x
cmp #3
bcc plr_d2
lda #$00
sta actyd,x
inc actsy,x
After performing the acceleration, move the player actor in Y-direction.
("moveactory") The actor index parameter that is required is already in the
X-register.
plr_d2: jsr moveactory
rts
Player actor death code ends here, now we check if the actor is jumping.
("actj" has nonzero value when jumping)
plr_nodeath: lda actj,x
beq plr_nofly
Set the frame 5 (jumping frame). Do a similar gravity acceleration &
actor Y-movement like above, but with a larger delay value.
plr_fly: lda #5
sta actf,x
inc actyd,x
lda actyd,x
cmp #5
bcc plr_fly2
lda #$00
sta actyd,x
inc actsy,x
Before the Y-movement, perform X-movement by a subroutine ("plisskenmovex").
plr_fly2: jsr plisskenmovex
jsr moveactory
Now check the Y-speed. If Y-speed is positive & greater than zero, it is
possible for the player to land on a platform (ends the jump).
lda actsy,x
beq plr_noground
bmi plr_noground
Check for ground below feet ("checkground"). This subroutine returns carry 0
if there is ground.
jsr checkground
bcs plr_noground
Player landed on ground. Reset Y-speed, jumping indicator and align Y-position
on a character boundary with the and operation.
lda #0
sta actsy,x
sta actj,x
lda acty,x
and #$f8
sta acty,x
plr_noground: rts
"Player not jumping"-code. If player has moved into a location where there's no
ground under feet, initiate falling (jumping without initial upwards Y-speed)
plr_nofly: jsr checkground
bcc plr_nofall
Set jumping indicator nonzero.
lda #1
sta actj,x
Initial Y-speed is 0, it will start to increase (downwards speed)
lda #0
sta actsy,x
Reset Y-acceleration delay counter.
sta actyd,x
jmp plr_fly
If player not falling, check for various joystick movements. First comes the
check of "joystick up" to initiate a new jump. Up must not have been previously
pressed.
plr_nofall: lda joystick
and #JOY_UP
beq plr_nojump
lda prevjoy
and #JOY_UP
bne plr_nojump
lda #1
sta actj,x
lda #0
sta actyd,x
Give the initial upwards (negative) Y-speed.
lda #-4
sta actsy,x
jmp plr_fly
plr_nojump: lda #0
sta actsx,x
Check for moving left. If moving left, set player facing left ("actd" 1),
give X-speed of -2 pixels (left) and jump to the walk animation code.
lda joystick
and #JOY_LEFT
beq plr_notleft
lda #1
sta actd,x
lda #-2
sta actsx,x
jmp plr_walkanim
Similar code for moving right.
plr_notleft: lda joystick
and #JOY_RIGHT
beq plr_notright
lda #0
sta actd,x
lda #2
sta actsx,x
jmp plr_walkanim
If not either left or right, set standing frame (frame 0).
plr_notright: lda #0
sta actf,x
jmp plr_domove
Walk animation. Increase animation frame delay counter, and when it has
counted to five, increase frame. And-operation is used to limit the
frame between 0-3, and 1 is then added (so the final frame range for
walking animation is 1-4).
plr_walkanim: inc actfd,x
lda actfd,x
cmp #5
bcc plr_walkanim2
lda #$00
sta actfd,x
lda actf,x
and #$03
clc
adc #$01
sta actf,x
plr_walkanim2:
Finally check for down direction (crouching).
plr_domove: lda joystick
and #JOY_DOWN
beq plr_noduck
Set animation frame to 6 (crouching) and make the player's Y-size now smaller
(for correct collision detection) and modify the shooting Y-modification as
well, as the weapon is held lower now.
lda #6
sta actf,x
lda #18
sta actsizey
lda #10
sta actshootymod
rts
plr_noduck: jsr plisskenmovex
rts
This is a subroutine for player actor's movement in X-direction. It allows
movement left (by calling the "moveactorx" subroutine) only if the actor's
X-coordinate is greater than 10, and movement right only if the X-coordinate
is less than 310, so the player actor isn't allowed to move outside the visible
screen.
plisskenmovex: lda actsx,x
bmi plr_moveleft
lda actxh,x
beq plr_moveok
lda actxl,x
cmp #(310-256)
bcc plr_moveok
plr_nomove: rts
plr_moveok: jsr moveactorx
rts
plr_moveleft: lda actxh,x
bne plr_moveok
lda actxl,x
cmp #11
bcs plr_moveok
rts
"Man": Enemy man actor move routine
-----------------------------------
This code is very similar to the "plr" move routine, so it isn't explained in
as much detail. Like before, X register contains the current actor index.
Check for enemy becoming dead.
man: lda acthp,x
bne man_notdead
For these kind of enemies, 3 is the death animation frame. If frame wasn't
already 3, init death animation & movement (similar upward Y-speed like
with the player) and increase player score.
lda actf,x
cmp #3
beq man_nodinit
jsr addenemyscore
lda #3
sta actf,x
lda #-5
sta actsy,x
lda #0
sta actyd,x
man_nodinit: inc actyd,x
lda actyd,x
cmp #2
bcc man_d2
lda #$00
sta actyd,x
inc actsy,x
man_d2: jsr moveactory
lda acty,x
cmp #240
bcc man_noremove
The enemy is removed (by setting the actor type to zero) when it moves off the
bottom of screen after death.
lda #0
sta actt,x
man_noremove: rts
man_notdead: lda actj,x
beq man_nofly
Enemy jumping code. 2 is the jumping animation frame for these enemies.
man_fly: lda #2
sta actf,x
inc actyd,x
lda actyd,x
cmp #5
bcc man_fly2
lda #$00
sta actyd,x
inc actsy,x
man_fly2: jsr moveactorx
jsr moveactory
lda actsy,x
beq man_noground
bmi man_noground
jsr checkground
bcs man_noground
lda #0
sta actsy,x
sta actj,x
lda acty,x
and #$f8
sta acty,x
man_noground: jmp man_shoot
rts
Enemy walking code. If an enemy has moved to a location where it doesn't have
ground under its feet, it jumps (this is a bit of cheating, since the player
actor would fall in a similar situation)
man_nofly: jsr checkground
bcc man_nojump
lda #1
sta actj,x
lda #-4
sta actsy,x
sta actyd,x
jmp man_fly
man_nojump: lda actd,x
beq man_right
The enemy's move speed depends on its type (there are 3 kinds of these
enemies). Actually the enemy type's maximum hitpoints are used as the move
speed.
ldy actt,x
dey
lda actmaxhp,y
Here the speed needs to be negated by two's complement for moving left.
eor #$ff
clc
adc #$01
sta actsx,x
jmp man_walkanim
man_right: ldy actt,x
dey
lda actmaxhp,y
sta actsx,x
man_walkanim: inc actfd,x
lda actfd,x
cmp #6
bcc man_walkanim2
lda #0
sta actfd,x
The enemy actor uses only frames 0 & 1 for walking animation, so it looks
quite primitive :)
lda actf,x
eor #$01
and #$01
sta actf,x
man_walkanim2: jsr moveactorx
Enemy shooting routine. Call "random" subroutine to get a pseudorandom number
for shooting decision.
man_shoot: jsr random
sta temp1
Compare the level number against the random value. If it's smaller then do not
shoot. This has the effect that enemies start to shoot more frequently in later
levels.
lda level
cmp temp1
bcc man_noshoot
Now search for a free enemy bullet (nonzero value in "bullett" array), using
the Y register as an index. Bullet indexes 8-15 are reserved for enemies.
ldy #8
manshootfind: lda bullett,y
beq manshootfound
iny
cpy #$10
bcc manshootfind
No bullet found, just exit
man_noshoot: rts
Copy the actor location & direction to bullet location & direction, and perform
Y-position modification, like in the player actor's shoot routine.
manshootfound: lda actxl,x
sta bulletxl,y
lda actxh,x
sta bulletxh,y
lda acty,x
sec
sbc #20
sta bullety,y
lda actd,x
sta bulletd,y
lda #1
sta bullett,y
rts
"Mc": Enemy motorist actor move routine
---------------------------------------
When the motorist is killed, he turns into an explosion.
mc: lda acthp,x
bne mc_notdead
jsr addenemyscore
Initialize animation frame for the explosion, and change actor type.
lda #0
sta actf,x
lda #ACT_EXPLOSION
sta actt,x
rts
Rest of the code is very similar to the player & enemy move routines seen
before.
mc_notdead: lda actj,x
beq mc_nofly
mc_fly: lda #0
sta actf,x
inc actyd,x
lda actyd,x
cmp #5
bcc mc_fly2
lda #$00
sta actyd,x
inc actsy,x
mc_fly2: jsr moveactorx
jsr moveactory
lda actsy,x
beq mc_noground
bmi mc_noground
jsr checkground
bcs mc_noground
lda #0
sta actsy,x
sta actj,x
lda acty,x
and #$f8
sta acty,x
mc_noground: rts
mc_nofly: jsr checkground
bcc mc_nojump
lda #1
sta actj,x
lda #-3
sta actsy,x
sta actyd,x
jmp mc_fly
mc_nojump: lda actd,x
beq mc_right
lda #-4
sta actsx,x
jmp mc_walkanim
mc_right: lda #4
sta actsx,x
mc_walkanim: lda actf,x
eor #$01
and #$01
sta actf,x
jsr moveactorx
Here is the difference: The motorist can kill the player actor by colliding.
Check collision between the enemy actor (index in X register) and player
(Y register loaded with 0, player actor's index) with the "actactcoll"
(actor-actor collision) subroutine.
ldy #0
jsr actactcoll
bcc mc_nocoll
Carry 1 indicates collision. Decrease player actor hitpoints (player actor has
only one so he's killed)
lda acthp,y
sec
sbc #$01
sta acthp,y
mc_nocoll: rts
"Expl": Explosion actor move routine
------------------------------------
This is a simple "move" routine because it only involves animation. Count
the frame delay up to 6 and after that increase the animation frame. The
explosion has 4 animation frames, after all these have been shown the
explosion is removed by putting zero value to actor type ("actt").
expl: inc actfd,x
lda actfd,x
cmp #6
bcc expl2
lda #0
sta actfd,x
inc actf,x
lda actf,x
cmp #4
bcc expl2
lda #0
sta actt,x
expl2: rts
"Addenemyscore" subroutine
--------------------------
Add score according to the actor type of the dead enemy (use a value from a
table), using decimal arithmetic.
addenemyscore: ldy actt,x
dey
sed
lda score
clc
adc actscorelo,y
sta score
lda score+1
adc actscorehi,y
sta score+1
bcc noextra2
When tens of thousands increase, give an extra life.
inc lives
noextra2: lda score+2
adc #$00
sta score+2
cld
If the kill meter is active, lenghten the kill meter bar on screen. "Killmeterd"
goes through values 0-4 to specify what char is drawn at the end of the meter,
to make it increase smoothly. "Killmeter" is the meter length in whole chars
(indicates the position of the meter's endpoint on screen). The chars used for
the meter are 100-104.
lda killactive
beq addenemyscore2
inc killmeterd
ldy killmeter
lda #100
clc
adc killmeterd
sta $400+46,y
lda killmeterd
cmp #4
bcc addenemyscore2
lda #$00
sta killmeterd
inc killmeter
addenemyscore2: rts
"Moveactorx" subroutine
-----------------------
Moves an actor (index indicated by the X-register) horizontally, according
to its speed ("actsx" array). "Actsx" is only 8 bits so to perform the 16-bit
position addition correctly we must check its sign before adding.
moveactorx: clc
lda actsx,x
bmi negmovex
adc actxl,x
sta actxl,x
lda actxh,x
adc #$00
sta actxh,x
rts
negmovex: adc actxl,x
sta actxl,x
lda actxh,x
adc #$ff
sta actxh,x
rts
"Moveactory" subroutine
-----------------------
Moves an actor (index indicated by the X-register) vertically, according to its
speed ("actsy" array). Y-position is only 8 bits like the speed so this
movement is easy to do.
moveactory: lda actsy,x
clc
adc acty,x
sta acty,x
rts
"Checkground" subroutine
------------------------
Checks for ground under the actor's (index indicated by X-register) feet. First
divide the actor's Y-position by 8 to get the screen row where we must check.
checkground: lda acty,x
lsr
lsr
lsr
If it's over the gamescreen portion of the screen, limit the row number.
cmp #24
bcc cg_notover1
lda #23
Fetch the screen row's memory address from a table.
cg_notover1: tay
lda rowtbllo,y
sta temp1
lda rowtblhi,y
sta temp2
Now divide the X-position by 8 to get the char column we need.
lda actxh,x
sta temp3
lda actxl,x
lsr temp3
ror
lsr temp3
ror
lsr temp3
ror
Move the column to Y-register, limit it to the visible screen boundaries (0-39)
if it's less or greater.
tay
bpl cg_notoverleft
ldy #0
jmp cg_notoverright
cg_notoverleft: cpy #39
bcc cg_notoverright
ldy #39
Then read the char from that location at screen memory. Characters 0-31 can be
walked on, so carry will be 0 in that case, otherwise 1.
cg_notoverright:lda (temp1),y
cmp #32
rts
This is the table of memory addresses of the 24 gamescreen rows.
rowtbllo:
N SET 0
REPEAT 24
dc.b #<($400+N*40)
N SET N+1
REPEND
rowtblhi:
N SET 0
REPEAT 24
dc.b #>($400+N*40)
N SET N+1
REPEND
"Random" subroutine
-------------------
Pseudorandom generator. Reads a byte from a certain memory range ($0b00-$bff,
the program code), then adds the previous random number and $d012 (raster line
Y-position) to it, returning the new random number in the accumulator. Code is
selfmodified to get the next byte to read on the next execution of this
subroutine.
random: lda $b00
adc randseed
adc $d012
sta randseed
inc random+1
rts
randseed: dc.b $73
"Bullactcoll" subroutine
------------------------
Bullet-actor collision. Parameters and return value (carry flag) are shown
below.
;X = bullet number (bullet must exist!)
;Y = actor number
;C=1 collision happened
The collision routines are all based on coordinate range checking. The bullets
use the same coordinate system as the actors.
Check that the actor exists and it doesn't have immortality time left.
bullactcoll: lda actt,y
beq bsc_nocoll
lda actimm,y
bne bsc_nocoll
Also, if an actor doesn't have hitpoints left, it doesn't participate in the
collision checking.
lda acthp,y
bne bsc1
bsc_nocoll: clc
rts
Get the actor X & Y size from a table, based on the actor type (modify the
CMP instructions directly). Note that registers have to be saved to temporary
locations.
bsc1: sty temp1
lda actt,y
tay
dey
lda actsizex,y
sta bsc_xcmp+1
lda actsizey,y
sta bsc_suby+1
Compare Y coordinate ranges first. The bullet is considered a point-like
object. If bullet is outside the actor's Y-size range, no collision has
happened.
ldy temp1
lda acty,y ;Check against bottom of actor
cmp bullety,x
bcc bsc_nocoll
sec
bsc_suby: sbc #$00 ;Check against top of actor
cmp bullety,x
bcs bsc_nocoll
Then compare X-coordinates. Get X distance between bullet & actor by
subtraction, and its absolute value (negate it if it's negative)
lda actxl,y ;Get X distance between bullet&actor
sec
sbc bulletxl,x
sta temp1
lda actxh,y
sbc bulletxh,x
sta temp2
bpl bsc_posofs
eor #$ff
sta temp2
lda temp1
eor #$ff
clc
adc #$01
sta temp1
lda temp2
adc #$00
sta temp2
Then check that bullet is not farther away than the actor's X size, or
collision hasn't happened.
bsc_posofs: lda temp2 ;X distance must not be
bne bsc_nocoll ;greater than X size
lda temp1
bsc_xcmp: cmp #$00
bcs bsc_nocoll
sec
rts
"Actactcoll" subroutine
-----------------------
Actor-actor collision. This is similar to the subroutine above, but both
actors have a size, and that must be taken into account in the coordinate
comparisions.
;X = actor number (must exist)
;Y = actor number
;C=1 collision happened
Check for actor existing, being not immortal and having hitpoints, like
before.
actactcoll: lda actt,y
beq ssc_nocoll
lda actimm,y
bne ssc_nocoll
lda acthp,y
bne ssc1
ssc_nocoll: clc
rts
Get the X & Y sizes of actors. Again, register saving to temporary locations
must happen, because of the need to use many table indexes.
ssc1: sty temp1
stx temp2
lda actt,y
tay
dey
lda actt,x
tax
dex
lda actsizex,y
clc
adc actsizex,x
sta ssc_xcmp+1
lda actsizey,y
sta ssc_ycmp+1
ldy temp1
ldx temp2
lda acty,y ;Check against bottom of actor
sec
sbc acty,x
bpl ssc_ypos
eor #$ff
clc
adc #$01
ssc_ypos:
ssc_ycmp: cmp #$00
bcs ssc_nocoll
lda actxl,y ;Get X distance between actors
sec
sbc actxl,x
sta temp1
lda actxh,y
sbc actxh,x
sta temp2
bpl ssc_posofs
eor #$ff
sta temp2
lda temp1
eor #$ff
clc
adc #$01
sta temp1
lda temp2
adc #$00
sta temp2
ssc_posofs: lda temp2 ;X distance must not be
bne ssc_nocoll ;greater than X size
lda temp1
ssc_xcmp: cmp #$00
bcs ssc_nocoll
sec
rts
"Spawnenemies" subroutine
-------------------------
Creates enemy actors to the left & right borders of the screen. Enemy appearance
frequency and enemy actor types that will be spawned depends on the level we're
on.
Multiply level number by 16 to get an index to the spawn table.
spawnenemies: lda level
sec
sbc #$01
asl
asl
asl
asl
sta spawnadd+1
Decision for spawning, if random number is in the range $10-$3f then don't
spawn.
jsr random
and #$3f
cmp #$10
bcc spawnok1
rts
spawnok1: clc
Add the random number to the spawn table index, to get the final position
in the table.
spawnadd: adc #$00
tax
Get enemy actor type from the spawn table. If it's 0, don't spawn.
lda levelspawntable,x
bne spawnok2
rts
Search for a free actor (actor type zero) in the actor index range 1-7 (enemies).
spawnok2: ldx #1
spawnsearch: ldy actt,x
beq spawnfound
inx
cpx #7
bcc spawnsearch
rts
spawnfound: sta actt,x
Enemy actor index is now in the X-register. Decision whether the enemy appears
on left or right. Give the corresponding X-coordinate to the enemy.
jsr random
and #$01
tay
sta actd,x
lda spawnxlo,y
sta actxl,x
lda spawnxhi,y
sta actxh,x
Random decision for the Y position of the actor.
jsr random
and #$03
clc
adc #$02
Multiply Y-coordinate by 32; enemies' initial Y-coordinates are aligned to the
blocks (4 chars high) on screen.
asl
asl
asl
asl
asl
sta acty,x
If there is no ground under the feet of an enemy, it will not be spawned. In
that case, simply zero the actor type and exit the routine.
jsr checkground
bcc spawnground
lda #0
sta actt,x
rts
Reset jumping, speed, animation frame, frame delay, immortality.
spawnground: lda #0
sta actj,x
sta actsx,x
sta actsy,x
sta actf,x
sta actfd,x
sta actyd,x
sta actimm,x
Give the initial hitpoints according to enemy actor type. (from a table)
ldy actt,x
dey
lda actmaxhp,y
sta acthp,x
rts
The types of enemies that will be spawned in each level.
;level1
levelspawntable:dc.b 0,2,0,2,0,3,0,2,0,2,0,3,0,2,0,2
;level2
dc.b 0,2,2,3,0,2,3,4,0,3,4,0,2,3,2,5
;level3
dc.b 2,2,3,3,2,3,4,4,3,3,4,4,3,2,5,5
Initial X-coordinates are either 0 or 320
spawnxlo: dc.b 0,<320
spawnxhi: dc.b 0,>320
"Checkscroll" subroutine
------------------------
Checks if the player actor's X-position is on the right side of the screen and
gives the scrolling speed in the accumulators (zero if no scrolling.)
checkscroll: ldx #$00
lda actxh
cmp #1
bcs needscroll
lda actxl
cmp #200
bcc noneedscroll
needscroll: ldx #$02
noneedscroll: txa
rts
"Drawbullets" subroutine
------------------------
Saves the chars that are underneath the bullet positions and draws the bullet
chars. There are 16 bullets (possibly) to draw.
The bullet index (X-register) will go from last to first.
drawbullets: ldx #15
Check if bullet is active. Skip if not.
dbloop: lda bullett,x
beq dbnext
Divide bullet Y-pos by 8 to get character row number.
lda bullety,x
lsr
lsr
lsr
If outside the visible gamescreen, skip.
cmp #23
bcs dbnext
Get the corresponding screen memory row address.
tay
lda rowtbllo,y
sta temp1
lda rowtblhi,y
sta temp2
Divide bullet X-pos by 8 to get column number.
lda bulletxh,x
sta temp3
lda bulletxl,x
lsr temp3
ror
lsr temp3
ror
lsr temp3
ror
If outside the visible gamescreen, skip.
cmp #40
bcs dbnext
Add the column to the row address to get the final memory location. Store
also the memory location to the bullet array for fast retrieval later.
clc
adc temp1
sta temp1
sta bulletlo,x
lda temp2
adc #$00
sta temp2
sta bullethi,x
ldy #0
Save the char under the bullet position and draw the bullet (char number 255)
lda (temp1),y
sta bulletunder,x
lda #255
sta (temp1),y
Set bullet type to 2 to indicate the bullet has been drawn on screen.
lda #2
sta bullett,x
Loop until all bullets done.
dbnext: dex
bpl dbloop
rts
"Erasebullets" subroutine
-------------------------
Restores the chars that were overwritten by bullets on screen. To ensure
correct restore, the index must go in reverse direction (from first to last)
than in the "drawbullets" routine.
Start from bullet index 0 (in X-register)
erasebullets: ldx #0
Skip if the bullet hasn't been drawn
ebloop: lda bullett,x
cmp #2
bne ebnext
ldy #0
The screen memory position has already been calculated.
lda bulletlo,x
sta temp1
lda bullethi,x
sta temp2
Restore the char that was under the bullet.
lda bulletunder,x
sta (temp1),y
Reset the bullet type to 1 to indicate it has been erased from the screen
lda #1
sta bullett,x
Loop until all 16 bullets have been checked.
ebnext: inx
cpx #16
bcc ebloop
rts
"Movebullets" subroutine
------------------------
Moves the bullets, if they exist, and checks their collisions to player
& enemies.
Start from the last bullet (X-register as index).
movebullets: ldx #15
Does the bullet exist?
mbloop: lda bullett,x
beq mbnext
Is it a player or enemy bullet?
cpx #8
bcs checkenemybull
It's a player bullet, loop through the enemy actor indexes 1-7 to check
collisions.
ldy #1
checkplayerbull:
jsr bullactcoll
bcc cpb_nocoll
If a collision happened, reduce the enemy's hitpoints by one and remove the
bullet that collided.
lda acthp,y
sec
sbc #$01
sta acthp,y
lda #$00
sta bullett,x
jmp mbnext
cpb_nocoll: iny
cpy #8
bcc checkplayerbull
jmp bullcheckdone
It's an enemy bullet, check collision to player actor and reduce player actor's
hitpoints (kill player actor!) + remove bullet if collided
checkenemybull: ldy #0
jsr bullactcoll
bcc bullcheckdone
lda acthp,y
sec
sbc #$01
sta acthp,y
lda #$00
sta bullett,x
jmp mbnext
Collision checking has been done, and the bullet wasn't removed. Next, move
the bullet. Check direction ("bulletd" array); move the bullet 8 pixels either
left or right depending on that.
bullcheckdone: lda bulletd,x
bne mbleft
Movement right.
mbright: lda bulletxl,x
clc
adc #8
sta bulletxl,x
lda bulletxh,x
adc #0
sta bulletxh,x
beq mbnext
If the bullet goes outside the screen, remove it.
lda bulletxl,x
cmp #(320-256)
bcc mbnext
mberase: lda #0
sta bullett,x
jmp mbnext
Movement left.
mbleft: lda bulletxl,x
sec
sbc #8
sta bulletxl,x
lda bulletxh,x
sbc #0
sta bulletxh,x
If the bullet goes outside the screen, remove it.
bmi mberase
Loop until all bullets have been moved.
mbnext: dex
bpl mbloop
rts
"Moveactors" subroutine
-----------------------
Calls the move routine of each actor (0-7) and decreases their immortality
counter (used only for the player). Also, for enemy actors (1-7) a check
is made to see if they have gone are outside the screen; in this case they
are removed.
Loop from last actor to first, with X register as index.
moveactors: ldx #7
Does the actor exist?
mactloop: lda actt,x
beq mactnext
If it has immortality left, decrease the immortality counter.
lda actimm,x
beq mact_noimm
dec actimm,x
If it's the player, skip the removal check.
mact_noimm: cpx #0
beq mact_noremove
lda actxh,x
beq mact_noremove
bmi mact_rleft
Check for X-coordinates greater/equal to 330 or less than -10, and remove
the actor in that case.
mact_rright: lda actxl,x
cmp #<(330)
bcc mact_noremove
lda #0
sta actt,x
jmp mactnext
mact_rleft: lda actxl,x
cmp #(256-10)
bcs mact_noremove
lda #0
sta actt,x
jmp mactnext
Make sure X is preserved for the next actor (although no actor move routine
should modify the X register)
mact_noremove: stx mact_restx+1
Get the move routine JSR address corresponding to the actor type's move
routine and call the move routine.
lda actt,x
tay
dey
lda actroutlo,y
sta mactjsr+1
lda actrouthi,y
sta mactjsr+2
mactjsr: jsr $0000
Go to next actor, loop until all have been done.
mact_restx: ldx #$00
mactnext: dex
bpl mactloop
rts
"Drawactors" subroutine
-----------------------
Transforms the actors' position and animation frame into actual sprite data
put to the sprite registers.
Init a few "virtual" sprite registers for the X-coordinate MSB, sprite on
bits and X & Y expansion. This is to prevent flicker.
drawactors: lda #$00
sta virtd010
sta virtd015
sta virtd017
sta virtd01d
Loop through all actors (X is the index).
ldx #7
Check that the actor exists, and move its type to the Y register, for use in
actortype properties (color, expansion etc.) table lookups.
dactloop: lda actt,x
bne dactok
jmp dactnext
dactok: tay
dey
If the actor is immortal, it flashes at the rate given by the 3th bit of the
immortality counter. When that bit is on, don't draw the actor.
lda actimm,x
and #$08
beq dact_noflash
jmp dactnext
Set the corresponding $d015 bit (sprite is on)
dact_noflash: lda virtd015
ora bittable,x
sta virtd015
Get the actor X-coordinate and subtract the actor's hotspot (X-center).
lda actxl,x
sec
sbc acthotx,y
sta temp1
lda actxh,x
sbc #$00
sta temp2
Do same for Y-coordinate.
lda acty,x
sec
sbc acthoty,y
sta temp3
Because the actor coordinate system's origin was (0,0) but the top-left edge is
(24,50) for the actual sprite coordinates, add those values.
lda temp3
clc
adc #50
sta temp3
lda temp1
clc
adc #24
sta temp1
lda temp2
adc #0
sta temp2
Get actor's "base frame" depending on its direction.
lda actd,x
beq dact_right
dact_left: lda actbaseframel,y
jmp dact_frame
dact_right: lda actbaseframer,y
dact_frame: clc
Add the animation frame to the base frame, and store the frame number to the
spriteframe pointers (last 8 bytes of screen memory)
adc actf,x
sta 2040,x
Get actor's color and store it to the sprite color register
lda actcolor,y
sta $d027,x
If actor is expanded in X or Y direction, set the corresponding expand bits.
lda actmagx,y
beq dact_nomagx
lda virtd01d
ora bittable,x
sta virtd01d
dact_nomagx: lda actmagy,y
beq dact_nomagy
lda virtd017
ora bittable,x
sta virtd017
Multiply actor (sprite) number by 2 to get the index to the X/Y coordinate
registers.
dact_nomagy: txa
asl
tay
Store the X-coordinate least significant byte and Y-coordinate.
lda temp1
sta $d000,y
lda temp3
sta $d001,y
Then handle X-coordinate most significant bit; set the $d010 bit if X-
coordinate is in the range 256-511
lda temp2
beq dactnext
lda virtd010
ora bittable,x
sta virtd010
Loop until all actors done.
dactnext: dex
bmi dactdone
jmp dactloop
Then dump the virtual sprite bit registers to the actual video registers.
dactdone: lda virtd010
sta $d010
lda virtd015
sta $d015
lda virtd017
sta $d017
lda virtd01d
sta $d01d
rts
Powers of two for the corresponding bits of each sprite
bittable: dc.b 1,2,4,8,16,32,64,128
virtd010: dc.b 0
virtd015: dc.b 0
virtd017: dc.b 0
virtd01d: dc.b 0
"Waitras" subroutine
--------------------
Waits until the raster interrupt counter increased by "raster0" has changed.
Resets the counter afterwards.
waitras: lda rastercount
cmp #$01
bcc waitras
lda #$00
sta rastercount
rts
"Initscroll" subroutine
-----------------------
Clears the screen and sets the correct color memory value (multicolor white)
for gamescreen displaying. Resets the map & block positions ("mapx", "blockx")
as well as the fine scrolling ("scrollx") and sets the map data pointer based
on the level we're on. The background graphics map data for the levels is
organized in the memory as follows:
1st block-row of 1st level (100 blocks = 100 bytes)
...
5th block-row of 1st level (100 blocks = 100 bytes)
1st block-row of 2nd level (100 blocks = 100 bytes)
...
5th block-row of 2nd level (100 blocks = 100 bytes)
1st block-row of 3rd level (100 blocks = 100 bytes)
...
5th block-row of 3rd level (100 blocks = 100 bytes)
initscroll:
ldx #39
iscr1:
N SET 0
REPEAT 24
lda #$20
sta $400+N*40,x
lda #$09
sta $d800+N*40,x
N SET N+1
REPEND
dex
bmi iscrdone1
jmp iscr1
iscrdone1: lda #$00
sta mapx
sta blockx
lda #$07
sta scrollx
lda level
sec
sbc #$01
asl
tax
lda levelmaptbl,x
sta mapadrlo
lda levelmaptbl+1,x
sta mapadrhi
;Finally, set the display mode used by "raster1" interrupt.
lda #DISPGAME
sta dispmode
rts
levelmaptbl: dc.w MAP, MAP+500, MAP+1000
"Doscroll" subroutine
---------------------
Performs X-scrolling. Amount of pixels to scroll (scrolling speed) is given
in the accumulator.
If already at the right edge of a level, do not scroll further.
doscroll: ldx mapx
cpx #100
bcc doscrollok
rts
doscrollok: sta scrsub+1
sta sprsub+1
Move all actors to the left by the amount of pixels to scroll.
ldx #7
doscrollspr: lda actxl,x
sec
sprsub: sbc #$00
sta actxl,x
lda actxh,x
sbc #$00
sta actxh,x
dex
bpl doscrollspr
Then subtract the scrolling amount from the X fine-scroll. If it goes to
negative, screen data must be shifted.
lda scrollx
sec
scrsub: sbc #$00
bmi scrshift
sta scrollx
rts
scrshift: and #$07
sta scrollx
First shift the top 10 rows of gamescreen (screen rows 4-13) one char to the
left (all rows not done at once to eliminate tearing effects on NTSC machines,
that have less rastertime).
ldx #$00
scrshiftloop1:
N SET 4
REPEAT 10
lda $400+N*40+1,x
sta $400+N*40,x
N SET N+1
REPEND
inx
cpx #39
bne scrshiftloop1
Then shift the bottom 10 rows of gamescreen (screen rows 14-23)
ldx #$00
scrshiftloop2:
N SET 14
REPEAT 10
lda $400+N*40+1,x
sta $400+N*40,x
N SET N+1
REPEND
inx
cpx #39
bne scrshiftloop2
Next it's time to draw new background graphics to the edge of the screen.
The mapdata tells what numbered blocks must be drawn, and the blocks (4x4
char sized) tell what chars must be drawn on screen.
Add the map x-position to the left edge address of map.
lda mapadrlo
clc
adc mapx
sta temp1
lda mapadrhi
adc #$00
sta temp2
This is the destination screen pointer, starting from the rightmost column
of screen row 4 (first gamescreen row).
lda #<($400+4*40+39)
sta temp3
lda #>($400+4*40+39)
sta temp4
This is the row counter (20 rows to do)
lda #20
sta temp5
ldy #$00
Data for each 4x4 block is stored in the following way:
0 1 2 3
4 5 6 7
8 9 a b
c d e f
So, to get on the next row in a block, 4 must be added to the memory address
from where fetching the block data. To get the horizontal position within a
block, the block-x position (0-3) can just be added to that address.
scrblockloop: ldx blockx
Get the block number from the map data.
lda (temp1),y
tay
Modify the LDA instruction to fetch block data, to point to the address of just
that block.
lda blocktbllo,y
sta scrblockget+1
lda blocktblhi,y
sta scrblockget+2
To get onto the next map-row, increase the map address by 100 bytes (done here
already)
lda temp1
clc
adc #100
sta temp1
lda temp2
adc #$00
sta temp2
ldy #$00
Now get the chars from the blockdata and put them on the screen. X register
is the position within the block.
scrblockget: lda $1000,x
sta (temp3),y
Add 40 to the destination screen address to get on the next row.
lda temp3
clc
adc #40
sta temp3
lda temp4
adc #0
sta temp4
Increase position within block with 4 to get on the next block row (as told
earlier)
txa
adc #4
tax
All 20 rows done?
dec temp5
beq scrblockready
If the block-position went to 16 or over that it's time to fetch the next
block from the map data.
cpx #$10
bcc scrblockget
jmp scrblockloop
New data has been drawn. Now increase the block & map-positions, so that the
next column of background graphics will be drawn next time.
scrblockready: inc blockx
lda blockx
cmp #$04
bcc scrblockready2
lda #$00
sta blockx
inc mapx
scrblockready2: rts
This is a table for the addresses of all background graphics blocks.
blocktbllo:
N SET 0
REPEAT 128
dc.b #<(BLOCKS+N*16)
N SET N+1
REPEND
blocktblhi:
N SET 0
REPEAT 128
dc.b #>(BLOCKS+N*16)
N SET N+1
REPEND
"Getjoystick" subroutine
------------------------
First set all bits of $dc00 to 1 to be able to read them correctly, then
save the current joystick control status to the previous status, then get
new status from $dc00, negating all the bits.
getjoystick: lda #$ff
sta $dc00
lda joystick
sta prevjoy
lda $dc00
eor #$ff
sta joystick
rts
"Showpic" subroutine
--------------------
Displays the title bitmap picture. Transfers the screen & color data that has
been stored after the bitmap to their correct locations (for bitmap display,
the screen memory resides at $5c00).
showpic: ldx #$00
showpicloop: lda $8000,x
sta $5c00,x
lda $8100,x
sta $5d00,x
lda $8200,x
sta $5e00,x
lda $8400,x
sta $d800,x
lda $8500,x
sta $d900,x
lda $8600,x
sta $da00,x
inx
bne showpicloop
showpic2: lda $8300,x
sta $5f00,x
lda $8700,x
sta $db00,x
inx
cpx #192
bne showpic2
Set titlescreen display mode for the "raster1" interrupt.
lda #DISPTITLE
sta dispmode
rts
"Initscreen" subroutine
-----------------------
Sets color registers (background graphics multicolors and sprite multicolors),
turns all sprites multicolored and makes them be display over the background.
Draws also the initial scorepanel display and turns it yellow.
initscreen: lda #$00
sta $d020
sta $d021
lda #$0e
sta $d022
lda #$06
sta $d023
lda #$ff
sta $d01c
lda #$00
sta $d01b
lda #$0a
sta $d025
lda #$00
sta $d026
ldx #39
ip_loop: lda paneltext,x
and #$3f
sta $400+24*40,x
lda #$07
sta $d800+24*40,x
dex
bpl ip_loop
rts
"Drawscores" subroutine
-----------------------
Draws all the elements of the status bar, like score, lives, level, time &
hiscore.
drawscores: ldy #$02
ldx #$02
ds1: lda score,x
Get the binary coded decimal at the high 4 bits of the score byte.
lsr
lsr
lsr
lsr
Add 48 - character code of '0'.
clc
adc #48
Store to screen.
sta $400+24*40,y
iny
Then, get the binary coded decimal at the low 4 bits of the score byte.
lda score,x
and #$0f
clc
adc #48
sta $400+24*40,y
iny
Loop for all 3 bytes of the score.
dex
bpl ds1
ldx #$02
ldy #34
Display the hiscore in a similar fashion.
ds2: lda hiscore,x
lsr
lsr
lsr
lsr
clc
adc #48
sta $400+24*40,y
iny
lda hiscore,x
and #$0f
clc
adc #48
sta $400+24*40,y
iny
dex
bpl ds2
Display lives. This is just one digit.
lda lives
clc
adc #48
sta $400+24*40+14
Display time, that has 2 binary coded digits (similar to what was done for
the score & hiscore).
lda time
lsr
lsr
lsr
lsr
clc
adc #48
sta $400+24*40+20
lda time
and #$0f
clc
adc #48
sta $400+24*40+21
Display level number (only one digit)
lda level
clc
adc #48
sta $400+24*40+28
rts
paneltext: dc.b "SC MEN TI LEV HI "
"Initraster" subroutine
-----------------------
Activates raster interrupts. "Raster0" interrupt is to be executed first.
initraster: sei
lda #<raster0 ;Set main IRQ vector
sta $0314
lda #>raster0
sta $0315
lda #$7f ;Set timer interrupt off
sta $dc0d
lda #$01 ;Set raster interrupt on
sta $d01a
lda $d011
and #$7f
sta $d011
lda #RASTER0POS ;Set low bits of position
sta $d012 ;for first raster interrupt
lda $dc0d ;Acknowledge timer interrupt
cli ;(for safety)
rts
"Raster0" interrupt
-------------------
Sets video registers for the display of the score panel (X-scrolling is
stationary and singlecolor, screen memory at $0400-$07ff, videobank is at
$0000-$3fff), plays music and increases "rastercount", then sets up "raster1"
to be executed next.
raster0: cld
lda #27
sta $d011
lda #$03
sta $dd00
lda #21
sta $d018
lda #8
sta $d016
inc $d019
jsr MUSIC+3
lda #<raster1
sta $0314
lda #>raster1
sta $0315
lda #RASTER1POS
sta $d012
inc rastercount
jmp $ea81
"Raster1" interrupt
-------------------
Sets video registers according to the displaymode ("dispmode"). The game
screen & textscreen both are at $0400-$07ff, videobank at $0000-$3fff, but the
difference is the X-scrolling: gamescreen has variable X-finescrolling
("scrollx") while the textscreen has stationary X-scrolling. The bitmap screen
is at videobank $4000-$7fff, screen memory $5c00-$5fff, with multicolor bitmap
graphics. Finally, "raster0" is set to be executed next to form a loop.
raster1: cld
lda dispmode
beq r1_gamemode
r1_titlemode: cmp #DISPTEXT
bne r1_pic
lda #3
sta $dd00
lda #27
sta $d011
lda #$18
sta $d016
lda #21
sta $d018
jmp r1_end
r1_pic: lda #2
sta $dd00
lda #59
sta $d011
lda #24
sta $d016
lda #$78
sta $d018
lda #$00
sta $d015
jmp r1_end
r1_gamemode: lda #$03
sta $dd00
lda #27
sta $d011
lda #30
sta $d018
lda scrollx
and #$07
ora #$10
sta $d016
r1_end: inc $d019
lda #<raster0
sta $0314
lda #>raster0
sta $0315
lda #RASTER0POS
sta $d012
jmp $ea81
The variables
-------------
General variables:
score: dc.b 0,0,0
hiscore: dc.b 0,0,0
lives: dc.b 3
level: dc.b 1
time: dc.b $99
timedl: dc.b 0
firedelay: dc.b 0
killactive: dc.b 0
killmeter: dc.b 0
killmeterd: dc.b 0
killlimit: dc.b 0
Actor variable arrays:
actxl: ds.b 8,0
actxh: ds.b 8,0
acty: ds.b 8,0
actf: ds.b 8,0
actfd: ds.b 8,0
actd: ds.b 8,0
actsx: ds.b 8,0
actsy: ds.b 8,0
actyd: ds.b 8,0
actj: ds.b 8,0
actt: ds.b 8,0
actimm: ds.b 8,0
acthp: ds.b 8,0
Tables for properties of different actor types:
X- and Y-hotspots (centers within the sprite):
acthotx: dc.b 12,12,12,12,24,24
acthoty: dc.b 40,40,40,40,40,40
X- and Y-magnification:
actmagx: dc.b 0,0,0,0,1,1
actmagy: dc.b 1,1,1,1,1,1
X- and Y-sizes:
actsizex: dc.b 12,12,12,12,30,48
actsizey: dc.b 42,42,42,42,32,42
Colors:
actcolor: dc.b 11,9,2,4,12,7
Base frames facing left and right:
actbaseframer: dc.b 128,144,144,144,152,156
actbaseframel: dc.b 136,148,148,148,154,156
Addresses of move routines:
actroutlo: dc.b <plr,<man,<man,<man,<mc,<expl
actrouthi: dc.b >plr,>man,>man,>man,>mc,>expl
Shooting Y-coord modification:
actshootymod: dc.b 20,20,20,20,20,0
Initial hitpoints:
actmaxhp: dc.b 1,1,2,3,5,0
Score for killing an enemy:
actscorelo: dc.b $00,$50,$50,$00,$00,$00
actscorehi: dc.b $00,$02,$04,$06,$10,$00
Bullet variable arrays:
bulletxl: ds.b 16,0
bulletxh: ds.b 16,0
bullety: ds.b 16,0
bulletd: ds.b 16,0
bullett: ds.b 16,0
bulletlo: ds.b 16,0
bullethi: ds.b 16,0
bulletunder: ds.b 16,0
Included binary data
--------------------
The sprites:
org SPRITES
incbin efny.spr
The chars: (the char-collision data saved by BGEDIT is unused)
org CHARS-$100
incbin efny.chr
The music, made with SadoTracker:
org MUSIC
incbin music.bin
The background map data (map-header saved by BGEDIT is unused)
org MAP-2
incbin efny.map
The blocks (block-color data saved by BGEDIT is unused)
org BLOCKS-$80
blocks: incbin efny.blk
The title bitmap picture:
org PICTURE
incbin plissken.pic
So, there we have reached the end of the Escape From New York sourcecode, and
almost the end of this rant. But for a closing I'll explain how the music
was extracted, and the commands in the makefile.
The music was saved with the pack/relocate option of SadoTracker on a D64
image (EFNYMUS.D64), starting from address $4000. Then, that .PRG file was
extracted from the disk image (don't remember what utility I used back then,
today I would use D642PRG in my commandline-utility collection). The
EFNY+PLAYER.PRG file was then converted to a raw binary file MUSIC.BIN
(without start address) with the PRG2BIN utility.
The makefile commands:
- EFNY.PRG depends on the source code, on the IFF/LBM title picture and the
music binary:
efny.prg: efny.s efny.lbm music.bin
- Execute the BENTON64 picture conversion utility, save the picture as a raw
binary file PLISSKEN.PIC with bitmap data (8kb) followed by screen data (1kb)
and color memory data (1kb)
benton64 efny.lbm plissken.pic -r
- Assemble the source code with DASM, output file is EFNY.PRG. Use verbose mode
and maximum of 3 passes.
dasm efny.s -oefny.prg -v3 -p3
- Compress the output file with PUCRUNCH (get it at
http://www.cs.tut.fi/~albert/Dev/pucrunch/) with execution start address
set at 2048.
pucrunch -x2048 efny.prg efny.prg
If you are interested, you can examine the sprite file EFNY.SPR with SPREDIT
and the background data with BGEDIT (fastest is to press F9 to "load all
leveldata" and type EFNY)
Ok, now this rant is finished.
Lasse Öörni
loorni@student.oulu.fi