Escape From New York dissected by Cadaver ----------------------------------------- This rant dissects and explains my Crap Game Compo 1999 entry "Escape From New York" for your enjoyement & learning exclusively. 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 efix.zip (binary) efixsrc.zip (source) General info ------------ 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. 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: 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) End of rant. Lasse Öörni loorni@gmail.com