Jump to content
Site Stability Read more... ×
  • Advertisement
  • entries
    104
  • comments
    103
  • views
    262275

About this blog

Musings of a hobbyist

Entries in this blog

 

A C64 Game - Step 28

The levels are currently rather ermm. sparse. Remember that a screen is built by several primitve types. To get things rolling a few new building types are added:

Namely a character/color area, an alternating line horizontally and vertically (alternating means toggling the characters +1/-1) and a quad (a 2x2 tile).




We'll start with the new constants:
  LD_AREA = 4 ;data contains x,y,width,height,char,color LD_LINE_H_ALT = 5 ;data contains x,y,width,char,color LD_LINE_V_ALT = 6 ;data contains x,y,height,char,color LD_QUAD = 7 ;data contains x,y,quad_id

...the new level building code table entries...
  LEVEL_ELEMENT_TABLE_LO !byte <.LevelComplete !byte <LevelLineH !byte <LevelLineV !byte <LevelObject !byte <LevelArea !byte <LevelLineHAlternating !byte <LevelLineVAlternating !byte <LevelQuad LEVEL_ELEMENT_TABLE_HI !byte >.LevelComplete !byte >LevelLineH !byte >LevelLineV !byte >LevelObject !byte >LevelArea !byte >LevelLineHAlternating !byte >LevelLineVAlternating !byte >LevelQuad

Here's the area code. Straight forward, fetch all needed parameters and build the area row by row:
  !zone LevelAreaLevelArea ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y tax lda SCREEN_LINE_OFFSET_TABLE_LO,x sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 lda SCREEN_LINE_OFFSET_TABLE_HI,x sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_3 + 1 ;width iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 sta PARAM6 ;height iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;char iny lda (ZEROPAGE_POINTER_1),y sta PARAM4 ;color iny lda (ZEROPAGE_POINTER_1),y sta PARAM5 ;store target pointers to screen and color ram tya pha .NextLineArea ldy PARAM1 .NextCharArea lda PARAM4 sta (ZEROPAGE_POINTER_2),y lda PARAM5 sta (ZEROPAGE_POINTER_3),y iny dec PARAM2 bne .NextCharArea dec PARAM3 beq .AreaDone ;move pointers down a line tya sec sbc #40 tay lda ZEROPAGE_POINTER_2 clc adc #40 sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 lda ZEROPAGE_POINTER_2 + 1 adc #0 sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_3 + 1 lda PARAM6 sta PARAM2 jmp .NextLineArea .AreaDone jmp NextLevelData
I won't go into details about the alternating lines, they are almost the same as the common lines, with one small addition: After every set character the lowest bit of the character number is toggled.


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

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

For the record: This makes building the quad tables really annoying but the speedup is worth it.
  !zone LevelQuad LevelQuad ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;item iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;store y for next data tya pha ldy PARAM2 lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_3 + 1 ldy PARAM1 ldx PARAM3 ;put image lda BLOCK_TABLE_UL_LOCATION0,x sta (ZEROPAGE_POINTER_2),y lda BLOCK_TABLE_UL_COLOR_LOCATION0,x sta (ZEROPAGE_POINTER_3),y iny lda BLOCK_TABLE_UR_LOCATION0,x sta (ZEROPAGE_POINTER_2),y lda BLOCK_TABLE_UR_COLOR_LOCATION0,x sta (ZEROPAGE_POINTER_3),y tya clc adc #39 tay lda BLOCK_TABLE_LL_LOCATION0,x sta (ZEROPAGE_POINTER_2),y lda BLOCK_TABLE_LL_COLOR_LOCATION0,x sta (ZEROPAGE_POINTER_3),y iny lda BLOCK_TABLE_LR_LOCATION0,x sta (ZEROPAGE_POINTER_2),y lda BLOCK_TABLE_LR_COLOR_LOCATION0,x sta (ZEROPAGE_POINTER_3),y jmp NextLevelData
step28.zip


Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 27

Actually a rather simple step, but with a few new changes.

We're adding a new enemy, a bat, that once shot, hides. And randomly reappears to attack you, only to vanish again.






The first problem:

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

Since we have a SPRITE_STATE per object anyway a new design decision get added. Any states >= 128 are treated as non colliding. In other words, bat gets hit, set state to a value >= 128 and set empty sprite. Done. To reappear, repeat reverse.


Let's start with the simpler getting hit part. Set invisible sprite, set sprite state to 128. Voila! !zone HitBehaviourVanish HitBehaviourVanish lda SPRITE_STATE,x bne .NoHit lda #SPRITE_BAT_VANISH sta SPRITE_POINTER_BASE,x lda #128 sta SPRITE_STATE,x .NoHit rts

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

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


  !zone BehaviourBatVanishing BehaviourBatVanishing lda SPRITE_STATE,x bne .NotNormal jmp .NormalUpdate .NotNormal cmp #128 beq .Vanish1 cmp #129 beq .Hidden cmp #130 beq .Spawn cmp #1 bne .NoSpecialBehaviour jmp .AttackFlight .NoSpecialBehaviour rts .Vanish1 lda DELAYED_GENERIC_COUNTER and #$7 bne .NoSpecialBehaviour lda #SPRITE_INVISIBLE sta SPRITE_POINTER_BASE,x inc SPRITE_STATE,x jsr GenerateRandomNumber adc #24 sta SPRITE_MOVE_POS,x rts .Spawn lda DELAYED_GENERIC_COUNTER and #$7 bne .NoSpecialBehaviour lda #1 sta SPRITE_STATE,x lda #SPRITE_BAT_1 sta SPRITE_POINTER_BASE,x rts .Hidden dec SPRITE_MOVE_POS,x beq .Unhide rts .Unhide ;position diagonal above/below player lda SPRITE_CHAR_POS_X cmp #10 bcc .SpawnOnRight cmp #30 bcs .SpawnOnLeft ;randomly choose jsr GenerateRandomNumber and #$1 beq .SpawnOnRight .SpawnOnLeft lda SPRITE_CHAR_POS_X sec sbc #5 sta PARAM1 lda #0 sta SPRITE_DIRECTION,x jmp .FindYSpawnPos .SpawnOnRight lda SPRITE_CHAR_POS_X clc adc #5 sta PARAM1 lda #1 sta SPRITE_DIRECTION,x .FindYSpawnPos lda SPRITE_CHAR_POS_Y cmp #5 bcc .SpawnBelow cmp #15 bcs .SpawnAbove ;randomly choose jsr GenerateRandomNumber and #$1 beq .SpawnAbove .SpawnBelow lda SPRITE_CHAR_POS_Y clc adc #5 sta PARAM2 lda #0 sta SPRITE_FALLING,x jmp .Reposition .SpawnAbove lda SPRITE_CHAR_POS_Y sec sbc #5 sta PARAM2 lda #1 sta SPRITE_FALLING,x .Reposition jsr CalcSpritePosFromCharPos inc SPRITE_STATE,x lda #SPRITE_BAT_VANISH sta SPRITE_POINTER_BASE,x rts .NormalUpdate lda DELAYED_GENERIC_COUNTER and #$3 bne .NoAnimUpdate inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x and #$3 sta SPRITE_ANIM_POS,x tay lda BAT_ANIMATION,y sta SPRITE_POINTER_BASE,x .NoAnimUpdate lda SPRITE_STATE,x bne .NoAction lda SPRITE_DIRECTION,x beq .MoveRight ;move left jsr ObjectMoveLeftBlocking beq .ToggleDirection rts .MoveRight jsr ObjectMoveRightBlocking beq .ToggleDirection rts .ToggleDirection lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x .NoAction rts .AttackFlight inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x cmp #80 beq .AttackDone cmp #40 beq .ChangeFlyDirection ;fly towards player lda SPRITE_DIRECTION,x beq .FlyRight stx PARAM5 jsr ObjectMoveLeft jmp .FlyUpDown .FlyRight stx PARAM5 jsr ObjectMoveRight .FlyUpDown ldx PARAM5 lda SPRITE_FALLING,x beq .FlyUp jsr ObjectMoveDown rts .FlyUp jsr ObjectMoveUp rts .ChangeFlyDirection ;change direction to avoid flying out of the screen lda SPRITE_CHAR_POS_Y,x cmp #5 bcc .ChangeY cmp #18 bcc .CheckXDir .ChangeY lda SPRITE_FALLING,x eor #$1 sta SPRITE_FALLING,x .CheckXDir lda SPRITE_CHAR_POS_X,x cmp #5 bcc .ChangeX cmp #32 bcs .ChangeX rts .ChangeX lda SPRITE_DIRECTION,x eor #$1 sta SPRITE_DIRECTION,x rts .AttackDone ;auto-vanish lda #0 sta SPRITE_STATE,x jmp HitBehaviourVanish step27.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 26

More gameplay action, whee!

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

And while underground the zombie will move about. With the effect, that the zombie will probably appear some where else, not where the player is waiting with the shotgun.
Adding a new enemy is pretty easy now, a new entry in the behaviour table. There are a few more entries in other tables (start color, start sprite, etc.) which we don't go into detail now.
  ENEMY_BEHAVIOUR_TABLE_LO !byte <PlayerControl !byte <BehaviourBatLR !byte <BehaviourBatUD !byte <BehaviourBat8 !byte <BehaviourMummy !byte <BehaviourZombie ENEMY_BEHAVIOUR_TABLE_HI !byte >PlayerControl !byte >BehaviourBatLR !byte >BehaviourBatUD !byte >BehaviourBat8 !byte >BehaviourMummy !byte >BehaviourZombie

First off the generic counter is checked to slow the zombie movement. The most interesting part in this step is the added state for the enemies. The zombie makes good use of it, as it comes with several states:
-normal walk
-collapsing (2 part)
-reappearing (2 part)
-hidden (walk)
  !zone BehaviourZombie BehaviourZombie lda DELAYED_GENERIC_COUNTER and #$3 beq .MovementUpdate rts .MovementUpdate lda SPRITE_STATE,x bne .OtherStates jmp .NormalWalk .OtherStates ;collapsing? cmp #128 beq .Collapsing1 cmp #129 beq .Collapsing2 cmp #131 beq .WakeUp1 cmp #132 beq .WakeUp2 cmp #130 bne .NotHidden jmp .Hidden .NotHidden rts .Collapsing1 lda #SPRITE_ZOMBIE_COLLAPSE_R_2 clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x inc SPRITE_STATE,x rts .Collapsing2 lda #SPRITE_INVISIBLE sta SPRITE_POINTER_BASE,x inc SPRITE_STATE,x ;generate hidden time jsr GenerateRandomNumber clc adc #25 sta SPRITE_MOVE_POS,x ;normalise position on full char ldy SPRITE_CHAR_POS_X_DELTA,x sty PARAM5 .CheckXPos beq .XPosClear jsr ObjectMoveLeft dec PARAM5 jmp .CheckXPos .XPosClear ldy SPRITE_CHAR_POS_Y_DELTA,x sty PARAM5 .CheckYPos beq .YPosClear jsr ObjectMoveUp dec PARAM5 jmp .CheckYPos .YPosClear rts .WakeUp1 lda #SPRITE_ZOMBIE_COLLAPSE_R_1 clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x inc SPRITE_STATE,x rts .WakeUp2 lda #SPRITE_ZOMBIE_WALK_R_1 clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x lda #0 sta SPRITE_STATE,x sta SPRITE_MOVE_POS,x rts .NormalWalk inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x and #$7 sta SPRITE_MOVE_POS,x cmp #4 bpl .CanMove rts .CanMove lda SPRITE_DIRECTION,x beq .MoveRight ;move left jsr ObjectWalkLeft beq .ToggleDirection rts .MoveRight jsr ObjectWalkRight beq .ToggleDirection rts .ToggleDirection lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x rts .Hidden ;are we apt to wake up? dec SPRITE_MOVE_POS,x bne .RandomMove ;wake up inc SPRITE_STATE,x lda #SPRITE_ZOMBIE_COLLAPSE_R_2 clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x rts .RandomMove ;move randomly left/right jsr GenerateRandomNumber and #$1 beq .MoveLeft ;move right if possible jsr CanWalkRight beq .Blocked inc SPRITE_CHAR_POS_X,x ldy #8 sty PARAM5 .MoveSpriteRight jsr MoveSpriteRight dec PARAM5 bne .MoveSpriteRight rts .MoveLeft jsr CanWalkLeft beq .Blocked dec SPRITE_CHAR_POS_X,x ldy #8 sty PARAM5 .MoveSpriteLeft jsr MoveSpriteLeft dec PARAM5 bne .MoveSpriteLeft rts .Blocked rts   Since shooting the zombie has a different effect than for any other enemy the hurt table acquires a new entry: ;behaviour for an enemy being hit ENEMY_HIT_BEHAVIOUR_TABLE_LO !byte <HitBehaviourHurt ;bat LR !byte <HitBehaviourHurt ;bat UD !byte <HitBehaviourHurt ;bat8 !byte <HitBehaviourHurt ;mummy !byte <HitBehaviourCrumble ;zombie ENEMY_HIT_BEHAVIOUR_TABLE_HI !byte >HitBehaviourHurt ;bat LR !byte >HitBehaviourHurt ;bat UD !byte >HitBehaviourHurt ;bat8 !byte >HitBehaviourHurt ;mummy !byte >HitBehaviourCrumble ;zombie   HitBehaviourCrumble is completely tailored to the zombie, it merely sets the collapse state. The collapsing itself is done in the behaviour code.
  !zone HitBehaviourCrumble HitBehaviourCrumble lda SPRITE_STATE,x bne .NoHit lda #SPRITE_ZOMBIE_COLLAPSE_R_1 clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x lda #128 sta SPRITE_STATE,x .NoHit rts
step26.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 25

Time to spruce up the title screen again.


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




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

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

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


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

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

Here we store the bitmap data at $2000: ;place the data at a valid bitmap position, this avoids copying the data * = $2000 TITLE_LOGO_BMP_DATA !byte 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 !byte 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ...   One of the color parts can also be fixed in memory:
  * = $2c00 TITLE_LOGO_SCREEN_CHAR !byte 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 !byte 0,0,0,0,0,0,0,0,0,0,32,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ...   The final part is copied to its required target location on demand:
  TITLE_LOGO_COLORRAM !byte 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ...   During settup up the title screen state we need to copy the color information to the color RAM. Since we only need 64 pixel height for the logo we only copy 320 bytes:
  ldx #0 .FillColor lda TITLE_LOGO_COLORRAM,x sta SCREEN_COLOR,x inx bne .FillColor .FillColor2 lda TITLE_LOGO_COLORRAM + 256,x sta SCREEN_COLOR + 256,x inx cpx #( 320 - 256 ) bne .FillColor2   Now to set up the initial raster interrupt. Note that before doing that we call jsr WaitFrame once so the raster line is at a specific location. You don't want to hit the interrupt on the wrong side.
  ;----------------------------------- ;init IRQ ;----------------------------------- !zone InitTitleIRQ InitTitleIRQ sei lda #$37 ; make sure that IO regs at $dxxx sta $1 ;are visible lda #$7f ;disable cia #1 generating timer irqs sta $dc0d ;which are used by the system to flash cursor, etc lda #$1 ;tell VIC we want him generate raster irqs sta $d01a lda #$10 ;nr of rasterline we want our irq occur at sta $d012 lda #$1b ;MSB of d011 is the MSB of the requested rasterline sta $d011 ;as rastercounter goes from 0-312 ;set irq vector to point to our routine lda #<IrqSetBitmapMode sta $314 lda #>IrqSetBitmapMode sta $315 ;acknowledge any pending cia timer interrupts ;this is just so we are 100% safe lda $dc0d lda $dd0d cli rts   Note the routine IrqSetBitmapMode (and IrqSetTextMode). These routines set their respective mode and setup the raster interrupt for the other part.
  ;----------------------------------- ;IRQ Title - set bitmap mode ;----------------------------------- !zone IrqSetBitmapMode IrqSetBitmapMode ;acknowledge VIC irq lda $d019 sta $d019 ;install top part lda #<IrqSetTextMode sta $314 lda #>IrqSetTextMode sta $315 ;nr of rasterline we want our irq occur at lda #$71 sta $d012 ;bitmap modus an lda #$3b sta $D011 ;set VIC to bank 0 lda $DD00 and #$fc ora #$3 sta $dd00 ;bitmap to lower half, screen char pos at 3 * 1024 ( + 16384) lda #%10111000 sta $D018 JMP $ea31 ;----------------------------------- ;IRQ Title - set text mode ;----------------------------------- !zone IrqSetTextMode IrqSetTextMode ;acknowledge VIC irq lda $d019 sta $d019 ;install scroller irq lda #<IrqSetBitmapMode sta $314 lda #>IrqSetBitmapMode sta $315 ;nr of rasterline we want our irq occur at lda #$10 sta $d012 ;disable bitmap mode lda #$1b sta $D011 ;set VIC to bank 3 lda $DD00 and #$fc sta $dd00 ;bitmap to lower half, screen char pos at 3 * 1024 ( + 16384) lda #%00111100 sta $D018 jmp $ea31   Once the title screen state is left the interrupt needs to be disabled:
  ;----------------------------------- ;release IRQ ;----------------------------------- !zone ReleaseTitleIRQ ReleaseTitleIRQ sei lda #$37 ; make sure that IO regs at $dxxx sta $1 ;are visible lda #$ff ;enable cia #1 generating timer irqs sta $dc0d ;which are used by the system to flash cursor, etc ;no more raster irqs lda #$00 sta $d01a lda #$31 sta $314 lda #$EA sta $315 ;acknowledge any pending cia timer interrupts ;this is just so we are 100% safe lda $dc0d lda $dd0d cli rts
step25.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 24

What use are highscores if they aren't saved?



With this step highscores are saved and loaded from your previously used medium (tape or disk). On startup the currently used medium is stored, save and load is done via bog standard kernal routines.
  The additions for this are rather miniscule. Both save and load routines work on a continious piece of memory. Fortunately both highscore names and scores are right next to each other.

For saving note that the auto-delete function in the common floppy drive is borked. Therefore we manually delete the file first and save it afterwards.
The "S0:" in front of the file name is the special code for Scratching the file. HIGHSCORE_DELETE_FILENAME !text "S0:HIGHSCORE" HIGHSCORE_DELETE_FILENAME_END !zone SaveScores SaveScores ;delete old save file first lda #HIGHSCORE_DELETE_FILENAME_END - HIGHSCORE_DELETE_FILENAME ldx #<HIGHSCORE_DELETE_FILENAME ldy #>HIGHSCORE_DELETE_FILENAME jsr KERNAL_SETNAM lda #$0F ; file number 15 ldx DRIVE_NUMBER ldy #$0F ; secondary address 15 jsr KERNAL_SETLFS jsr $FFC0 ; call OPEN ; if carry set, the file could not be opened bcs .ErrorDelete ldx #$0F ; filenumber 15 jsr $FFC9 ; call CHKOUT (file 15 now used as output) .close lda #$0F ; filenumber 15 jsr $FFC3 ; call CLOSE ldx #$00 ; filenumber 0 jsr $FFC9 ; call CHKOUT (reset output device) jmp .SaveNow .ErrorDelete ;Akkumulator contains BASIC error code ;most likely errors: ;A = $5 (DEVICE NOT PRESENT) ;... error handling for open errors ... lda #65 sta $cc00 jmp .close ; even if OPEN failed, the file has to be closed .SaveNow lda #9 ldx #<HIGHSCORE_FILENAME ldy #>HIGHSCORE_FILENAME jsr KERNAL_SETNAM lda #$00 ldx DRIVE_NUMBER ldy #$00 jsr KERNAL_SETLFS lda #<HIGHSCORE_SCORE sta $C1 lda #>HIGHSCORE_SCORE sta $C2 ldx #<HIGHSCORE_DATA_END ldy #>HIGHSCORE_DATA_END lda #$C1 ; start address located in $C1/$C2 jsr $FFD8 ; call SAVE ;if carry set, a save error has happened; bcs .SaveError rts   Loading the score back in means mostly inserting the corresponding load calls:
  ;-------------------------------------------------- ;load high scores ;returns 1 if ok, 0 otherwise ;-------------------------------------------------- !zone LoadScores LoadScores ;disable kernal messages (do not want to see load error etc.) lda #$00 jsr KERNAL_SETMSG ;set logical file parameters lda #15 ldx DRIVE_NUMBER ldy #0 jsr KERNAL_SETLFS ;set filename lda #9 ldx #<HIGHSCORE_FILENAME ldy #>HIGHSCORE_FILENAME jsr KERNAL_SETNAM ;load to address lda #$00 ; 0 = load ldx #<HIGHSCORE_SCORE ldy #>HIGHSCORE_SCORE jsr KERNAL_LOAD bcs .LoadError ;flag whether ok or not is set into the Carry flag lda #1 rts .LoadError lda #0 rts
step24.zip

Previous StepNext Step

Endurion

Endurion

 

A C64 game - Step 23

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

Adding to the score moving we find the spot for the name, shift all lower score names down and let the player enter his name. The Kernal routine we're using is called GETIN and is placed in the ROM at $FFE4.
The following huge code snippet is placed in the old CheckForHighscore routine. For explanation reasons it's separated.
The first block moves the name entries of lower scores downwards. Remember, the name entries are stored continously in the location at HIGHSCORE_NAME.
  ;move names down ;shift older entries down, add new entry lda #( HIGHSCORE_ENTRY_COUNT - 1 ) sta PARAM2 ;y carries the offset in the score text, position at start of second last entry ldy #( ( HIGHSCORE_NAME_SIZE + 1 ) * ( HIGHSCORE_ENTRY_COUNT - 2 ) ) .CopyName lda PARAM2 cmp PARAM1 beq .SetNewName ;copy name ldx #0 .CopyNextNameChar lda HIGHSCORE_NAME,y sta HIGHSCORE_NAME + ( HIGHSCORE_NAME_SIZE + 1 ),y iny inx cpx #HIGHSCORE_NAME_SIZE bne .CopyNextNameChar tya sec sbc #( HIGHSCORE_NAME_SIZE + HIGHSCORE_NAME_SIZE + 1 ) tay dec PARAM2 jmp .CopyName   Here's the interesting part. First the proper offset of the new name inside the big text is calculated and the old name cleared out.
  .SetNewName ;calc y for new name offset ldy PARAM1 lda #0 .AddNameOffset cpy #0 beq .NameOffsetFound clc adc #( HIGHSCORE_NAME_SIZE + 1 ) dey jmp .AddNameOffset .NameOffsetFound tay ;clear old name ldx #0 sty PARAM3 lda #32 .ClearNextChar sta HIGHSCORE_NAME,y iny inx cpx #HIGHSCORE_NAME_SIZE bne .ClearNextChar   Here's the meat, the actual name entry. Of course with Backspace support and Enter to finalize the entry. Out of sheer lazyness the newly entered char is only inserted in the text and the text then fully displayed.
  ldy PARAM3 ;enter name ldx #0 stx PARAM3 jmp .ShowChar .GetNextChar sty PARAM4 ;use ROM routines, read char jsr KERNAL_GETIN beq .GetNextChar ;return pressed? cmp #13 beq .EnterPressed ;DEL pressed? cmp #20 bne .NotDel ;DEL pressed ldy PARAM4 ldx PARAM3 beq .GetNextChar dec PARAM3 dey dex lda #32 sta HIGHSCORE_NAME,y jmp .ShowChar .NotDel ldy PARAM4 ;pressed key >= 32 or <= 96? cmp #32 bcc .GetNextChar cmp #96 bcs .GetNextChar ;max length reached already? ldx PARAM3 cpx #HIGHSCORE_NAME_SIZE bcs .GetNextChar ;save text sta HIGHSCORE_NAME,y iny inx .ShowChar stx PARAM3 sty PARAM4 ;display high scores ;x,y pos of name lda #6 sta PARAM1 lda #10 sta PARAM2 lda #<HIGHSCORE_NAME sta ZEROPAGE_POINTER_1 lda #>HIGHSCORE_NAME sta ZEROPAGE_POINTER_1 + 1 jsr DisplayText ldx PARAM3 ldy PARAM4 jmp .GetNextChar .EnterPressed ;fill entry with blanks lda #32 ldx PARAM3 ldy PARAM4 .FillNextChar cpx #HIGHSCORE_NAME_SIZE beq .FilledUp sta HIGHSCORE_NAME,y iny inx jmp .FillNextChar .FilledUp jmp TitleScreen   step23.zip


Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 22

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

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



First the newly added text fields, note that '-' is used as line break and '*' as end of text marker.
Another note: If you default fill high scores always have an easily beatable top score. People like to be better than someone else. HIGHSCORE_SCORE !text "00050000-" !text "00040000-" !text "00030000-" !text "00020000-" !text "00010000-" !text "00001000-" !text "00000300-" !text "00000100*" HIGHSCORE_NAME !text "SUPERNATURAL-" !text "SUPERNATURAL-" !text "SUPERNATURAL-" !text "SUPERNATURAL-" !text "SUPERNATURAL-" !text "SUPERNATURAL-" !text "SUPERNATURAL-" !text "SUPERNATURAL*"   In the title screen layout code we add the display routines:
  ;display high scores ;x,y pos of name lda #6 sta PARAM1 lda #10 sta PARAM2 lda #<HIGHSCORE_NAME sta ZEROPAGE_POINTER_1 lda #>HIGHSCORE_NAME sta ZEROPAGE_POINTER_1 + 1 jsr DisplayText ;x,y pos of score lda #25 sta PARAM1 lda #10 sta PARAM2 lda #<HIGHSCORE_SCORE sta ZEROPAGE_POINTER_1 lda #>HIGHSCORE_SCORE sta ZEROPAGE_POINTER_1 + 1 jsr DisplayText   And last but not least, the actual checking for a highscore. You'll see that the score is not actually kept in a variable. Instead the screen chars at the display are used to compare against the text.
  ;------------------------------------------------------------ ;check if the player got a new highscore entry ;------------------------------------------------------------ !zone CheckForHighscore CheckForHighscore lda #0 sta PARAM1 ldy #0 .CheckScoreEntry ldx #0 sty PARAM2 .CheckNextDigit lda SCREEN_CHAR + ( 23 * 40 + 8 ),x cmp HIGHSCORE_SCORE,y bcc .NotHigher bne .IsHigher ;need to check next digit iny inx cpx #HIGHSCORE_SCORE_SIZE beq .IsHigher jmp .CheckNextDigit .NotHigher inc PARAM1 lda PARAM1 cmp #HIGHSCORE_ENTRY_COUNT beq .NoNewHighscore ;y points somewhere inside the score, recalc next line pos lda PARAM2 clc adc #( HIGHSCORE_SCORE_SIZE + 1 ) tay jmp .CheckScoreEntry .NoNewHighscore jmp TitleScreen .IsHigher ;shift older entries down, add new entry lda #( HIGHSCORE_ENTRY_COUNT - 1 ) sta PARAM2 ;y carries the offset in the score text, position at start of second last entry ldy #( ( HIGHSCORE_SCORE_SIZE + 1 ) * ( HIGHSCORE_ENTRY_COUNT - 2 ) ) .CopyScore lda PARAM2 cmp PARAM1 beq .SetNewScore ;copy score ldx #0 .CopyNextScoreDigit lda HIGHSCORE_SCORE,y sta HIGHSCORE_SCORE + ( HIGHSCORE_SCORE_SIZE + 1 ),y iny inx cpx #HIGHSCORE_SCORE_SIZE bne .CopyNextScoreDigit tya sec sbc #( HIGHSCORE_SCORE_SIZE + HIGHSCORE_SCORE_SIZE + 1 ) tay dec PARAM2 jmp .CopyScore .SetNewScore ;y points at score above the new entry tya clc adc #( HIGHSCORE_SCORE_SIZE + 1 ) tay ldx #0 .SetNextScoreDigit lda SCREEN_CHAR + ( 23 * 40 + 8 ),x sta HIGHSCORE_SCORE,y iny inx cpx #HIGHSCORE_SCORE_SIZE bne .SetNextScoreDigit jmp TitleScreen   step22.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 21

Now some more tidbits. A game is not complete without a neat title screen:


For now this is nothing more than a almost empty screen and a separate control loop. The loop simply waits for a button press and then jumps to the game main loop. Once the player lost all lives he is returned to the title loop.

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

Rather unspectacular, the title screen code. First the screen is displayed and then the check button loop entered. Once the button has been pressed the game variables are reset and the code runs into the main game loop. ;------------------------------------------------------------ ;the title screen game loop ;------------------------------------------------------------ !zone TitleScreen TitleScreen ldx #0 stx BUTTON_PRESSED stx BUTTON_RELEASED sta VIC_SPRITE_ENABLE ;clear screen lda #32 ldy #0 jsr ClearScreen ;display title logo lda #<TEXT_TITLE sta ZEROPAGE_POINTER_1 lda #>TEXT_TITLE sta ZEROPAGE_POINTER_1 + 1 lda #0 sta PARAM1 lda #1 sta PARAM2 jsr DisplayText ;display start text lda #<TEXT_FIRE_TO_START sta ZEROPAGE_POINTER_1 lda #>TEXT_FIRE_TO_START sta ZEROPAGE_POINTER_1 + 1 lda #11 sta PARAM1 lda #23 sta PARAM2 jsr DisplayText .TitleLoop jsr WaitFrame lda #$10 bit $dc00 bne .ButtonNotPressed ;button pushed lda BUTTON_RELEASED bne .Restart jmp .TitleLoop .ButtonNotPressed lda #1 sta BUTTON_RELEASED jmp .TitleLoop .Restart ;game start values lda #3 sta PLAYER_LIVES ;setup level jsr StartLevel lda #0 sta LEVEL_NR jsr BuildScreen jsr CopyLevelToBackBuffer ;------------------------------------------------------------ ;the main game loop ;------------------------------------------------------------   Of course the counter part needs to be added to the DeadControl routine. If the player lost his last life, return to the title screen: !zone DeadControl DeadControl lda SPRITE_ACTIVE beq .PlayerIsDead rts .PlayerIsDead lda #$10 bit $dc00 bne .ButtonNotPressed ;button pushed lda BUTTON_RELEASED bne .Restart rts .ButtonNotPressed lda #1 sta BUTTON_RELEASED rts .Restart ;if last live return to title lda PLAYER_LIVES bne .RestartLevel jmp TitleScreen .RestartLevel
step21.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 20

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




Adding the reload part is done with the following code piece. If the joystick is not moved and the player isn't waiting for the recoil to end the variable PLAYER_STAND_STILL_TIME is increased. Once it hits 40 frames one shell is added.
  ;check if player moved lda $dc00 and #$1f cmp #$1f bne .PlayerMoved ;do not reload while recoil lda PLAYER_SHOT_PAUSE bne .PlayerMoved inc PLAYER_STAND_STILL_TIME lda PLAYER_STAND_STILL_TIME cmp #40 bne .HandleFire ;reload lda #1 sta PLAYER_STAND_STILL_TIME ;already fully loaded? lda PLAYER_SHELLS cmp PLAYER_SHELLS_MAX beq .HandleFire inc PLAYER_SHELLS ;display loaded shells ldy PLAYER_SHELLS lda #2 sta SCREEN_COLOR + 23 * 40 + 18,y lda #7 sta SCREEN_COLOR + 24 * 40 + 18,y   The other addition enhances the PickItem routine. Simply check the picked item type, and either add a new bullet slot or increase the player live counter.
  !zone PickItem PickItem lda ITEM_ACTIVE,y cmp #ITEM_BULLET beq .EffectBullet cmp #ITEM_HEALTH beq .EffectHealth .RemoveItem lda #ITEM_NONE sta ITEM_ACTIVE,y lda #3 jsr IncreaseScore jsr RemoveItemImage rts .EffectBullet lda PLAYER_SHELLS_MAX cmp #5 beq .RemoveItem ldx PLAYER_SHELLS_MAX lda #224 sta SCREEN_CHAR + 23 * 40 + 19,x lda #225 sta SCREEN_CHAR + 24 * 40 + 19,x lda #6 sta SCREEN_COLOR + 23 * 40 + 19,x sta SCREEN_COLOR + 24 * 40 + 19,x inc PLAYER_SHELLS_MAX jmp .RemoveItem .EffectHealth lda PLAYER_LIVES cmp #99 beq .RemoveItem inc PLAYER_LIVES sty PARAM1 jsr DisplayLiveNumber ldy PARAM1 jmp .RemoveItem
step20.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 19

Nothing really new this time, just constant use of existing parts. A new enemy, some sort of mummy, which slowly walks back and forth. And takes a few more hits than the bats.

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

Adding the enemy itself is nothing more than adding new entries to the existing tables
  ;object type constants TYPE_PLAYER = 1 TYPE_BAT_LR = 2 TYPE_BAT_UD = 3 TYPE_BAT_8 = 4 TYPE_MUMMY = 5 ENEMY_BEHAVIOUR_TABLE_LO !byte <PlayerControl !byte <BehaviourBatLR !byte <BehaviourBatUD !byte <BehaviourBat8 !byte <BehaviourMummy ENEMY_BEHAVIOUR_TABLE_HI !byte >PlayerControl !byte >BehaviourBatLR !byte >BehaviourBatUD !byte >BehaviourBat8 !byte >BehaviourMummy IS_TYPE_ENEMY !byte 0 ;dummy entry for inactive object !byte 0 ;player !byte 1 ;bat_lr !byte 1 ;bat_ud !byte 1 ;bat 8 !byte 1 ;mummy TYPE_START_SPRITE !byte 0 ;dummy entry for inactive object !byte SPRITE_PLAYER_STAND_R !byte SPRITE_BAT_1 !byte SPRITE_BAT_1 !byte SPRITE_BAT_2 !byte SPRITE_MUMMY_R_1 TYPE_START_COLOR !byte 0 !byte 10 !byte 3 !byte 3 !byte 8 !byte 1 TYPE_START_MULTICOLOR !byte 0 !byte 1 !byte 0 !byte 0 !byte 0 !byte 0 TYPE_START_HP !byte 0 !byte 1 !byte 5 !byte 5 !byte 5 !byte 10   The behaviour code looks quite simple, as most of the new code is inside subroutines ObjectWalkLeft/ObjectWalkRight. ;------------------------------------------------------------ ;simply walk left/right, do not fall off ;------------------------------------------------------------ !zone BehaviourMummy BehaviourMummy lda DELAYED_GENERIC_COUNTER and #$3 beq .MovementUpdate rts .MovementUpdate inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x and #$7 sta SPRITE_MOVE_POS,x cmp #4 bpl .CanMove rts .CanMove lda SPRITE_DIRECTION,x beq .MoveRight ;move left jsr ObjectWalkLeft beq .ToggleDirection rts .MoveRight jsr ObjectWalkRight beq .ToggleDirection rts .ToggleDirection lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x rts   The new subroutines are a more complex version of ObjectMoveLeft/ObjectMoveRight. They add a new check, so the enemy only walks forward, if it's not blocked and there is no gap in the floor in front:
  ;------------------------------------------------------------ ;walk object right if not blocked, do not fall off ;x = object index ;------------------------------------------------------------ !zone ObjectWalkRight ObjectWalkRight lda SPRITE_CHAR_POS_X_DELTA,x beq .CheckCanMoveRight .CanMoveRight inc SPRITE_CHAR_POS_X_DELTA,x lda SPRITE_CHAR_POS_X_DELTA,x cmp #8 bne .NoCharStep lda #0 sta SPRITE_CHAR_POS_X_DELTA,x inc SPRITE_CHAR_POS_X,x .NoCharStep jsr MoveSpriteRight lda #1 rts .CheckCanMoveRight ldy SPRITE_CHAR_POS_Y,x iny lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ldy SPRITE_CHAR_POS_X,x iny lda (ZEROPAGE_POINTER_1),y jsr IsCharBlockingFall beq .BlockedRight ldy SPRITE_CHAR_POS_Y,x dey lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ldy SPRITE_CHAR_POS_X,x iny lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .BlockedRight tya clc adc #40 tay lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .BlockedRight jmp .CanMoveRight .BlockedRight lda #0 rts   step19.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 18

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

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

Every objects get an animation delay (SPRITE_ANIM_DELAY), animation pos (SPRITE_ANIM_POS) and movement pos counter (SPRITE_MOVE_POS).





Remember, adding a new type means just adding the new constant and entries to the startup value tables.
If you wonder about the flickering white border on the bottom half: It's an easy way to see how long the actual per frame code runs. You'll notice more complex code taking quite a bit more time.

Here's a detailed look at the path code. It's actually pretty straight forward. Read the next byte. Check if the high bit is set and use the result to either move left/right. Rinse and repeat for Y.
  ;------------------------------------------------------------ ;move in flat 8 ;------------------------------------------------------------ !zone BehaviourBat8 BehaviourBat8 ;do not update animation too fast lda DELAYED_GENERIC_COUNTER and #$3 bne .NoAnimUpdate inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x and #$3 sta SPRITE_ANIM_POS,x tay lda BAT_ANIMATION,y sta SPRITE_POINTER_BASE,x .NoAnimUpdate inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x and #31 sta SPRITE_MOVE_POS,x ;process next path pos tay lda PATH_8_DX,y beq .NoXMoveNeeded sta PARAM1 and #$80 beq .MoveRight ;move left lda PARAM1 and #$7f sta PARAM1 .MoveLeft jsr MoveSpriteLeft dec PARAM1 bne .MoveLeft jmp .XMoveDone .MoveRight jsr MoveSpriteRight dec PARAM1 bne .MoveRight .NoXMoveNeeded .XMoveDone ldy SPRITE_MOVE_POS,x lda PATH_8_DY,y beq .NoYMoveNeeded sta PARAM1 and #$80 beq .MoveDown ;move up lda PARAM1 and #$7f sta PARAM1 .MoveUp jsr MoveSpriteUp dec PARAM1 bne .MoveUp rts .MoveDown jsr MoveSpriteDown dec PARAM1 bne .MoveDown .NoYMoveNeeded rts   The tables themselves are handmade. For the planned path we just need to make sure we end up where we started: PATH_8_DX !byte $86 !byte $86 !byte $85 !byte $84 !byte $83 !byte $82 !byte $81 !byte 0 !byte 0 !byte 1 !byte 2 !byte 3 !byte 4 !byte 5 !byte 6 !byte 6 !byte 6 !byte 6 !byte 5 !byte 4 !byte 3 !byte 2 !byte 1 !byte 0 !byte 0 !byte $81 !byte $82 !byte $83 !byte $84 !byte $85 !byte $86 !byte $86 PATH_8_DY !byte 0 !byte 1 !byte 2 !byte 3 !byte 4 !byte 5 !byte 6 !byte 6 !byte 6 !byte 6 !byte 5 !byte 4 !byte 3 !byte 2 !byte 1 !byte 0 !byte 0 !byte $81 !byte $82 !byte $83 !byte $84 !byte $85 !byte $86 !byte $86 !byte $86 !byte $86 !byte $85 !byte $84 !byte $83 !byte $82 !byte $81 !byte 0   step18.zip
Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 17

Another rather small step, but visually pleasing. We're enhancing the player sprite with animation and better jump abilities.

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

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



Here are the new parts for walking left:
  ;animate player lda SPRITE_FALLING bne .NoAnimLNeeded lda PLAYER_JUMP_POS bne .NoAnimLNeeded inc SPRITE_ANIM_DELAY lda SPRITE_ANIM_DELAY cmp #8 bne .NoAnimLNeeded lda #0 sta SPRITE_ANIM_DELAY inc SPRITE_ANIM_POS lda SPRITE_ANIM_POS and #$3 sta SPRITE_ANIM_POS .NoAnimLNeeded   The same for right movement:
  ;animate player lda SPRITE_FALLING bne .NoAnimRNeeded lda PLAYER_JUMP_POS bne .NoAnimRNeeded inc SPRITE_ANIM_DELAY lda SPRITE_ANIM_DELAY cmp #8 bne .NoAnimRNeeded lda #0 sta SPRITE_ANIM_DELAY inc SPRITE_ANIM_POS lda SPRITE_ANIM_POS and #$3 sta SPRITE_ANIM_POS .NoAnimRNeeded   And all the missing animation for jumping, falling, recoil and combined states. Note that the sprites are arranged in right/left pairs, so that adding SPRITE_DIRECTION (0 = facing right, 1 = facing left) to the sprite frame results in the proper sprite.
  ;update player animation lda SPRITE_FALLING bne .AnimFalling lda PLAYER_JUMP_POS bne .AnimJumping ;is player shooting? lda PLAYER_SHOT_PAUSE beq .AnimNoRecoil ;recoil anim lda SPRITE_ANIM_POS asl clc adc SPRITE_DIRECTION adc #SPRITE_PLAYER_WALK_R_1 adc #8 sta SPRITE_POINTER_BASE rts .AnimNoRecoil lda SPRITE_ANIM_POS asl clc adc SPRITE_DIRECTION adc #SPRITE_PLAYER_WALK_R_1 sta SPRITE_POINTER_BASE rts .AnimFalling lda PLAYER_SHOT_PAUSE bne .AnimFallingNoRecoil lda #SPRITE_PLAYER_FALL_R clc adc SPRITE_DIRECTION sta SPRITE_POINTER_BASE rts .AnimFallingNoRecoil lda #SPRITE_PLAYER_FALL_RECOIL_R clc adc SPRITE_DIRECTION sta SPRITE_POINTER_BASE rts .AnimJumping lda PLAYER_SHOT_PAUSE bne .AnimJumpingNoRecoil lda #SPRITE_PLAYER_JUMP_R clc adc SPRITE_DIRECTION sta SPRITE_POINTER_BASE rts .AnimJumpingNoRecoil lda #SPRITE_PLAYER_JUMP_RECOIL_R clc adc SPRITE_DIRECTION sta SPRITE_POINTER_BASE rts   step17.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 16

Now for some rather small addition, which however feels like a bigger step: Score/Live/Level display.

We already have a text display function, so we add a new default text for the display with initial 00000 values. Note that the score doesn't fit into a byte easily. We only update the numbers on the screen, we do not store the score in another location.
This makes it quite easy to update. For every step we start at the right most digit and increase it. If it hits the digit after '9', set to '0' again and repeat the step on char to the left. For retro sake we don't start at the right most score digit, but the second right most (making increase steps always being 10). If you look closer at a lot of older games you'll see that their right most score digit never changes (Bubble Bobble, etc.)

  Small text entry:
  TEXT_DISPLAY !text " SCORE: 000000 LIVES: 03 LEVEL: 00 *"   Increase score bit:
  ;------------------------------------------------------------ ;increases score by A ;note that the score is only shown ; not held in a variable ;------------------------------------------------------------ !zone IncreaseScore IncreaseScore sta PARAM1 stx PARAM2 sty PARAM3 .IncreaseBy1 ldx #4 .IncreaseDigit inc SCREEN_CHAR + ( 23 * 40 + 8 ),x lda SCREEN_CHAR + ( 23 * 40 + 8 ),x cmp #58 bne .IncreaseBy1Done ;looped digit, increase next lda #48 sta SCREEN_CHAR + ( 23 * 40 + 8 ),x dex ;TODO - this might overflow jmp .IncreaseDigit .IncreaseBy1Done dec PARAM1 bne .IncreaseBy1 ;increase complete, restore x,y ldx PARAM2 ldy PARAM3 rts   Another neat effect is the display of the level number and lives. Due to the hard coded screen position I've made two specialized functions instead of a generic one.

Interesting anecdote:

When I first had to display a decimal number I was stumped due to no available div operator. You actually need to divide by yourself (subtract divisor and increase count until done). That's what the call to DivideBy10 does.
  ;------------------------------------------------------------ ;displays level number ;------------------------------------------------------------ !zone DisplayLevelNumber DisplayLevelNumber lda LEVEL_NR clc adc #1 jsr DivideBy10 pha ;10 digit tya clc adc #48 sta SCREEN_CHAR + ( 23 * 40 + 37 ) pla clc adc #48 sta SCREEN_CHAR + ( 23 * 40 + 38 ) rts ;------------------------------------------------------------ ;displays live number ;------------------------------------------------------------ !zone DisplayLiveNumber DisplayLiveNumber lda PLAYER_LIVES jsr DivideBy10 pha ;10 digit tya clc adc #48 sta SCREEN_CHAR + ( 23 * 40 + 24 ) pla clc adc #48 sta SCREEN_CHAR + ( 23 * 40 + 25 ) rts ;------------------------------------------------------------ ;divides A by 10 ;returns remainder in A ;returns result in Y ;------------------------------------------------------------ !zone DivideBy10 DivideBy10 sec ldy #$FF .divloop iny sbc #10 bcs .divloop adc #10 rts   step16.zip
Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 15

Now we start with a few game essentials: Progressing to the next level.



Not too complicated. We keep a counter of enemies alive (NUMBER_ENEMIES_ALIVE) which is initially set to 0. Since we already have a lookup table IS_TYPE_ENEMY we simply add a check inside AddObject. If the new object is an enemy, increase the counter:
  ;adjust enemy counter ldx PARAM3 lda IS_TYPE_ENEMY,x beq .NoEnemy inc NUMBER_ENEMIES_ALIVE .NoEnemy   The other spot where this comes in is when we kill an enemy. Inside our FireShot routine we add: .EnemyKilled ldy SPRITE_ACTIVE,x lda IS_TYPE_ENEMY,y beq .NoEnemy dec NUMBER_ENEMIES_ALIVE .NoEnemy jsr RemoveObject   For the level change we add a new control routine (GameFlowControl) in the main game loop. Once the enemy count reaches 0 we increase a level done delay so we don't immediately jump onwards. Once reached, disable all sprites, build next level and continue.

For now there's two simple levels with the last looping back to the first.
  ;------------------------------------------------------------ ;the main game loop ;------------------------------------------------------------ GameLoop jsr WaitFrame jsr GameFlowControl jsr DeadControl jsr ObjectControl jsr CheckCollisions jmp GameLoop ;------------------------------------------------------------ ;controls the game flow ;------------------------------------------------------------ !zone GameFlowControl GameFlowControl inc DELAYED_GENERIC_COUNTER lda DELAYED_GENERIC_COUNTER cmp #8 bne .NoTimedActionYet lda #0 sta DELAYED_GENERIC_COUNTER ;level done delay lda NUMBER_ENEMIES_ALIVE bne .NotDoneYet inc LEVEL_DONE_DELAY lda LEVEL_DONE_DELAY cmp #20 beq .GoToNextLevel inc VIC_BORDER_COLOR .NotDoneYet .NoTimedActionYet rts .GoToNextLevel lda #0 sta VIC_SPRITE_ENABLE inc LEVEL_NR jsr BuildScreen jsr CopyLevelToBackBuffer rts
step15.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 14

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

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

Effectively we double the work during level building. We start out with the new "buffers":
  ;address of the screen backbuffer SCREEN_BACK_CHAR = $C800 ;address of the screen backbuffer SCREEN_BACK_COLOR = $C400
After calling the BuildScreen subroutine we copy the screen and color RAM to the backup buffers. Note the check for 230 bytes. We only have a play field of 40x23 characters, so only 4 * 230 = 920 bytes are needed. ;copy level data to back buffer ldx #$00 .ClearLoop lda SCREEN_CHAR,x sta SCREEN_BACK_CHAR,x lda SCREEN_CHAR + 230,x sta SCREEN_BACK_CHAR + 230,x lda SCREEN_CHAR + 460,x sta SCREEN_BACK_CHAR + 460,x lda SCREEN_CHAR + 690,x sta SCREEN_BACK_CHAR + 690,x inx cpx #230 bne .ClearLoop ldx #$00 .ColorLoop lda SCREEN_COLOR,x sta SCREEN_BACK_COLOR,x lda SCREEN_COLOR + 230,x sta SCREEN_BACK_COLOR + 230,x lda SCREEN_COLOR + 460,x sta SCREEN_BACK_COLOR + 460,x lda SCREEN_COLOR + 690,x sta SCREEN_BACK_COLOR + 690,x inx cpx #230 bne .ColorLoop
The repaint item function is thusly modified to simply copy the character and color values from the backup buffer:
  ;------------------------------------------------------------ ;remove item image from screen ;Y = item index ;------------------------------------------------------------ !zone RemoveItemImage RemoveItemImage sty PARAM2 ;set up pointers lda ITEM_POS_Y,y tay lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 sta ZEROPAGE_POINTER_4 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_2 + 1 sec sbc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_3 + 1 sec sbc #( ( SCREEN_BACK_CHAR - SCREEN_BACK_COLOR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_4 + 1 ldx PARAM2 ldy ITEM_POS_X,x ;... and copying lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_2),y lda (ZEROPAGE_POINTER_3),y sta (ZEROPAGE_POINTER_1),y iny lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_2),y lda (ZEROPAGE_POINTER_3),y sta (ZEROPAGE_POINTER_1),y tya clc adc #39 tay lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_2),y lda (ZEROPAGE_POINTER_3),y sta (ZEROPAGE_POINTER_1),y iny lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_2),y lda (ZEROPAGE_POINTER_3),y sta (ZEROPAGE_POINTER_1),y ;repaint other items to avoid broken overlapped items ldx #0 .RepaintLoop lda ITEM_ACTIVE,x cmp #ITEM_NONE beq .RepaintNextItem txa pha jsr PutItemImage pla tax .RepaintNextItem inx cpx #ITEM_COUNT bne .RepaintLoop ldy PARAM2 rts
step14.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 13

And yet again a bigger step.


Of course we need lots of goodies from the killed enemies. We add items. Items are displayed as 2x2 block of characters. There's a new list of possible items with location added (ITEM_ACTIVE, etc.). The list also stores the original background behind the items.
To get an item spawned just walk beside one of the enemies (look in their direction) and keep fire pressed until it dies.

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




Inside the player subroutine FireShot we add another subroutine call after killing an enemy: .EnemyKilled jsr RemoveObject jsr SpawnItem   SpawnItem itself resembles the AddObject routine. First we loop over the active item table (ITEM_ACTIVE) to find a free slot.

Once found we randomly chose the item type (for now there are two types). The ugly part below that stores the original character and color at the item position and puts the items char and color in its place. Remember the item is sized 2x2 chars, so we need to store 8 bytes overall. However to keep the code comfortable, we actually use 8 tables. This allows use to only work with the item index instead of manually accessing a second index. There's only two index registers after all.
  ;------------------------------------------------------------ ;spawns an item at char position from object x ;X = object index ;------------------------------------------------------------ !zone SpawnItem SpawnItem ;find free item slot ldy #0 .CheckNextItemSlot lda ITEM_ACTIVE,y cmp #ITEM_NONE beq .FreeSlotFound iny cpy #ITEM_COUNT bne .CheckNextItemSlot rts .FreeSlotFound jsr GenerateRandomNumber and #$1 sta ITEM_ACTIVE,y lda SPRITE_CHAR_POS_X,x sta ITEM_POS_X,y lda SPRITE_CHAR_POS_Y,x sta ITEM_POS_Y,y sty PARAM1 ;find address in screen buffer... tay lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 sta ZEROPAGE_POINTER_2 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ;...and for the color buffer clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_2 + 1 ldy SPRITE_CHAR_POS_X,x ldx PARAM1 ;store old background and put item ;we do not take overlapping items in account yet! lda (ZEROPAGE_POINTER_1),y sta ITEM_BACK_CHAR_UL,x lda (ZEROPAGE_POINTER_2),y sta ITEM_BACK_COLOR_UL,x lda ITEM_CHAR_UL,x sta (ZEROPAGE_POINTER_1),y lda ITEM_COLOR_UL,x sta (ZEROPAGE_POINTER_2),y iny lda (ZEROPAGE_POINTER_1),y sta ITEM_BACK_CHAR_UR,x lda (ZEROPAGE_POINTER_2),y sta ITEM_BACK_COLOR_UR,x lda ITEM_CHAR_UR,x sta (ZEROPAGE_POINTER_1),y lda ITEM_COLOR_UR,x sta (ZEROPAGE_POINTER_2),y tya clc adc #39 tay lda (ZEROPAGE_POINTER_1),y sta ITEM_BACK_CHAR_LL,x lda (ZEROPAGE_POINTER_2),y sta ITEM_BACK_COLOR_LL,x lda ITEM_CHAR_LL,x sta (ZEROPAGE_POINTER_1),y lda ITEM_COLOR_LL,x sta (ZEROPAGE_POINTER_2),y iny lda (ZEROPAGE_POINTER_1),y sta ITEM_BACK_CHAR_LR,x lda (ZEROPAGE_POINTER_2),y sta ITEM_BACK_COLOR_LR,x lda ITEM_CHAR_LR,x sta (ZEROPAGE_POINTER_1),y lda ITEM_COLOR_LR,x sta (ZEROPAGE_POINTER_2),y rts
step13.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 12

One of the more complex steps. And also one I someday need to heavily optimize. The player can now shoot an enemy.

The central function for this is FireShot. We don't use a bullet but insta-shot. However walls should block the shot as well. This means, we need to take the current player direction and position, and work our way to the left/right until we hit an enemy or a wall.
Since there's no direct collision involved we take the character pos of the player, add or decrease the x pos and compare against all alive enemies pos. Rinse and repeat until done.
I've given the enemies 5 HP, and the player has a shot delay of 10 frames. Therefore it takes a while for the enemy to disappear (best tested with the box on top). If the player is purple the shot delay is active.
We start with adding a fire delay to PlayerControl:
  lda PLAYER_SHOT_PAUSE bne .FirePauseActive lda #1 sta VIC_SPRITE_COLOR lda #$10 bit $dc00 bne .NotFirePushed jsr FireShot jmp .FireDone .FirePauseActive dec PLAYER_SHOT_PAUSE .FireDone .NotFirePushed
This simply checks for PLAYER_SHOT_PAUSE. If it is higher than 0 the player is still pausing. If so the counter is decreased and the fire function skipped. If the counter is zero, we check the fire button and if pressed call the FireShot routine.

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

Now the hard part. There is no visible bullet. So we take the current player position, increase/decrease X and check for a blocking char or a hittable enemy. If the bullet is blocked, done. If an enemy is hit, decrease its health by one point. Once the health is down to zero the enemy is removed.
  !zone FireShotFireShot ;frame delay until next shot lda #10 sta PLAYER_SHOT_PAUSE ;mark player as shooting lda #4 sta VIC_SPRITE_COLOR ldy SPRITE_CHAR_POS_Y dey lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ldy SPRITE_CHAR_POS_X .ShotContinue lda SPRITE_DIRECTION beq .ShootRight ;shooting left dey lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .ShotDone jmp .CheckHitEnemy .ShootRight iny lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .ShotDone .CheckHitEnemy ;hit an enemy? ldx #1 .CheckEnemy stx PARAM2 lda SPRITE_ACTIVE,x beq .CheckNextEnemy tax lda IS_TYPE_ENEMY,x beq .CheckNextEnemy ;sprite pos matches on x? ldx PARAM2 sty PARAM1 lda SPRITE_CHAR_POS_X,x cmp PARAM1 bne .CheckNextEnemy ;sprite pos matches on y? lda SPRITE_CHAR_POS_Y,x cmp SPRITE_CHAR_POS_Y beq .EnemyHit ;sprite pos matches on y + 1? clc adc #1 cmp SPRITE_CHAR_POS_Y beq .EnemyHit ;sprite pos matches on y - 1? sec sbc #2 cmp SPRITE_CHAR_POS_Y bne .CheckNextEnemy .EnemyHit ;enemy hit! dec SPRITE_HP,x lda SPRITE_HP,x beq .EnemyKilled jmp .ShotDone .EnemyKilled jsr RemoveObject jmp .ShotDone .CheckNextEnemy ldx PARAM2 inx cpx #8 bne .CheckEnemy jmp .ShotContinue .ShotDone rts   step12.zip
Previous Step Next Step

Endurion

Endurion

 

A C64 game - Step 11

From colliding to dying is a small step. Once the player collides with an enemy we kill him by removing the player object. A "Press Fire to Restart" message is displayed and a press on the button will revive the player object.
We add the function RemoveObject which simply removes the object from the SPRITE_ACTIVE table and disables its sprite. While we wait for the player to press the button all the rest of the game moves on.


First of all we add the getting killed part in our old routine "CheckCollisions". Nothing ground breaking, a call to the text display function follows by removing the object and resetting the button released flag.
  .PlayerCollidedWithEnemy ;display text lda #<TEXT_PRESS_FIRE sta ZEROPAGE_POINTER_1 lda #>TEXT_PRESS_FIRE sta ZEROPAGE_POINTER_1 + 1 lda #10 sta PARAM1 lda #23 sta PARAM2 jsr DisplayText ldx #0 stx BUTTON_PRESSED stx BUTTON_RELEASED jsr RemoveObject rts

A new call is added to the main game loop which controls behaviour when the player is dead: GameLoop jsr WaitFrame jsr DeadControl jsr ObjectControl jsr CheckCollisions jmp GameLoop   Surprisingly easy. We check if the player is really dead, if he isn't, bail out. Then we check for the joystick button being pressed, but only allow to go on, if the button has been released before. If all that happened, we simply force the player object back into life (for now with hard coded values).
  !zone DeadControl DeadControl lda SPRITE_ACTIVE beq .PlayerIsDead rts .PlayerIsDead lda #$10 bit $dc00 bne .ButtonNotPressed ;button pushed lda BUTTON_RELEASED bne .Restart rts .ButtonNotPressed lda #1 sta BUTTON_RELEASED rts .Restart lda #5 sta PARAM1 lda #4 sta PARAM2 ;type lda #TYPE_PLAYER sta PARAM3 ldx #0 lda PARAM3 sta SPRITE_ACTIVE,x ;PARAM1 and PARAM2 hold x,y already jsr CalcSpritePosFromCharPos ;enable sprite lda BIT_TABLE,x ora VIC_SPRITE_ENABLE sta VIC_SPRITE_ENABLE ;initialise enemy values lda #SPRITE_PLAYER sta SPRITE_POINTER_BASE,x ;look right per default lda #0 sta SPRITE_DIRECTION,x rts
step11.zip

Previous Step 10Next Step

Endurion

Endurion

 

A C64 game - Step 10

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

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

  The routine CheckCollisions is simply added to the main game loop: GameLoop jsr WaitFrame jsr ObjectControl jsr CheckCollisions jmp GameLoop   The function CheckCollision just loops through the active object list and calls IsEnemyCollidingWithPlayer for every active entry:
  ;------------------------------------------------------------ ;check object collisions (enemy vs. player etc.) ;x ;------------------------------------------------------------ CheckCollisions ldx #1 .CollisionLoop lda SPRITE_ACTIVE,x bne .CheckObject .NextObject inx cpx #8 bne .CollisionLoop lda #0 sta VIC_BORDER_COLOR rts .CheckObject stx PARAM2 jsr IsEnemyCollidingWithPlayer bne .PlayerCollidedWithEnemy ldx PARAM2 jmp .NextObject .PlayerCollidedWithEnemy lda #1 sta VIC_BORDER_COLOR ;ldx #0 ;jsr RemoveObject rts   IsEnemyCollidingWithPlayer employs a few tricks to ease the calculation.
First we do the Y coordinate check to weed out. For the X coordinate: Since the actual X position is 9 bits we half the value (half the X coordinate and add 128 if the extended X bit is set). Now the comparation is easy.
The routine then returns 1 if a collision occurs and 0 if not.
  ;------------------------------------------------------------ ;check object collision with player (object 0) ;x = enemy index ;return a = 1 when colliding, a = 0 when not ;------------------------------------------------------------ !zone IsEnemyCollidingWithPlayer .CalculateSimpleXPos ;Returns a with simple x pos (x halved + 128 if > 256) ;modifies y lda BIT_TABLE,x and SPRITE_POS_X_EXTEND beq .NoXBit lda SPRITE_POS_X,x lsr clc adc #128 rts .NoXBit lda SPRITE_POS_X,x lsr rts IsEnemyCollidingWithPlayer ;modifies X ;check y pos lda SPRITE_POS_Y,x sec sbc #( OBJECT_HEIGHT ) ;offset to bottom cmp SPRITE_POS_Y bcs .NotTouching clc adc #( OBJECT_HEIGHT + OBJECT_HEIGHT - 1 ) cmp SPRITE_POS_Y bcc .NotTouching ;X = Index in enemy-table jsr .CalculateSimpleXPos sta PARAM1 ldx #0 jsr .CalculateSimpleXPos sec sbc #4 ;position X-Anfang Player - 12 Pixel cmp PARAM1 bcs .NotTouching adc #8 cmp PARAM1 bcc .NotTouching lda #1 rts .NotTouching lda #0 rts   step10.zip
Previous Step 9 Next Step 11

Endurion

Endurion

 

A C64 game - Step 9

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

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

Loop over all active objects and jump at their behaviour code. Note that we apply a nasty trick. Since jsr doesn't allow for indirect jumps we manually push the return address on the stack and then call indirect jmp. This allows for the behaviour code to return with rts.
  ;------------------------------------------------------------ ;Enemy Behaviour ;------------------------------------------------------------ !zone ObjectControl ObjectControl ldx #0 .ObjectLoop ldy SPRITE_ACTIVE,x beq .NextObject ;enemy is active dey lda ENEMY_BEHAVIOUR_TABLE_LO,y sta ZEROPAGE_POINTER_2 lda ENEMY_BEHAVIOUR_TABLE_HI,y sta ZEROPAGE_POINTER_2 + 1 ;set up return address for rts lda #>( .NextObject - 1 ) pha lda #<( .NextObject - 1 ) pha jmp (ZEROPAGE_POINTER_2) .NextObject inx cpx #8 bne .ObjectLoop rts   The main game loop is now changed; removed the call of PlayerControl and added the call to ObjectControl: ;------------------------------------------------------------ ;the main game loop ;------------------------------------------------------------ GameLoop jsr WaitFrame jsr ObjectControl jmp GameLoop   The behaviour table is built from the behaviour code addresses. Actually we use two tables for high and low byte, this way we don't have to mess with the index. The < and > operators return the low and high byte of a 16 bit value.
  ENEMY_BEHAVIOUR_TABLE_LO !byte <PlayerControl !byte <BehaviourDumbEnemyLR !byte <BehaviourDumbEnemyUD ENEMY_BEHAVIOUR_TABLE_HI !byte >PlayerControl !byte >BehaviourDumbEnemyLR !byte >BehaviourDumbEnemyUD
step9.zip



Previous Step 8 Next Step 10

Endurion

Endurion

 

A C64 game - Step 8

Of course a game isn't a game without some challenge. Therefore we need enemies. Since we have some neat little level build code why not use it for enemies as well?
We add a new level primitive type LD_OBJECT which adds objects (= sprites). We use it for both player and enemies. A new table SPRITE_ACTIVE is added to see if a sprite is used (and which type).


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


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


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

How we add an object during level buildup:
  .Object ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;type iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;store y for later tya pha ;add object to sprite array jsr FindEmptySpriteSlot beq .NoFreeSlot lda PARAM3 sta SPRITE_ACTIVE,x ;PARAM1 and PARAM2 hold x,y already jsr CalcSpritePosFromCharPos ;enable sprite lda BIT_TABLE,x ora VIC_SPRITE_ENABLE sta VIC_SPRITE_ENABLE lda #SPRITE_PLAYER sta SPRITE_POINTER_BASE,x .NoFreeSlot jmp .NextLevelData
step8.zip


Previous Step 7 Next Step 9

Endurion

Endurion

 

A C64 game - Step 7

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

This step shows:
-gravity (accelerating)
-jumping (following a delta y curve)

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


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


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


  .PlayerIsJumping inc PLAYER_JUMP_POS lda PLAYER_JUMP_POS cmp #JUMP_TABLE_SIZE bne .JumpOn lda #0 sta PLAYER_JUMP_POS jmp .JumpComplete .JumpOn ldx PLAYER_JUMP_POS lda PLAYER_JUMP_TABLE,x beq .JumpComplete sta PARAM5 .JumpContinue jsr PlayerMoveUp beq .JumpBlocked dec PARAM5 bne .JumpContinue jmp .JumpComplete .JumpBlocked lda #0 sta PLAYER_JUMP_POS jmp .JumpStopped

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

If the player is falling the player is moved down entry-of-fall-table pixel. .PlayerFell ldx PLAYER_FALL_POS lda FALL_SPEED_TABLE,x beq .FallComplete sta PARAM5 .FallLoop dec PARAM5 beq .FallComplete jsr PlayerMoveDown jmp .FallLoop .FallComplete lda PLAYER_FALL_POS cmp #( FALL_TABLE_SIZE - 1 ) beq .FallSpeedAtMax inc PLAYER_FALL_POS .FallSpeedAtMax


step7.zip

Previous Step 6 Next Step 8

Endurion

Endurion

 

A C64 Game - Step 6

And onwards we go: Obviously we don't want the player to move through walls. In this step we check the chars in the players way to see if they are blocking.
To make this easier we store the character pos and the character delta pos (0 to 7) for x and y for every sprite (SPRITE_CHAR_POS_X, SPRITE_CHAR_POS_X_DELTA). If the sprite is not at a character brink the move is allowed, if it hits the brink, check the characters at the target.

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

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

This step shows:
-calculating character positions while moving about
-checking the position for blocking chars
-calculating the required sprite position from character pos (for starting a sprite at a specific place)

Since the code is basically the same for all four directions I'll only go into details on one of them:
  ;------------------------------------------------------------ ;PlayerMoveLeft ;------------------------------------------------------------ !zone PlayerMoveLeft PlayerMoveLeft ldx #0 ;check if we are on the brink of a character lda SPRITE_CHAR_POS_X_DELTA beq .CheckCanMoveLeft ;no, we are not .CanMoveLeft dec SPRITE_CHAR_POS_X_DELTA jsr MoveSpriteLeft rts .CheckCanMoveLeft lda SPRITE_CHAR_POS_Y_DELTA beq .NoThirdCharCheckNeeded ;find the character in the screen buffer ldy SPRITE_CHAR_POS_Y lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 lda SPRITE_CHAR_POS_X clc adc #39 ;39 equals one line down (40 chars) and one to the left (-1) tay lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .BlockedLeft .NoThirdCharCheckNeeded ldy SPRITE_CHAR_POS_Y dey lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ldy SPRITE_CHAR_POS_X dey lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .BlockedLeft tya clc adc #40 tay lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .BlockedLeft lda #8 sta SPRITE_CHAR_POS_X_DELTA dec SPRITE_CHAR_POS_X jmp .CanMoveLeft .BlockedLeft rts   The subroutine IsCharBlocking is rather primitive, as described it only checks if the character is smaller than 128:
  ;------------------------------------------------------------ ;IsCharBlocking ;checks if a char is blocking ;A contains the character ;returns 1 for blocking, 0 for not blocking ;------------------------------------------------------------ !zone IsCharBlocking IsCharBlocking cmp #128 bpl .Blocking lda #0 rts .Blocking lda #1 rts
step6.zip

Previous Step 5 Next Step 7

Endurion

Endurion

 

A C64 game - Step 5

Our next big step:

Obviously we want some play screen, and more obviously, we are not going to store full screens (we've got only 64Kb Ram after all). Therefore there's a level build routine that allows to build a screen with various building elements. For now we'll start out with vertical and horizontal lines. Since we always have a level border at the screen edges we'll have the border as a second screen data block (LEVEL_BORDER_DATA).
To allow faster screen building we also have a table with the precalculated char offsets of a vertical line (SCREEN_LINE_OFFSET_TABLE_LO and SCREEN_LINE_OFFSET_TABLE_HI). Note that with the C64s mnemonics the best way to get stuff done is tables, tables, tables.
The BuildScreen sub routine clears the play screen area, builds the level data and then the border. The level data is a collection of primitives which are then worked through until LD_END is hit.

The first addition is the call to the buildroutine:
  ;setup level lda #0 sta LEVEL_NR jsr BuildScreen   The BuildScreen routine sets up a level completely. It starts out with clearing the current play area (not the full screen). Then LEVEL_NR is used to look up the location of the level data in table SCREEN_DATA_TABLE.
.BuildLevel is jumped to to actually work through the level primitives and put them on screen. Then we use LEVEL_BORDER_DATA as second level to display a border on the screens edges. .BuildLevel uses Y as index through the data. Depending on the first byte different routines are called (.LineH, .LineV, .LevelComplete). Since I think levels may be complex and use more than 256 bytes we add Y to the level data pointers so we can start out with Y=0 for the next primitive.
  ;------------------------------------------------------------ ;BuildScreen ;creates a screen from level data ;------------------------------------------------------------ !zone BuildScreen BuildScreen lda #0 ldy #6 jsr ClearPlayScreen ;get pointer to real level data from table ldx LEVEL_NR lda SCREEN_DATA_TABLE,x sta ZEROPAGE_POINTER_1 lda SCREEN_DATA_TABLE + 1,x sta ZEROPAGE_POINTER_1 + 1 jsr .BuildLevel ;get pointer to real level data from table lda #<LEVEL_BORDER_DATA sta ZEROPAGE_POINTER_1 lda #>LEVEL_BORDER_DATA sta ZEROPAGE_POINTER_1 + 1 jsr .BuildLevel rts .BuildLevel ;work through data ldy #255 .LevelDataLoop iny lda (ZEROPAGE_POINTER_1),y cmp #LD_END beq .LevelComplete cmp #LD_LINE_H beq .LineH cmp #LD_LINE_V beq .LineV .LevelComplete rts .NextLevelData pla ;adjust pointers so we are able to access more ;than 256 bytes of level data clc adc #1 adc ZEROPAGE_POINTER_1 sta ZEROPAGE_POINTER_1 lda ZEROPAGE_POINTER_1 + 1 adc #0 sta ZEROPAGE_POINTER_1 + 1 ldy #255 jmp .LevelDataLoop   The primitive display routines .LineH and .LineV are rather straight forward. Read the parameters, and put the character and color values in place.
  .LineH ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;width iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;char iny lda (ZEROPAGE_POINTER_1),y sta PARAM4 ;color iny lda (ZEROPAGE_POINTER_1),y sta PARAM5 ;store target pointers to screen and color ram ldx PARAM2 lda SCREEN_LINE_OFFSET_TABLE_LO,x sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 lda SCREEN_LINE_OFFSET_TABLE_HI,x sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_3 + 1 tya pha ldy PARAM1 .NextChar lda PARAM4 sta (ZEROPAGE_POINTER_2),y lda PARAM5 sta (ZEROPAGE_POINTER_3),y iny dec PARAM3 bne .NextChar jmp .NextLevelData .LineV ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;height iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;char iny lda (ZEROPAGE_POINTER_1),y sta PARAM4 ;color iny lda (ZEROPAGE_POINTER_1),y sta PARAM5 ;store target pointers to screen and color ram ldx PARAM2 lda SCREEN_LINE_OFFSET_TABLE_LO,x sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 lda SCREEN_LINE_OFFSET_TABLE_HI,x sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_3 + 1 tya pha ldy PARAM1 .NextCharV lda PARAM4 sta (ZEROPAGE_POINTER_2),y lda PARAM5 sta (ZEROPAGE_POINTER_3),y ;adjust pointer lda ZEROPAGE_POINTER_2 clc adc #40 sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 lda ZEROPAGE_POINTER_2 + 1 adc #0 sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) & 0xff00 ) >> 8 sta ZEROPAGE_POINTER_3 + 1 dec PARAM3 bne .NextCharV jmp .NextLevelData
step5.zip

Previous Step 4 Next Step 6

Endurion

Endurion

 

A C64 game - Step 4

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

For clarification: The C64 has 8 hardware sprites. That's 8 objects of the size 24 x 21 pixels. They can be placed anywhere. The coordinates are stored in memory addresses. However since the X resolution is higher than 256 all the sprites 9th bit is stored in another memory location (which makes it highly annoying to work with).
Sprite coordinates are set in X, Y pairs via the memory locations 53248 (=X sprite 0), 53249 (=Y sprite 0), 53250 (=X sprite 1), etc. The extended sprite bits are stored in 53248 + 16.

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

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


This step shows:
-Joystick control
-Sprite extended x bit



Inside the GameLoop we add a call to the players control function: jsr PlayerControl   PlayerControl itself checks the joystick port (II) and calls the proper direction move routines. Note that the move routines themselves simply set the object index to 0 (for the player) and call a generic sprite move routine.
  ;------------------------------------------------------------ ;check joystick (player control) ;------------------------------------------------------------ !zone PlayerControl PlayerControl lda #$2 bit $dc00 bne .NotDownPressed jsr PlayerMoveDown .NotDownPressed lda #$1 bit $dc00 bne .NotUpPressed jsr PlayerMoveUp .NotUpPressed lda #$4 bit $dc00 bne .NotLeftPressed jsr PlayerMoveLeft .NotLeftPressed lda #$8 bit $dc00 bne .NotRightPressed jsr PlayerMoveRight .NotRightPressed rts PlayerMoveLeft ldx #0 jsr MoveSpriteLeft rts PlayerMoveRight ldx #0 jsr MoveSpriteRight rts PlayerMoveUp ldx #0 jsr MoveSpriteUp rts PlayerMoveDown ldx #0 jsr MoveSpriteDown rts   The sprite move routines are rather simple, update the position counter variables and set the actual sprite registers. A bit more complicated are the X move functions. If X reaches the wraparound, the extended x bit (the 9th bit) is looked up in a table and then added/removed.
  ;------------------------------------------------------------ ;Move Sprite Left ;expect x as sprite index (0 to 7) ;------------------------------------------------------------ !zone MoveSpriteLeft MoveSpriteLeft dec SPRITE_POS_X,x bpl .NoChangeInExtendedFlag lda BIT_TABLE,x eor #$ff and SPRITE_POS_X_EXTEND sta SPRITE_POS_X_EXTEND sta VIC_SPRITE_X_EXTEND .NoChangeInExtendedFlag txa asl tay lda SPRITE_POS_X,x sta VIC_SPRITE_X_POS,y rts ;------------------------------------------------------------ ;Move Sprite Right ;expect x as sprite index (0 to 7) ;------------------------------------------------------------ !zone MoveSpriteRight MoveSpriteRight inc SPRITE_POS_X,x lda SPRITE_POS_X,x bne .NoChangeInExtendedFlag lda BIT_TABLE,x ora SPRITE_POS_X_EXTEND sta SPRITE_POS_X_EXTEND sta VIC_SPRITE_X_EXTEND .NoChangeInExtendedFlag txa asl tay lda SPRITE_POS_X,x sta VIC_SPRITE_X_POS,y rts ;------------------------------------------------------------ ;Move Sprite Up ;expect x as sprite index (0 to 7) ;------------------------------------------------------------ !zone MoveSpriteUp MoveSpriteUp dec SPRITE_POS_Y,x txa asl tay lda SPRITE_POS_Y,x sta 53249,y rts ;------------------------------------------------------------ ;Move Sprite Down ;expect x as sprite index (0 to 7) ;------------------------------------------------------------ !zone MoveSpriteDown MoveSpriteDown inc SPRITE_POS_Y,x txa asl tay lda SPRITE_POS_Y,x sta 53249,y rts step4.zip

Previous Step 3 Next Step 5

Endurion

Endurion

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!