Jump to content
  • Advertisement
  • entries
    104
  • comments
    103
  • views
    262181

About this blog

Musings of a hobbyist

Entries in this blog

 

A C64 Game - Step 53

Here comes our first boss. Nothing too difficult, but different. Let's see what this works out to



The boss moves quite similar to the ghost however he's got a special attack. If nothing else happens the boss is homing in on you. If you shoot him two times he goes into attack mode (and you better step back).

For performance reason the beams are made of characters, a horizontal and vertical line at the boss position. ;------------------------------------------------------------ ;boss ;------------------------------------------------------------ !zone BehaviourBoss BehaviourBoss BOSS_MOVE_SPEED = 1 lda SPRITE_HITBACK,x beq .NoHitBack dec SPRITE_HITBACK,x ldy SPRITE_HITBACK,x lda BOSS_FLASH_TABLE,y sta VIC_SPRITE_COLOR,x cpy #0 bne .NoHitBack ;make vulnerable again lda SPRITE_STATE,x cmp #128 bne .NoHitBack lda #0 sta SPRITE_STATE,x .NoHitBack lda DELAYED_GENERIC_COUNTER and #$03 bne .NoAnimUpdate lda SPRITE_POINTER_BASE,x eor #$01 sta SPRITE_POINTER_BASE,x .NoAnimUpdate lda SPRITE_STATE,x and #$7f bne .NotFollowPlayer jmp .FollowPlayer .NotFollowPlayer cmp #1 beq .AttackMode rts .AttackMode ;Attack modes (more modes?) inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x cmp #4 beq .NextAttackStep rts .NextAttackStep lda #0 sta SPRITE_MOVE_POS,x inc SPRITE_MODE_POS,x lda SPRITE_MODE_POS,x cmp #11 bcc .BeamNotDangerous cmp #29 bcs .BeamNotDangerous ;does player hit beam? ldy #0 jsr CheckIsPlayerCollidingWithBeam ldy #1 jsr CheckIsPlayerCollidingWithBeam .BeamNotDangerous lda SPRITE_MODE_POS,x cmp #11 beq .BeamStep1 cmp #12 beq .BeamStep2 cmp #13 beq .BeamStep3 cmp #16 beq .BeamStep4 cmp #17 beq .BeamStep3 cmp #18 beq .BeamStep4 cmp #19 beq .BeamStep3 cmp #20 beq .BeamStep4 cmp #21 beq .BeamStep3 cmp #22 beq .BeamStep4 cmp #23 beq .BeamStep3 cmp #24 beq .BeamStep4 cmp #25 beq .BeamStep3 cmp #26 beq .BeamStep4 cmp #27 beq .BeamStep3 cmp #28 beq .BeamStep4 cmp #29 beq .BeamStep3 cmp #30 beq .BeamEnd rts .BeamStep1 ;beam lda #BEAM_TYPE_DARK jsr .DrawBeam rts .BeamStep2 ;beam lda #BEAM_TYPE_MEDIUM jsr .DrawBeam rts .BeamStep3 ;beam lda #BEAM_TYPE_LIGHT jsr .DrawBeam rts .BeamStep4 ;beam lda #BEAM_TYPE_LIGHT2 jsr .DrawBeam rts .BeamEnd jsr .RestoreBeam lda #0 sta SPRITE_STATE,x rts .DrawBeam tay lda BEAM_CHAR_H,y sta PARAM1 lda BEAM_CHAR_V,y sta PARAM2 lda BEAM_COLOR,y sta PARAM3 ldy SPRITE_CHAR_POS_Y,x lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 sta ZEROPAGE_POINTER_2 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 ) sta ZEROPAGE_POINTER_2 + 1 stx PARAM6 ldy #1 .HLoop lda PARAM1 sta (ZEROPAGE_POINTER_1),y lda PARAM3 sta (ZEROPAGE_POINTER_2),y iny cpy #39 bne .HLoop ;vertical beam ldy SPRITE_CHAR_POS_X,x ldx #1 .NextLine lda SCREEN_LINE_OFFSET_TABLE_LO,x sta ZEROPAGE_POINTER_1 sta ZEROPAGE_POINTER_2 lda SCREEN_LINE_OFFSET_TABLE_HI,x sta ZEROPAGE_POINTER_1 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 ) sta ZEROPAGE_POINTER_2 + 1 lda PARAM2 sta (ZEROPAGE_POINTER_1),y lda PARAM3 sta (ZEROPAGE_POINTER_2),y inx cpx #22 bne .NextLine ldx PARAM6 rts .RestoreBeam ldy SPRITE_CHAR_POS_Y,x lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 sta ZEROPAGE_POINTER_4 lda SCREEN_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 sec sbc #( ( SCREEN_CHAR - SCREEN_BACK_CHAR ) >> 8 ) sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) >> 8 ) sta ZEROPAGE_POINTER_3 + 1 sec sbc #( ( SCREEN_COLOR - SCREEN_BACK_COLOR ) >> 8 ) sta ZEROPAGE_POINTER_4 + 1 stx PARAM6 ldy #1 - lda (ZEROPAGE_POINTER_2),y sta (ZEROPAGE_POINTER_1),y lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_3),y iny cpy #39 bne - ;vertical beam ldy SPRITE_CHAR_POS_X,x ldx #1 .NextLineR lda SCREEN_LINE_OFFSET_TABLE_LO,x sta ZEROPAGE_POINTER_1 sta ZEROPAGE_POINTER_2 sta ZEROPAGE_POINTER_3 sta ZEROPAGE_POINTER_4 lda SCREEN_LINE_OFFSET_TABLE_HI,x sta ZEROPAGE_POINTER_1 + 1 clc adc #( ( SCREEN_BACK_CHAR - SCREEN_CHAR ) >> 8 ) sta ZEROPAGE_POINTER_2 + 1 clc adc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) >> 8 ) sta ZEROPAGE_POINTER_3 + 1 sec sbc #( ( SCREEN_COLOR - SCREEN_BACK_COLOR ) >> 8 ) sta ZEROPAGE_POINTER_4 + 1 lda (ZEROPAGE_POINTER_2),y sta (ZEROPAGE_POINTER_1),y lda (ZEROPAGE_POINTER_4),y sta (ZEROPAGE_POINTER_3),y inx cpx #22 bne .NextLineR ldx PARAM6 rts .FollowPlayer inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #10 beq .DoCheckMove jmp .DoGhostMove .DoCheckMove lda #0 sta SPRITE_ANIM_DELAY,x txa and #$01 tay lda SPRITE_ACTIVE,y cmp #TYPE_PLAYER_DEAN beq .FoundPlayer cmp #TYPE_PLAYER_SAM beq .FoundPlayer ;check other player tya eor #1 tay lda SPRITE_ACTIVE,y cmp #TYPE_PLAYER_DEAN beq .FoundPlayer cmp #TYPE_PLAYER_SAM beq .FoundPlayer ;no player to hunt rts .FoundPlayer ;player index in y lda SPRITE_CHAR_POS_X,y cmp SPRITE_CHAR_POS_X,x bpl .MoveRight ;move left lda SPRITE_DIRECTION,x bne .AlreadyLookingLeft lda SPRITE_MOVE_POS,x beq .TurnLNow dec SPRITE_MOVE_POS,x bne .CheckYNow .TurnLNow ;turning now lda #1 sta SPRITE_DIRECTION,x lda #SPRITE_BOSS_L_1 sta SPRITE_POINTER_BASE,x jmp .CheckYNow .AlreadyLookingLeft lda SPRITE_MOVE_POS,x cmp #BOSS_MOVE_SPEED beq .CheckYNow inc SPRITE_MOVE_POS,x jmp .CheckYNow .MoveRight lda SPRITE_DIRECTION,x beq .AlreadyLookingRight lda SPRITE_MOVE_POS,x beq .TurnRNow dec SPRITE_MOVE_POS,x bne .CheckYNow ;turning now .TurnRNow lda #0 sta SPRITE_DIRECTION,x lda #SPRITE_BOSS_R_1 sta SPRITE_POINTER_BASE,x jmp .CheckYNow .AlreadyLookingRight lda SPRITE_MOVE_POS,x cmp #BOSS_MOVE_SPEED beq .CheckYNow inc SPRITE_MOVE_POS,x jmp .CheckYNow .CheckYNow ;player index in y lda SPRITE_CHAR_POS_Y,y cmp SPRITE_CHAR_POS_Y,x bpl .MoveDown ;move left lda SPRITE_DIRECTION_Y,x bne .AlreadyLookingUp lda SPRITE_MOVE_POS_Y,x beq .TurnUNow dec SPRITE_MOVE_POS_Y,x bne .DoGhostMove .TurnUNow ;turning now lda #1 sta SPRITE_DIRECTION_Y,x jmp .DoGhostMove .AlreadyLookingUp lda SPRITE_MOVE_POS_Y,x cmp #BOSS_MOVE_SPEED beq .DoGhostMove inc SPRITE_MOVE_POS_Y,x jmp .DoGhostMove .MoveDown lda SPRITE_DIRECTION_Y,x beq .AlreadyLookingDown lda SPRITE_MOVE_POS_Y,x beq .TurnDNow dec SPRITE_MOVE_POS_Y,x bne .DoGhostMove ;turning now .TurnDNow lda #0 sta SPRITE_DIRECTION_Y,x jmp .DoGhostMove .AlreadyLookingDown lda SPRITE_MOVE_POS_Y,x cmp #BOSS_MOVE_SPEED beq .DoGhostMove inc SPRITE_MOVE_POS_Y,x jmp .DoGhostMove .DoGhostMove ;move X times ldy SPRITE_MOVE_POS,x sty PARAM4 beq .DoY lda SPRITE_DIRECTION,x beq .DoRight .MoveLoopL jsr ObjectMoveLeftBlocking dec PARAM4 bne .MoveLoopL jmp .DoY .DoRight .MoveLoopR jsr ObjectMoveRightBlocking dec PARAM4 bne .MoveLoopR .DoY ;move X times ldy SPRITE_MOVE_POS_Y,x sty PARAM4 beq .MoveDone lda SPRITE_DIRECTION_Y,x beq .DoDown .MoveLoopU jsr ObjectMoveUpBlocking dec PARAM4 bne .MoveLoopU jmp .MoveDone .DoDown .MoveLoopD jsr ObjectMoveDownBlockingNoPlatform dec PARAM4 bne .MoveLoopD .MoveDone rts
Checking the player for collision with the beam is heavily simplified due to the beam being horizontal/vertical. It's a cheap comparison of position values: ;------------------------------------------------------------ ;check player vs. beam ; beam boss index in x ; player index in y ;------------------------------------------------------------ !zone CheckIsPlayerCollidingWithBeam CheckIsPlayerCollidingWithBeam lda SPRITE_ACTIVE,y bne .PlayerIsActive .PlayerNotActive rts .PlayerIsActive lda SPRITE_STATE,y cmp #128 bcs .PlayerNotActive ;compare char positions in x lda SPRITE_CHAR_POS_X,x cmp SPRITE_CHAR_POS_X,y beq .PlayerHit clc adc #1 cmp SPRITE_CHAR_POS_X,y beq .PlayerHit sec sbc #2 cmp SPRITE_CHAR_POS_X,y beq .PlayerHit ;compare char positions in y lda SPRITE_CHAR_POS_Y,x cmp SPRITE_CHAR_POS_Y,y beq .PlayerHit clc adc #1 cmp SPRITE_CHAR_POS_Y,y beq .PlayerHit sec sbc #2 cmp SPRITE_CHAR_POS_Y,y beq .PlayerHit ;not hit rts .PlayerHit ;player killed lda #129 sta SPRITE_STATE,y lda #SPRITE_PLAYER_DEAD sta SPRITE_POINTER_BASE,y lda #0 sta SPRITE_MOVE_POS,y lda SPRITE_ACTIVE,y cmp #TYPE_PLAYER_SAM bne .PlayerWasDean ;reset Sam specific variables lda #0 sta SPRITE_HELD .PlayerWasDean rts
The boss only enters attack mode for every second hit, therefore he gets a special treatment in his hit function: ;------------------------------------------------------------ ;hit behaviour for boss ;------------------------------------------------------------ !zone HitBehaviourBoss HitBehaviourBoss lda #8 sta SPRITE_HITBACK,x ;make invincible for a short while lda SPRITE_STATE,x ora #$80 sta SPRITE_STATE,x ;boss switches tactic lda SPRITE_HP,x and #$01 beq .SwitchToAttack rts .SwitchToAttack lda #129 sta SPRITE_STATE,x lda #0 sta SPRITE_MODE_POS,x sta SPRITE_MOVE_POS,x sta SPRITE_MOVE_POS_Y,x rts
The less interesting parts are known, add new values for the boss object plus the color/character tables for the beam. step53.zip
Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 52

To make spawning enemies a bit more player friendly spawns are now shown before the enemy actually appears.




The way to implement this is quite simple. We've got a neat object system running already, so we make the spawn animation just another type.
Once the spawn life time is up the object is replaced in spot with the proper object type.

During a spawn point process we store the target type in SPRITE_ANNOYED (since spawns do not get annoyed): ;store spawn type in SPRITE_ANNOYED ldx PARAM7 lda PARAM5 sta SPRITE_ANNOYED,x
The spawns behaviour is straight forward, animate, count life time down and finally spawn the final object: ;------------------------------------------------------------ ;Spawn ;------------------------------------------------------------ !zone BehaviourSpawn BehaviourSpawn inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #3 beq .UpdateAnimation rts .UpdateAnimation lda #0 sta SPRITE_ANIM_DELAY,x lda SPRITE_POINTER_BASE,x eor #$01 sta SPRITE_POINTER_BASE,x inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x cmp #20 beq .SpawnNow rts .SpawnNow lda SPRITE_ANNOYED,x sta PARAM3 lda SPRITE_CHAR_POS_X,x sta PARAM1 lda SPRITE_CHAR_POS_Y,x sta PARAM2 stx PARAM7 lda #1 jsr SpawnObject ldx PARAM7 rts
The rest is too simple to show it here line by line, add new constants for the sprite, add the entry to the behaviour and hurt tables, add entries to the type start tables (color, sprite, etc.) and we're done.

And now the player is not killed by suddenly appearing enemies. step52.zip
Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 51

More cooperative hodge podge. An experimental feature, both players are needed to kill an enemy. Sam must hold the enemy, so Dean can shoot it.



To accomodate this the changes are quite simple:

First block the hurting of enemies when they are held by Sam: ldy SPRITE_HELD dey ldx SPRITE_ACTIVE,y lda IS_TYPE_ENEMY,x cmp #2 bne .NormalHurtByForce ;in 2p mode? ;TODO - if only one player is left? lda GAME_MODE cmp #2 bne .NormalHurtByForce ;no further action jmp .NoEnemyHeld .NormalHurtByForce ldx PARAM6
Then, when Dean's bullet hits, check if the play mode is 2 player, and the enemy is actually held by Sam: ;is two player enemy? ldy SPRITE_ACTIVE,x lda IS_TYPE_ENEMY,y cmp #2 bne .HitEnemy ;in 2p mode? ;TODO - if only one player is left? lda GAME_MODE cmp #2 bne .HitEnemy ldy SPRITE_HELD dey sty PARAM1 cpx PARAM1 beq .HitEnemy ;enemy would be hit, but is not held jmp .ShotDone .HitEnemy
And obviously, when an enemy is hurt, release Sam's lock: lda SPRITE_HELD sta PARAM1 dec PARAM1 cpx PARAM1 bne .NotHeldEnemy lda #0 sta SPRITE_HELD .NotHeldEnemy
As you can see, there're still TODOs left. Also, if 2 player mode makes this behaviour general or only for special enemies is yet to be decided. step51.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 50

In this step the powerups for range increase and decrease reload delay are made permanent. They won't fade with time.
You can also collect up to five extras to reach the maximum. And the powerup stays even when you get killed. (Don't you hate it when you die in Bubble Bobble and you're slow again).



Previously we had a flag that stored the time left of a faster reload. The max times are now kept in a table, and a faster reload step value is stored instead.

The speed table RELOAD_SPEED_MAX and other counters for this update: PLAYER_RELOAD_SPEED !byte 0 RELOAD_SPEED !byte 1,1,1,1,1 RELOAD_SPEED_MAX !byte 40,35,30,25,20
Initialising on game restart: lda #0 sta PLAYER_RELOAD_SPEED
During standing still the reload speed value is now used to count down the time. ldy PLAYER_RELOAD_SPEED lda PLAYER_STAND_STILL_TIME clc adc RELOAD_SPEED,y cmp RELOAD_SPEED_MAX,y bcs .ReloadTimeDone sta PLAYER_STAND_STILL_TIME jmp .HandleFire .ReloadTimeDone lda #0 sta PLAYER_STAND_STILL_TIME
During pickup of the increase reload speed the counter needs to be updated: lda PLAYER_RELOAD_SPEED cmp #4 beq .SpeedHighestAlready inc PLAYER_RELOAD_SPEED .SpeedHighestAlready
Making the force range increase permanent is even easier, we simply remove all instances where it was reset to the start value on respawning and starting the next level. step50.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 49

Bug fixing time. Don't ever let the bug count get out of hand.

Most enemies wouldn't have a proper hit back effect. Also, some sprites are moving inside the floor visually. We'll fix that in this step.



Hit back works the same for all enemies, so let's make it a new sub routine. The routine is to be called at the start of an enemies custom behaviour code: ;------------------------------------------------------------ ;handles simple hitback ;------------------------------------------------------------ !zone HandleHitBack HandleHitBack lda SPRITE_HITBACK,x beq .NoHitBack dec SPRITE_HITBACK,x lda SPRITE_HITBACK_DIRECTION,x beq .HitBackRight ;move left jsr ObjectMoveLeftBlocking lda #1 rts .HitBackRight jsr ObjectMoveRightBlocking lda #1 rts .NoHitBack lda #0 rts
For every behaviour routine we add this in: jsr HandleHitBack beq .NoHitBack rts .NoHitBack
Fortunately, to fix the sprite offset problem we already have the solution in form of a start offset table. We just amend some values (note all the new entries with value 2): TYPE_START_DELTA_Y !byte 0 ;dummy !byte 0 ;player dean !byte 0 ;bat 1 !byte 0 ;bat 1 !byte 0 ;bat 2 !byte 0 ;mummy !byte 0 ;zombie !byte 0 ;nasty bat !byte 0 ;spider !byte 0 ;explosion !byte 0 ;player sam !byte 2 ;wolf !byte 0 ;ghost skeleton !byte 2 ;jumping toad !byte 0 ;eye !byte 0 ;floating ghost !byte 0 ;fly !byte 2 ;slime !byte 2 ;frankenstein !byte 2 ;hand !byte 2 ;devil !byte 0 ;impala 1 !byte 0 ;impala 2 !byte 0 ;impala 3 !byte 0 ;impala driver !byte 0 ;impala debris
step49.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 48

This update doesn't really add anything mentionable code wise, however there's a full chapter of 10 stages.

Have fun!

step48.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 47

Poor Sam got nerfed. Now his force range starts out really low (about 5 characters), but can be improved by picking the proper extra. And finally cheat keys appear. Press 1 to advance to the next stage.



We modify the existing SamUseForce routine to check for a max range (stored in PLAYER_FORCE_RANGE): ;------------------------------------------------------------ ;sam uses power ;returns 1 when holding an enemy ;------------------------------------------------------------ !zone SamUseForce SamUseForce lda SPRITE_HELD beq .NoSpriteHeldNow lda #1 rts .NoSpriteHeldNow stx PARAM6 ldy SPRITE_CHAR_POS_Y,x dey lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ldy SPRITE_CHAR_POS_X,x lda #0 sta PARAM7 .ShotContinue lda PARAM7 cmp PLAYER_FORCE_RANGE beq .OutOfRange inc PARAM7 ;y contains shot X pos ;PARAM6 contains x sprite index of player ldx PARAM6 lda SPRITE_DIRECTION,x beq .ShootRight ;shooting left dey lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking beq .CheckHitEnemy ldx PARAM6 .ShotDoneMiss .OutOfRange lda #0 .ShotDoneHit rts .ShootRight iny lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .ShotDoneMiss .CheckHitEnemy ;hit an enemy? ldx #0 .CheckEnemy stx PARAM2 sty PARAM1 lda SPRITE_ACTIVE,x beq .CheckNextEnemy tax lda IS_TYPE_ENEMY,x beq .CheckNextEnemy ldx PARAM2 ;is vulnerable? lda SPRITE_STATE,x cmp #128 bpl .CheckNextEnemy ;sprite pos matches on x? lda SPRITE_CHAR_POS_X,x cmp PARAM1 bne .CheckNextEnemy ;sprite pos matches on y? ldy PARAM6 lda SPRITE_CHAR_POS_Y,x cmp SPRITE_CHAR_POS_Y,y beq .EnemyHit ;sprite pos matches on y + 1? clc adc #1 cmp SPRITE_CHAR_POS_Y,y beq .EnemyHit ;sprite pos matches on y - 1? sec sbc #2 cmp SPRITE_CHAR_POS_Y,y bne .CheckNextEnemy .EnemyHit ;enemy hit! stx SPRITE_HELD inc SPRITE_HELD ;call enemy hit behaviour ldy SPRITE_ACTIVE,x ;enemy is active dey dey lda ENEMY_HIT_BEHAVIOUR_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda ENEMY_HIT_BEHAVIOUR_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ;set up return address for rts lda #>( .ShotDoneHit - 1 ) pha lda #<( .ShotDoneHit - 1 ) pha ;1 as return value lda #1 jmp (ZEROPAGE_POINTER_1) .CheckNextEnemy ldx PARAM2 ldy PARAM1 inx cpx #8 beq .NoEnemyHit jmp .CheckEnemy .NoEnemyHit jmp .ShotContinue
If the extra is picked up the range is simply increased to a max of 38 (Remember, screen width is 40 characters): .EffectIncForceRange cpx #0 beq .DeanDoesNotUseForce lda PLAYER_FORCE_RANGE clc adc #2 sta PLAYER_FORCE_RANGE cmp #38 bcs .NotTooLong lda #38 sta PLAYER_FORCE_RANGE .NotTooLong jmp .RemoveItem

Adding cheat keys is way easier. Since the Kernal (yes, with an 'a') comes with a keyboard check routine we just call that. Notice that for this to work you must not have the Kernal disabled (remember the memory layout in the beginning): JSR $FFE4 ;GETIN BEQ .NOCHEAT CMP #49 bne .NOCHEAT ;jump to next level jsr StartLevel inc LEVEL_NR jsr BuildScreen jsr CopyLevelToBackBuffer jsr DisplayGetReady .NOCHEAT

And still something new: Element Areas. A new primitive that fills an area with m/n repeats of an element: !zone LevelElementArea LevelElementArea ;!byte LD_ELEMENT_AREA,24,16,5,1,EL_SN_BROWN_ROCK ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 sta PARAM10 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;x count iny lda (ZEROPAGE_POINTER_1),y sta PARAM7 sta PARAM9 ;y count iny lda (ZEROPAGE_POINTER_1),y sta PARAM8 ;type iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;store y for later tya pha .NextElementRow jsr DrawLevelElement dec PARAM7 beq .RowDone lda PARAM1 clc adc PARAM4 sta PARAM1 jmp .NextElementRow .RowDone lda PARAM2 clc adc PARAM5 sta PARAM2 lda PARAM9 sta PARAM7 lda PARAM10 sta PARAM1 dec PARAM8 bne .NextElementRow jmp NextLevelData
step47.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 46

This time we'll add a chapter intro. The game is supposed to be separated in themed chapters with an intro for each. There's nothing much to it, we'll display a short text while the boys drive to their new target.



For now we simply show the first page after starting the game: lda #0 jsr ShowStory
Most of the code is spent to actually setup the impala and driver sprites. The text itself is displayed by our trusty DisplayText routine. Followed by the obligatory wait for button press and release. ;------------------------------------------------------------ ;story pages ;------------------------------------------------------------ !zone ShowStory ShowStory ;clear screen lda #32 ldy #1 jsr ClearScreen lda # sta ZEROPAGE_POINTER_1 lda #>TEXT_STORY_1 sta ZEROPAGE_POINTER_1 + 1 lda #1 sta PARAM1 lda #1 sta PARAM2 jsr DisplayText lda #41 sta PARAM1 lda #20 sta PARAM2 lda #TYPE_IMPALA_1 sta PARAM3 jsr FindEmptySpriteSlot jsr SpawnObject lda #44 sta PARAM1 lda #TYPE_IMPALA_DRIVER sta PARAM3 jsr FindEmptySpriteSlot jsr SpawnObject lda #44 sta PARAM1 lda #TYPE_IMPALA_2 sta PARAM3 jsr FindEmptySpriteSlot jsr SpawnObject lda #47 sta PARAM1 lda #TYPE_IMPALA_3 sta PARAM3 jsr FindEmptySpriteSlot jsr SpawnObject lda #48 sta PARAM1 lda #TYPE_IMPALA_DEBRIS sta PARAM3 jsr FindEmptySpriteSlot jsr SpawnObject lda #12 sta VIC_SPRITE_MULTICOLOR_1 lda #11 sta VIC_SPRITE_MULTICOLOR_2 lda #0 sta BUTTON_RELEASED .StoryLoop jsr WaitFrame jsr ObjectControl ;ldx #0 ;jsr MoveSpriteLeft ;inx ;jsr MoveSpriteLeft ;inx ;jsr MoveSpriteLeft ;inx ;jsr MoveSpriteLeft ;inx ;jsr MoveSpriteLeft lda #$10 bit JOYSTICK_PORT_II bne .ButtonNotPressed ;button pushed lda BUTTON_RELEASED beq .StoryLoop lda #0 sta VIC_SPRITE_ENABLE lda #11 sta VIC_SPRITE_MULTICOLOR_1 lda #1 sta VIC_SPRITE_MULTICOLOR_2 rts .ButtonNotPressed lda #1 sta BUTTON_RELEASED jmp .StoryLoop

The impala objects are simple game objects with the same behaviour. Move to the center, wait for a while and then move off the left side. ;------------------------------------------------------------ ;drive left/pause/drive off left ;------------------------------------------------------------ !zone BehaviourImpala BehaviourImpalaDebris inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x and #$04 lsr lsr clc adc #SPRITE_DEBRIS_1 sta SPRITE_POINTER_BASE,x BehaviourImpala lda SPRITE_STATE,x beq .DriveFirstHalf cmp #1 beq .HandlePause ;drive off jsr MoveSpriteLeft lda SPRITE_POS_X,x beq .DriveDone rts .DriveDone jsr RemoveObject rts .DriveFirstHalf jsr MoveSpriteLeft inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x cmp #200 beq .NextState rts .NextState inc SPRITE_STATE,x lda #0 sta SPRITE_MOVE_POS,x rts .HandlePause inc SPRITE_MOVE_POS,x beq .NextState rts
The text is stored as usual. Nice to see, a - acts as CR, * as end of text. TEXT_STORY_1 !text "A LOCAL NEWSPAPER MENTIONS SEVERAL-" !text "MISSING PEOPLE",59," THIS SEEMS TO BE A-" !text "RECURRING PATTERN EVERY 44 YEARS",59,"-" !text "WE SHOULD INVESTIGATE THE TOWN-" !text "CEMETARY",59,"*"
step46.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 45

In this version nothing much is added to the code. However an external level editor was added (Windows executable) which helps a lot in churning out pretty levels faster.




Most prominent addition are level elements. Before the level was built mostly of simple primitives (line of an character, etc.). With the editor so called Elements are added. These consist of a variable sized character and color block. Elements can be arranged as single object, lines or areas. This helps a lot in reusing bigger level parts and keeping memory usage down.

The elements are stored in several tables. A major lookup table that points to a elements character and color tables, and two lookup tables holding an elements width and height. The editor tries to fold element tables into each other to save memory. For example if there's a big brick sized 4x2 characters, and you have two smaller elements showing the left and right halfs of the brick, the element data is reused.

Note that the element area code is not implemented in this step.

Moral of the story:
As soon as you see you're going ahead with a project having an easy to use editor is quite important. It aids you in faster content creation, faster testing and overall usually prettier results. Nothing crushes productivity better than annoying tools (or manual boring work without tools). ;------------------------------------------------------------ ;draws a level element ;PARAM1 = X ;PARAM2 = Y ;PARAM3 = TYPE ;returns element width in PARAM4 ;returns element height in PARAM5 ;------------------------------------------------------------ !zone DrawLevelElement DrawLevelElement ldy PARAM3 lda SNELEMENT_TABLE_LO,y sta .LoadCode + 1 lda SNELEMENT_TABLE_HI,y sta .LoadCode + 2 lda SNELEMENT_COLOR_TABLE_LO,y sta .LoadCodeColor + 1 lda SNELEMENT_COLOR_TABLE_HI,y sta .LoadCodeColor + 2 lda SNELEMENT_WIDTH_TABLE,y sta PARAM4 lda SNELEMENT_HEIGHT_TABLE,y sta PARAM5 sta PARAM6 ldy PARAM2 lda SCREEN_LINE_OFFSET_TABLE_LO,y clc adc PARAM1 sta .StoreCode + 1 sta .StoreCodeColor + 1 sta ZEROPAGE_POINTER_4 lda SCREEN_LINE_OFFSET_TABLE_HI,y adc #0 sta .StoreCode + 2 adc #( ( >SCREEN_COLOR ) - ( >SCREEN_CHAR ) ) sta .StoreCodeColor + 2 .NextRow ldx #0 ;display a row .Row .LoadCode lda $8000,x .StoreCode sta $8000,x .LoadCodeColor lda $8000,x .StoreCodeColor sta $8000,x inx cpx PARAM4 bne .Row ;eine zeile nach unten dec PARAM6 beq .ElementDone ;should be faster? lda .LoadCode + 1 clc adc PARAM4 sta .LoadCode + 1 lda .LoadCode + 2 adc #0 sta .LoadCode + 2 lda .LoadCodeColor + 1 clc adc PARAM4 sta .LoadCodeColor + 1 lda .LoadCodeColor + 2 adc #0 sta .LoadCodeColor + 2 lda .StoreCode + 1 clc adc #40 sta .StoreCode + 1 lda .StoreCode + 2 adc #0 sta .StoreCode + 2 lda .StoreCodeColor + 1 clc adc #40 sta .StoreCodeColor + 1 lda .StoreCodeColor + 2 adc #0 sta .StoreCodeColor + 2 jmp .NextRow .ElementDone rts !zone LevelElement LevelElement LevelElementArea ; !byte LD_ELEMENT,0,0,EL_BLUE_BRICK_4x3 ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;type iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;store y for later tya pha jsr DrawLevelElement jmp NextLevelData
The element line primitives are very similar, they just loop over the element draw routine: !zone LevelElementH LevelElementH ; !byte LD_ELEMENT_LINE_H,x,y,width,element ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;x count iny lda (ZEROPAGE_POINTER_1),y sta PARAM7 ;type iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;store y for later tya pha .NextElement jsr DrawLevelElement dec PARAM7 beq .Done lda PARAM1 clc adc PARAM4 sta PARAM1 jmp .NextElement .Done jmp NextLevelData !zone LevelElementV LevelElementV ; !byte LD_ELEMENT_LINE_V,x,y,num,element ;X pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM1 ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta PARAM2 ;y count iny lda (ZEROPAGE_POINTER_1),y sta PARAM7 ;type iny lda (ZEROPAGE_POINTER_1),y sta PARAM3 ;store y for later tya pha .NextElement jsr DrawLevelElement dec PARAM7 beq .Done lda PARAM2 clc adc PARAM5 sta PARAM2 jmp .NextElement .Done jmp NextLevelData
The editor exports the level structure to a separate file, this is then included in the main file via the !source macro. step45.zip Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 44

And yet more enemies, an underground hand and a devil skeleton. Nothing spectacularely new.



The devil skeleton is behaving mostly like the mummy. Walk back and forth, if a player is seen, speed towards him. !zone BehaviourDevil BehaviourDevil lda SPRITE_HITBACK,x beq .NoHitBack dec SPRITE_HITBACK,x lda SPRITE_HITBACK_DIRECTION,x beq .HitBackRight ;move left jsr ObjectMoveLeftBlocking rts .HitBackRight jsr ObjectMoveRightBlocking rts .NoHitBack jsr ObjectMoveDownBlocking beq .NotFalling rts .NotFalling inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #8 bne .NoAnimUpdate lda #0 sta SPRITE_ANIM_DELAY,x inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x cmp #3 bne .NoWrap lda #0 .NoWrap sta SPRITE_ANIM_POS,x clc asl adc SPRITE_DIRECTION,x adc #SPRITE_DEVIL_WALK_R_1 sta SPRITE_POINTER_BASE,x .NoAnimUpdate lda SPRITE_CHAR_POS_Y,x cmp SPRITE_CHAR_POS_Y bne .NoPlayerInSight ;player on same height ;looking at the player? jsr LookingAtPlayer beq .NoPlayerInSight lda SPRITE_DIRECTION,x beq .AttackRight ;attack to left jsr ObjectMoveLeftBlocking jsr ObjectMoveLeftBlocking beq .ToggleDirection rts .AttackRight ;attack to left jsr ObjectMoveRightBlocking jsr ObjectMoveRightBlocking beq .ToggleDirection rts .NoPlayerInSight lda DELAYED_GENERIC_COUNTER and #$03 beq .MovementUpdate rts .MovementUpdate inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x and #$03 sta SPRITE_MOVE_POS,x lda SPRITE_DIRECTION,x beq .MoveRight ;move left jsr ObjectWalkLeft beq .ToggleDirection rts .MoveRight jsr ObjectWalkRight beq .ToggleDirection rts .ToggleDirection lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x clc adc #SPRITE_DEVIL_WALK_R_1 sta SPRITE_POINTER_BASE,x rts

The hand is a bit more interesting. It is stationary, but usually stays underground. In intervals the hand rises and tries to grab at the player. ;------------------------------------------------------------ ;simply appear and hide again ;state 128 = invisible ; 0 = rising/hiding ;------------------------------------------------------------ !zone BehaviourHand BehaviourHand lda DELAYED_GENERIC_COUNTER and #$03 beq .MovementUpdate .NoMovement rts .MovementUpdate lda SPRITE_STATE,x bne .HiddenState inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #3 bne .NoMovement lda #0 sta SPRITE_ANIM_DELAY,x inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x cmp #6 beq .EnterHiddenState .UpdateHandSprite ldy SPRITE_ANIM_POS,x lda HAND_ANIM_TABLE,y sta SPRITE_POINTER_BASE,x lda HAND_COLOR_TABLE,y sta VIC_SPRITE_COLOR,x rts .EnterHiddenState lda #SPRITE_INVISIBLE sta SPRITE_POINTER_BASE,x jsr GenerateRandomNumber sta SPRITE_MOVE_POS,x lda #128 sta SPRITE_STATE,x .StillHidden rts .HiddenState dec SPRITE_MOVE_POS,x bne .StillHidden ;unhiding lda #0 sta SPRITE_STATE,x sta SPRITE_ANIM_DELAY,x sta SPRITE_ANIM_POS,x jmp .UpdateHandSprite
Note that also for the hand the empty sprite comes to use. It's basically an empty image to avoid disabling and enabling the sprite. This however comes at the cost of 63 bytes for the image. step44.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 43

And another two new enemies, Frankenstein's monster and a slime.






Frankenstein's monster behaves very much like zombies, but is a bit stronger.
  ;------------------------------------------------------------ ;simply walk left/right, do not fall off ;state 128 = invisible ; 1 = rising ; 0 = moving ; 2 = collapsing ;------------------------------------------------------------ !zone BehaviourFrankenstein BehaviourFrankenstein lda SPRITE_HITBACK,x beq .NoHitBack dec SPRITE_HITBACK,x lda SPRITE_HITBACK_DIRECTION,x beq .HitBackRight ;move left jsr ObjectMoveLeftBlocking rts .HitBackRight jsr ObjectMoveRightBlocking rts .NoHitBack lda SPRITE_JUMP_POS,x bne .IsJumping jsr ObjectMoveDownBlocking bne .Falling .IsJumping lda DELAYED_GENERIC_COUNTER and #$03 beq .MovementUpdate .NoMovement rts .Falling lda DELAYED_GENERIC_COUNTER and #$03 bne .NoMovement jmp .WalkWithoutAnimation .MovementUpdate lda SPRITE_JUMP_POS,x bne .UpdateJump lda SPRITE_STATE,x bne .OtherStates ;moving jsr GenerateRandomNumber cmp #17 beq .Jump jmp .NormalWalk .OtherStates ;collapsing? cmp #2 beq .Collapsing cmp #1 beq .Rising cmp #128 bne .NotHidden jmp .Hidden .NotHidden rts .Jump ;start jump lda #SPRITE_FRANKIE_JUMP_R clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x .UpdateJump jsr UpdateSpriteJump ;still move jmp .WalkWithoutAnimation .NoUpdate rts .Collapsing inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #3 bne .NoUpdate lda #0 sta SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_POS,x beq .CollapseDone dec SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x clc asl adc #SPRITE_FRANKIE_RISE_R_1 adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x rts .CollapseDone ;on to hidden state lda #128 sta SPRITE_STATE,x lda #SPRITE_INVISIBLE sta SPRITE_POINTER_BASE,x ;generate hidden time jsr GenerateRandomNumber and #$31 clc adc #25 sta SPRITE_MOVE_POS,x ;normalise position on full char ldy SPRITE_CHAR_POS_X_DELTA,x sty PARAM5 .CheckXPos beq .XPosClear jsr ObjectMoveLeft dec PARAM5 jmp .CheckXPos .XPosClear ldy SPRITE_CHAR_POS_Y_DELTA,x sty PARAM5 .CheckYPos beq .YPosClear jsr ObjectMoveUp dec PARAM5 jmp .CheckYPos .YPosClear rts .Rising inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #3 bne .NoUpdate lda #0 sta SPRITE_ANIM_DELAY,x inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x cmp #3 beq .RiseDone clc asl adc #SPRITE_FRANKIE_RISE_R_1 adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x rts .RiseDone lda #SPRITE_FRANKIE_WALK_R_1 clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x lda #0 sta SPRITE_MOVE_POS,x sta SPRITE_ANIM_DELAY,x sta SPRITE_ANIM_POS,x sta SPRITE_STATE,x rts .NormalWalk inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #3 bne .NoAnimUpdate lda #0 sta SPRITE_ANIM_DELAY,x inc SPRITE_MOVE_POS,x .NoAnimUpdate lda SPRITE_MOVE_POS,x and #$03 sta SPRITE_MOVE_POS,x clc asl adc #SPRITE_FRANKIE_WALK_R_1 adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x .WalkWithoutAnimation lda SPRITE_DIRECTION,x beq .MoveRight ;move left jsr ObjectMoveLeftBlocking beq .ToggleDirection lda SPRITE_ANNOYED,x beq .NotAnnoyed jsr ObjectMoveLeftBlocking beq .ToggleDirection .NotAnnoyed rts .MoveRight jsr ObjectMoveRightBlocking beq .ToggleDirection lda SPRITE_ANNOYED,x beq .NotAnnoyed jsr ObjectMoveRightBlocking beq .ToggleDirection rts .ToggleDirection lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x rts .Hidden ;are we apt to wake up? dec SPRITE_MOVE_POS,x bne .RandomMove ;wake up lda #1 sta SPRITE_STATE,x lda #SPRITE_FRANKIE_RISE_R_1 clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x rts .RandomMove ;move randomly left/right jsr GenerateRandomNumber and #$01 beq .MoveLeft ;move right if possible jsr CanWalkRight beq .Blocked inc SPRITE_CHAR_POS_X,x ldy #8 sty PARAM5 .MoveSpriteRight jsr MoveSpriteRight dec PARAM5 bne .MoveSpriteRight rts .MoveLeft jsr CanWalkLeft beq .Blocked dec SPRITE_CHAR_POS_X,x ldy #8 sty PARAM5 .MoveSpriteLeft jsr MoveSpriteLeft dec PARAM5 bne .MoveSpriteLeft rts .Blocked rts
The slime sports new behaviour. Ducking and jumping through the stage trying to slime the player.
  ;------------------------------------------------------------ ;slime ;------------------------------------------------------------ !zone BehaviourSlime BehaviourSlime lda SPRITE_HITBACK,x beq .NoHitBack dec SPRITE_HITBACK,x lda SPRITE_HITBACK_DIRECTION,x beq .HitBackRight ;move left jsr ObjectMoveLeftBlocking rts .HitBackRight jsr ObjectMoveRightBlocking rts .NoHitBack ;state 0 = jumping ;state 1 = ducking ;state 2 = ducked ;state 3 = unducking lda SPRITE_STATE,x beq .SlimeJumping cmp #2 beq .SlimeDucked inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #6 bne .AnimPause lda #0 sta SPRITE_ANIM_DELAY,x ldy SPRITE_ANIM_POS,x inc SPRITE_ANIM_POS,x lda SPRITE_STATE,x cmp #3 beq .SlimeUnducking cpy #3 beq .DuckDone lda SLIME_DUCK_ANIMATION_TABLE,y clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x rts .DuckDone ;start ducked state lda #0 sta SPRITE_ANIM_POS,x lda #2 sta SPRITE_STATE,x jsr GenerateRandomNumber sta SPRITE_MOVE_POS,x .AnimPause rts .SlimeUnducking cpy #3 beq .UnduckDone lda SLIME_UNDUCK_ANIMATION_TABLE,y clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x rts .UnduckDone ;start jump lda #0 sta SPRITE_ANIM_POS,x sta SPRITE_STATE,x inc SPRITE_JUMP_POS,x rts .SlimeDucked dec SPRITE_MOVE_POS,x bne .StayDucked inc SPRITE_STATE,x .StayDucked rts .SlimeJumping lda SPRITE_JUMP_POS,x beq .FallIfPossible ;toad is jumping lda SPRITE_JUMP_POS,x cmp #TOAD_JUMP_TABLE_SIZE bne .JumpOn ;jump done jmp .JumpBlocked .JumpOn ldy SPRITE_JUMP_POS,x inc SPRITE_JUMP_POS,x lda TOAD_JUMP_TABLE,y bne .KeepJumping ;no jump movement needed jmp .SlimeMove .KeepJumping sta PARAM5 .JumpContinue jsr ObjectMoveUpBlocking beq .JumpBlocked dec PARAM5 bne .JumpContinue jmp .SlimeMove .JumpBlocked lda #0 sta SPRITE_JUMP_POS,x jmp .SlimeMove .FallIfPossible jsr UpdateSpriteFall beq .CanJump jmp .SlimeMove .CanJump inc SPRITE_STATE,x lda #0 sta SPRITE_ANIM_DELAY,x sta SPRITE_ANIM_POS,x lda SPRITE_DIRECTION,x beq .LookingRight lda #SPRITE_SLIME_L_1 sta SPRITE_POINTER_BASE,x rts .LookingRight lda #SPRITE_SLIME_R_1 sta SPRITE_POINTER_BASE,x rts ;simple move left/right .SlimeMove lda SPRITE_DIRECTION,x beq .MoveRight jsr ObjectMoveLeftBlocking beq .ToggleDirection rts .MoveRight jsr ObjectMoveRightBlocking beq .ToggleDirection rts .ToggleDirection lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x clc adc #SPRITE_SLIME_R_1 sta SPRITE_POINTER_BASE,x rts

Beside the new monsters a few fixes and features are added as well. For one the LookingAtPlayer function would only notice player 1. Now it works for both players:
  ;------------------------------------------------------------ ;determins if object is looking at player ;X = sprite index ;returns 1 if looking at player, 0 if not ;------------------------------------------------------------ !zone LookingAtPlayer LookingAtPlayer lda SPRITE_DIRECTION,x beq .LookingRight lda SPRITE_ACTIVE cmp #TYPE_PLAYER_DEAN bne .NotDean lda SPRITE_CHAR_POS_X,x cmp SPRITE_CHAR_POS_X bpl .LookingAtPlayer .NotDean lda SPRITE_ACTIVE + 1 cmp #TYPE_PLAYER_SAM bne .NoPlayerInSight lda SPRITE_CHAR_POS_X,x cmp SPRITE_CHAR_POS_X + 1 bpl .LookingAtPlayer jmp .NoPlayerInSight .LookingRight lda SPRITE_ACTIVE cmp #TYPE_PLAYER_DEAN bne .NotDeanR lda SPRITE_CHAR_POS_X,x cmp SPRITE_CHAR_POS_X bmi .LookingAtPlayer .NotDeanR lda SPRITE_ACTIVE + 1 cmp #TYPE_PLAYER_SAM bne .NoPlayerInSight lda SPRITE_CHAR_POS_X,x cmp SPRITE_CHAR_POS_X + 1 bmi .LookingAtPlayer jmp .NoPlayerInSight .LookingAtPlayer lda #1 rts .NoPlayerInSight lda #0 rts
And a little usability thing. Previously you could only change the game mode by pressing up in the title screen. Now it also works when pressing down:

  lda #$02 bit JOYSTICK_PORT_II bne .NotDownPressed lda DOWN_RELEASED beq .DownPressed lda GAME_MODE bne .NoGameModeWrap2 lda #3 sta GAME_MODE .NoGameModeWrap2 dec GAME_MODE ;redisplay game mode ldx GAME_MODE lda TEXT_GAME_MODE_LO,x sta ZEROPAGE_POINTER_1 lda TEXT_GAME_MODE_HI,x sta ZEROPAGE_POINTER_1 + 1 lda #11 sta PARAM1 lda #21 sta PARAM2 jsr DisplayText lda #0 jmp .DownPressed .NotDownPressed lda #1 .DownPressed sta DOWN_RELEASED   step43.zip
Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 42

So after a break last week we carry on. There were even more enemy sprites created, so here we go. New are the floating ghost and the fly.





The floating ghost is homing in on the player, but currently blocked by platforms downwards. To amend this we add a new routine. It's a clone of ObjectMoveDownBlocking, the only difference is the call to IsCharBlocking. ;------------------------------------------------------------ ;move object down if not blocked ;x = object index ;------------------------------------------------------------ !zone ObjectMoveDownBlockingNoPlatform ObjectMoveDownBlockingNoPlatform lda SPRITE_CHAR_POS_Y_DELTA,x beq .CheckCanMoveDown .CanMoveDown inc SPRITE_CHAR_POS_Y_DELTA,x lda SPRITE_CHAR_POS_Y_DELTA,x cmp #8 bne .NoCharStep lda #0 sta SPRITE_CHAR_POS_Y_DELTA,x inc SPRITE_CHAR_POS_Y,x .NoCharStep jsr MoveSpriteDown lda #1 rts .CheckCanMoveDown lda SPRITE_CHAR_POS_X_DELTA,x beq .NoSecondCharCheckNeeded ldy SPRITE_CHAR_POS_Y,x iny lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ldy SPRITE_CHAR_POS_X,x iny lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .BlockedDown .NoSecondCharCheckNeeded ldy SPRITE_CHAR_POS_Y,x iny lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ldy SPRITE_CHAR_POS_X,x lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .BlockedDown jmp .CanMoveDown .BlockedDown lda #0 rts
We had code for the ghost skeleton last week, but it was only animating. Now here comes the homing in part. The most annoying part is actually to decide which player to follow. For now it settles on one of the two, that may be subject to change later. ;------------------------------------------------------------ ;ghost skeleton ;------------------------------------------------------------ !zone BehaviourGhostSkeleton BehaviourGhostSkeleton GHOST_MOVE_SPEED = 1 lda DELAYED_GENERIC_COUNTER and #$03 bne .NoAnimUpdate inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x and #$03 sta SPRITE_ANIM_POS,x tay lda GHOST_SKELETON_ANIMATION_TABLE,y sta SPRITE_POINTER_BASE,x .NoAnimUpdate inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #10 beq .DoCheckMove jmp .DoGhostMove .DoCheckMove lda #0 sta SPRITE_ANIM_DELAY,x txa and #$01 tay lda SPRITE_ACTIVE,y cmp #TYPE_PLAYER_DEAN beq .FoundPlayer cmp #TYPE_PLAYER_SAM beq .FoundPlayer ;check other player tya eor #1 tay lda SPRITE_ACTIVE,y cmp #TYPE_PLAYER_DEAN beq .FoundPlayer cmp #TYPE_PLAYER_SAM beq .FoundPlayer ;no player to hunt rts .FoundPlayer ;player index in y lda SPRITE_CHAR_POS_X,y cmp SPRITE_CHAR_POS_X,x bpl .MoveRight ;move left lda SPRITE_DIRECTION,x bne .AlreadyLookingLeft lda SPRITE_MOVE_POS,x beq .TurnLNow dec SPRITE_MOVE_POS,x bne .CheckYNow .TurnLNow ;turning now lda #1 sta SPRITE_DIRECTION,x jmp .CheckYNow .AlreadyLookingLeft lda SPRITE_MOVE_POS,x cmp #GHOST_MOVE_SPEED beq .CheckYNow inc SPRITE_MOVE_POS,x jmp .CheckYNow .MoveRight lda SPRITE_DIRECTION,x beq .AlreadyLookingRight lda SPRITE_MOVE_POS,x beq .TurnRNow dec SPRITE_MOVE_POS,x bne .CheckYNow ;turning now .TurnRNow lda #0 sta SPRITE_DIRECTION,x jmp .CheckYNow .AlreadyLookingRight lda SPRITE_MOVE_POS,x cmp #GHOST_MOVE_SPEED beq .CheckYNow inc SPRITE_MOVE_POS,x jmp .CheckYNow .CheckYNow ;player index in y lda SPRITE_CHAR_POS_Y,y cmp SPRITE_CHAR_POS_Y,x bpl .MoveDown ;move left lda SPRITE_DIRECTION_Y,x bne .AlreadyLookingUp lda SPRITE_MOVE_POS_Y,x beq .TurnUNow dec SPRITE_MOVE_POS_Y,x bne .DoGhostMove .TurnUNow ;turning now lda #1 sta SPRITE_DIRECTION_Y,x jmp .DoGhostMove .AlreadyLookingUp lda SPRITE_MOVE_POS_Y,x cmp #GHOST_MOVE_SPEED beq .DoGhostMove inc SPRITE_MOVE_POS_Y,x jmp .DoGhostMove .MoveDown lda SPRITE_DIRECTION_Y,x beq .AlreadyLookingDown lda SPRITE_MOVE_POS_Y,x beq .TurnDNow dec SPRITE_MOVE_POS_Y,x bne .DoGhostMove ;turning now .TurnDNow lda #0 sta SPRITE_DIRECTION_Y,x jmp .DoGhostMove .AlreadyLookingDown lda SPRITE_MOVE_POS_Y,x cmp #GHOST_MOVE_SPEED beq .DoGhostMove inc SPRITE_MOVE_POS_Y,x jmp .DoGhostMove .DoGhostMove ;move X times ldy SPRITE_MOVE_POS,x sty PARAM4 beq .DoY lda SPRITE_DIRECTION,x beq .DoRight .MoveLoopL jsr ObjectMoveLeftBlocking dec PARAM4 bne .MoveLoopL jmp .DoY .DoRight .MoveLoopR jsr ObjectMoveRightBlocking dec PARAM4 bne .MoveLoopR .DoY ;move X times ldy SPRITE_MOVE_POS_Y,x sty PARAM4 beq .MoveDone lda SPRITE_DIRECTION_Y,x beq .DoDown .MoveLoopU jsr ObjectMoveUpBlocking dec PARAM4 bne .MoveLoopU jmp .MoveDone .DoDown .MoveLoopD jsr ObjectMoveDownBlockingNoPlatform dec PARAM4 bne .MoveLoopD .MoveDone rts
And last but not least, the annoying fly. Moving randomly about it surely annoys the heck out of you. ;------------------------------------------------------------ ;move randomly diagonal ;------------------------------------------------------------ !zone BehaviourFly BehaviourFly lda DELAYED_GENERIC_COUNTER and #$01 bne .NoAnimUpdate lda SPRITE_ANIM_POS,x eor #1 sta SPRITE_ANIM_POS,x clc adc #SPRITE_FLY_1 sta SPRITE_POINTER_BASE,x .NoAnimUpdate lda SPRITE_STATE,x beq .Move dec SPRITE_MOVE_POS,x bne .NoAction ;can move again dec SPRITE_STATE,x jsr GenerateRandomNumber sta SPRITE_MOVE_POS,x jsr GenerateRandomNumber and #$03 cmp #3 bne .ValueOK lda #2 .ValueOK sta SPRITE_DIRECTION,x jsr GenerateRandomNumber and #$03 cmp #3 bne .ValueOK2 lda #2 .ValueOK2 sta SPRITE_DIRECTION_Y,x .NoAction rts .Move dec SPRITE_MOVE_POS,x bne .CanMove ;wait jsr GenerateRandomNumber sta SPRITE_MOVE_POS,x inc SPRITE_STATE,x rts .CanMove lda SPRITE_DIRECTION,x beq .MoveRight cmp #2 beq .MoveY ;move left jsr ObjectMoveLeftBlocking beq .ToggleDirection jmp .MoveY .MoveRight jsr ObjectMoveRightBlocking beq .ToggleDirection jmp .MoveY .ToggleDirection lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x .MoveY lda SPRITE_DIRECTION_Y,x beq .MoveDown cmp #2 beq .NoYMovement ;move up jsr ObjectMoveUpBlocking beq .ToggleDirectionY rts .MoveDown jsr ObjectMoveDownBlockingNoPlatform beq .ToggleDirectionY .NoYMovement rts .ToggleDirectionY lda SPRITE_DIRECTION_Y,x eor #1 sta SPRITE_DIRECTION_Y,x rts
step42.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 41

Finally: A very talented chap volunteered to create imagery for the game. Lots of decent sprites and chars were created.
To celebrate there's also a slew of new enemies:

Wolfman
Eye
Jumping toad
Ghost skeleton



Code wise this is mainly add the sprites to the binary include and the new behaviours. As you can see, the ghost skeleton doesn't do anything interesting (yet). The jumping toad however is quite complex. It's movement consists of ducking, jumping and waiting. Still, for now, it's quite predictable. ;------------------------------------------------------------ ;ghost skeleton ;------------------------------------------------------------ !zone BehaviourGhostSkeleton BehaviourGhostSkeleton lda DELAYED_GENERIC_COUNTER and #$03 bne .NoAnimUpdate inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x and #$03 sta SPRITE_ANIM_POS,x tay lda GHOST_SKELETON_ANIMATION_TABLE,y sta SPRITE_POINTER_BASE,x .NoAnimUpdate .NormalUpdate ;does not do anything yet rts ;------------------------------------------------------------ ;jumping toad ;------------------------------------------------------------ !zone BehaviourJumpingToad BehaviourJumpingToad lda SPRITE_HITBACK,x beq .NoHitBack dec SPRITE_HITBACK,x lda SPRITE_HITBACK_DIRECTION,x beq .HitBackRight ;move left jsr ObjectMoveLeftBlocking rts .HitBackRight jsr ObjectMoveRightBlocking rts .NoHitBack lda SPRITE_STATE,x beq .NotDucking inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #6 bne .StillDucking lda #0 sta SPRITE_ANIM_DELAY,x ldy SPRITE_ANIM_POS,x inc SPRITE_ANIM_POS,x lda TOAD_JUMP_ANIMATION_TABLE,y sta SPRITE_POINTER_BASE,x cmp #SPRITE_JUMPING_TOAD_1 bne .StillDucking ;start jump lda #0 sta SPRITE_STATE,x inc SPRITE_JUMP_POS,x .StillDucking rts .NotDucking lda SPRITE_JUMP_POS,x beq .FallIfPossible ;toad is jumping lda SPRITE_JUMP_POS,x cmp #TOAD_JUMP_TABLE_SIZE bne .JumpOn ;jump done jmp .JumpBlocked .JumpOn ldy SPRITE_JUMP_POS,x inc SPRITE_JUMP_POS,x lda TOAD_JUMP_TABLE,y bne .KeepJumping ;no jump movement needed jmp .ToadMove .KeepJumping sta PARAM5 .JumpContinue jsr ObjectMoveUpBlocking beq .JumpBlocked dec PARAM5 bne .JumpContinue jmp .ToadMove .JumpBlocked lda #0 sta SPRITE_JUMP_POS,x jmp .ToadMove .FallIfPossible jsr UpdateSpriteFall beq .CanJump jmp .ToadMove .CanJump inc SPRITE_STATE,x lda #0 sta SPRITE_ANIM_DELAY,x sta SPRITE_ANIM_POS,x lda #SPRITE_JUMPING_TOAD_2 sta SPRITE_POINTER_BASE,x rts ;simple move left/right .ToadMove lda SPRITE_DIRECTION,x beq .MoveRight jsr ObjectMoveLeftBlocking beq .ToggleDirection rts .MoveRight jsr ObjectMoveRightBlocking beq .ToggleDirection rts .ToggleDirection lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x rts
Then we have the wolfman. Running about the level and randomly jumping. What's worse, the angrier it gets, the faster it runs. !zone BehaviourWolf BehaviourWolf lda SPRITE_HITBACK,x beq .NoHitBack dec SPRITE_HITBACK,x lda SPRITE_HITBACK_DIRECTION,x beq .HitBackRight ;move left jsr ObjectMoveLeftBlocking rts .HitBackRight jsr ObjectMoveRightBlocking rts .NoHitBack ;animate wolf lda SPRITE_JUMP_POS,x bne .NoAnimUpdate inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #4 bne .NoAnimUpdate lda #0 sta SPRITE_ANIM_DELAY,x inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x and #$03 sta SPRITE_ANIM_POS,x tay lda SPRITE_DIRECTION,x beq .FacingLeft lda WOLF_ANIMATION_TABLE,y sta SPRITE_POINTER_BASE,x jmp .NoAnimUpdate .FacingLeft lda WOLF_ANIMATION_TABLE,y clc adc #( SPRITE_WOLF_WALK_R_1 - SPRITE_WOLF_WALK_L_1 ) sta SPRITE_POINTER_BASE,x .NoAnimUpdate lda SPRITE_JUMP_POS,x bne .NoFallHandling jsr UpdateSpriteFall sta SPRITE_FALLING,x bne .IsFalling ;neither jumping nor falling jsr GenerateRandomNumber and #$0f cmp SPRITE_ANNOYED,x bpl .IsFalling ;random jump jmp .Jumping .IsFalling .NoFallHandling lda SPRITE_ANNOYED,x clc adc #2 sta PARAM6 .MoveStep dec PARAM6 beq .MoveDone lda SPRITE_DIRECTION,x beq .MoveRight ;move left lda SPRITE_JUMP_POS,x ora SPRITE_FALLING,x bne .OnlyMoveLeft jsr ObjectWalkOrJumpLeft beq .ToggleDirection jmp .MoveStep .MoveDone lda SPRITE_JUMP_POS,x beq .NotJumping .Jumping jsr UpdateSpriteJump .NotJumping rts .OnlyMoveLeft jsr ObjectMoveLeftBlocking beq .ToggleDirection jmp .MoveStep .MoveRight lda SPRITE_JUMP_POS,x ora SPRITE_FALLING,x bne .OnlyMoveRight jsr ObjectWalkOrJumpRight beq .ToggleDirection jmp .MoveStep .OnlyMoveRight jsr ObjectMoveRightBlocking beq .ToggleDirection jmp .MoveStep .ToggleDirection lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x jmp .MoveStep
The eye is rather easy again. It moves diagonally, but stops every now and then to take a look. !zone BehaviourEye BehaviourEye lda DELAYED_GENERIC_COUNTER and #$03 bne .NoAnimUpdate ;inc SPRITE_ANIM_POS,x ;lda SPRITE_ANIM_POS,x ;and #$03 ;sta SPRITE_ANIM_POS,x ;tay ;lda BAT_ANIMATION,y ;sta SPRITE_POINTER_BASE,x .NoAnimUpdate lda SPRITE_STATE,x beq .Move cmp #1 beq .EyeOpen cmp #2 beq .EyeIsOpen ;eye closes inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #3 bne .NoActionNow lda #0 sta SPRITE_ANIM_DELAY,x ;close animation dec SPRITE_ANIM_POS,x dec SPRITE_POINTER_BASE,x ldy SPRITE_ANIM_POS,x lda EYE_COLOR_TABLE,y sta VIC_SPRITE_COLOR,x cpy #0 bne .NoActionNow ;can move again lda #0 sta SPRITE_STATE,x rts .EyeOpen inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #3 bne .NoActionNow lda #0 sta SPRITE_ANIM_DELAY,x ;open animation inc SPRITE_ANIM_POS,x inc SPRITE_POINTER_BASE,x ldy SPRITE_ANIM_POS,x lda EYE_COLOR_TABLE,y sta VIC_SPRITE_COLOR,x cpy #3 bne .NoActionNow ;now wait inc SPRITE_STATE,x rts .EyeIsOpen inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #30 beq .EyeCloseNow .NoActionNow rts .EyeCloseNow lda #3 sta SPRITE_STATE,x lda #0 sta SPRITE_ANIM_DELAY,x rts .Move jsr GenerateRandomNumber cmp #7 bne .MoveNow ;start blinking lda #1 sta SPRITE_STATE,x rts .MoveNow lda SPRITE_DIRECTION,x beq .MoveRight ;move left jsr ObjectMoveLeftBlocking beq .ToggleDirection jmp .MoveY .MoveRight jsr ObjectMoveRightBlocking beq .ToggleDirection jmp .MoveY .ToggleDirection lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x .MoveY lda SPRITE_DIRECTION_Y,x beq .MoveDown ;move up jsr ObjectMoveUpBlocking beq .ToggleDirectionY rts .MoveDown jsr ObjectMoveDownBlocking beq .ToggleDirectionY rts .ToggleDirectionY lda SPRITE_DIRECTION_Y,x eor #1 sta SPRITE_DIRECTION_Y,x rts
TL/DR: Have fun! step41.zip


Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 40

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



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


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

Note that for the spawn spot tables we use the !fill macro. It fills memory with n bytes of value x. ;number of possible spawn spots SPAWN_SPOT_COUNT = 8 SPAWN_SPOT_X !fill SPAWN_SPOT_COUNT,0 SPAWN_SPOT_Y !fill SPAWN_SPOT_COUNT,0 SPAWN_SPOT_ACTIVE !fill SPAWN_SPOT_COUNT,0 SPAWN_SPOT_TYPE !fill SPAWN_SPOT_COUNT,0 SPAWN_SPOT_SPAWN_COUNT !fill SPAWN_SPOT_COUNT,0 SPAWN_SPOT_DELAY !fill SPAWN_SPOT_COUNT,0 NUMBER_SPAWN_SPOTS_ALIVE !byte 0
In GameFlowControl we add the call to handle the spawn spots, and modify the level-done check: jsr ProcessSpawnSpots ;------------------------ ;slow events inc DELAYED_GENERIC_COUNTER lda DELAYED_GENERIC_COUNTER cmp #8 bne .NoTimedActionYet lda #0 sta DELAYED_GENERIC_COUNTER ;level done delay lda NUMBER_ENEMIES_ALIVE bne .NotDoneYet lda NUMBER_SPAWN_SPOTS_ALIVE bne .NotDoneYet
Handling a spawn spot is easy. If a spawn spots delay counter reaches zero we check if the number of alive enemies is below 4. If it is, spawn an enemy, if it isn't reset the delay counter. Once the spawn spot object counter reaches zero the active spawn spot is removed.
  ;------------------------------------------------------------ ;handle spawn spots ;------------------------------------------------------------ !zone ProcessSpawnSpots ProcessSpawnSpots ldx #0 .SpawnSpotLoop lda SPAWN_SPOT_ACTIVE,x beq .NextSpawnSpot lda SPAWN_SPOT_DELAY,x beq .TryToSpawn dec SPAWN_SPOT_DELAY,x jmp .NextSpawnSpot .RemoveSpawnSpot lda #0 sta SPAWN_SPOT_ACTIVE,x dec NUMBER_SPAWN_SPOTS_ALIVE .NextSpawnSpot inx cpx #SPAWN_SPOT_COUNT bne .SpawnSpotLoop rts .TryToSpawn lda #128 sta SPAWN_SPOT_DELAY,x lda NUMBER_ENEMIES_ALIVE cmp #3 bpl .DoNotSpawn stx PARAM4 lda SPAWN_SPOT_TYPE,x sta PARAM3 lda SPAWN_SPOT_X,x sta PARAM1 lda SPAWN_SPOT_Y,x sta PARAM2 ;spawn object jsr FindEmptySpriteSlot beq .DoNotSpawn ;x is sprite slot ;PARAM1 is X ;PARAM2 is Y ;PARAM3 is object type jsr SpawnObject ;restore x ldx PARAM4 dec SPAWN_SPOT_SPAWN_COUNT,x beq .RemoveSpawnSpot .DoNotSpawn jmp .NextSpawnSpot
A new level element LD_SPAWN_SPOT is added to add spawn spots to the stage table. The code simply stores the data in the next free spawn spot slot.
  !zone LevelSpawnSpot LevelSpawnSpot ;find free spot ldx #0 .ExamineNextSpot lda SPAWN_SPOT_ACTIVE,x beq .EmptySpotFound inx cpx SPAWN_SPOT_COUNT bne .ExamineNextSpot jmp NextLevelData .EmptySpotFound inc NUMBER_SPAWN_SPOTS_ALIVE lda #1 sta SPAWN_SPOT_ACTIVE,x ;X pos iny lda (ZEROPAGE_POINTER_1),y sta SPAWN_SPOT_X,x ;Y pos iny lda (ZEROPAGE_POINTER_1),y sta SPAWN_SPOT_Y,x ;type iny lda (ZEROPAGE_POINTER_1),y sta SPAWN_SPOT_TYPE,x ;count iny lda (ZEROPAGE_POINTER_1),y sta SPAWN_SPOT_SPAWN_COUNT,x tya pha jmp NextLevelData step40.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 39

And now technically a rather small step, but with huge impact: Music.

For now we're using a demo song from GoatTracker. GoatTracker is a PC tracker tool that lets you compose songs and provides the player source. It's most common for music players to be stored at $1000. Since musicians on the C64 usually need to be programmers as well the music mostly comes with the player code.



At the beginning we initialise the player.
  ;set volume to max lda #15 sta 53248 ;initialise music player (play song #0) lda #0 jsr MUSIC_PLAYER
The song and player code can be exported to a binary blob, this is simply included in the source: * = $3000 MUSIC_PLAYER !binary "gt2music.bin"
Once a frame we call the play music routine to advance the music:
  ;------------------------------------------------------------ ;wait for the raster to reach line $f8 ;this is keeping our timing stable ;------------------------------------------------------------ !zone WaitFrame WaitFrame ;are we on line $F8 already? if so, wait for the next full screen ;prevents mistimings if called too fast lda $d012 cmp #$F8 beq WaitFrame ;wait for the raster to reach line $f8 (should be closer to the start of this line this way) .WaitStep2 lda $d012 cmp #$F8 bne .WaitStep2 ;play music jsr MUSIC_PLAYER + 3 rts

That's it. Easy as pie.


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

Have fun! step39.zip Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 38

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

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




We start by adding the game modes: ;game mode types GT_SINGLE_PLAYER_DEAN = 0 GT_SINGLE_PLAYER_SAM = 1 GT_COOP = 2

Of course the current mode needs to be displayed in the main menu. We use GAME_MODE as index into a text mode description table. ldx GAME_MODE lda TEXT_GAME_MODE_LO,x sta ZEROPAGE_POINTER_1 lda TEXT_GAME_MODE_HI,x sta ZEROPAGE_POINTER_1 + 1 lda #11 sta PARAM1 lda #21 sta PARAM2 jsr DisplayText
...and let the player toggle through the game modes with a joystick up movement:
  lda #$01 bit JOYSTICK_PORT_II bne .NotUpPressed lda UP_RELEASED beq .UpPressed inc GAME_MODE lda GAME_MODE cmp #3 bne .NoGameModeWrap lda #0 sta GAME_MODE .NoGameModeWrap ;redisplay game mode ldx GAME_MODE lda TEXT_GAME_MODE_LO,x sta ZEROPAGE_POINTER_1 lda TEXT_GAME_MODE_HI,x sta ZEROPAGE_POINTER_1 + 1 lda #11 sta PARAM1 lda #21 sta PARAM2 jsr DisplayText lda #0 jmp .UpPressed .NotUpPressed lda #1 .UpPressed sta UP_RELEASED
The game mode incurs a few changes in the score display: ;score display according to game mode lda GAME_MODE cmp #GT_SINGLE_PLAYER_DEAN beq .DeanOnly cmp #GT_SINGLE_PLAYER_SAM beq .SamOnly lda #<TEXT_DISPLAY_DEAN_AND_SAM sta ZEROPAGE_POINTER_1 lda #>TEXT_DISPLAY_DEAN_AND_SAM sta ZEROPAGE_POINTER_1 + 1 jmp .DisplayDisplay .DeanOnly lda #0 sta PLAYER_LIVES + 1 lda #<TEXT_DISPLAY_DEAN_ONLY sta ZEROPAGE_POINTER_1 lda #>TEXT_DISPLAY_DEAN_ONLY sta ZEROPAGE_POINTER_1 + 1 jmp .DisplayDisplay .SamOnly lda #0 sta PLAYER_LIVES lda #<TEXT_DISPLAY_SAM_ONLY sta ZEROPAGE_POINTER_1 lda #>TEXT_DISPLAY_SAM_ONLY sta ZEROPAGE_POINTER_1 + 1 .DisplayDisplay

..and the joystick port a player uses. This snippet makes sure that for single player modes the used joystick port is port 2.
  ;settings per game mode ;default ports lda #0 sta PLAYER_JOYSTICK_PORT lda #1 sta PLAYER_JOYSTICK_PORT + 1 lda GAME_MODE cmp #GT_SINGLE_PLAYER_SAM bne .NoPortChange lda #0 sta PLAYER_JOYSTICK_PORT + 1 .NoPortChange
A lot of changes from Dean to Same are quite simple to implement. Reuse Dean's code, but add an index to the sprite tables. Since Dean is always in slot 0, the ",x" was omitted to speed code up. Now we just put ,x for every sprite table access (Sam is always in slot 1), and the basic code just works.

Some pieces had to be written anew from scratch; for example the fire code for Sam. Sam does not use a shotgun, he uses his demonic forces instead. Sam needs to look at an enemy, and keep fire pressed. Doing that a enemy will get frozen and take damage. However Sam cannot move during that phase. .FireSam ldy PLAYER_JOYSTICK_PORT,x lda JOYSTICK_PORT_II,y and #$10 bne .SamNotFirePushed lda #1 sta PLAYER_FIRE_PRESSED_TIME,x stx PARAM6 jsr SamUseForce beq .NoEnemyHeld ;Sam needs to keep pressed inc PLAYER_SHOT_PAUSE,x lda PLAYER_SHOT_PAUSE,x cmp #40 beq .EnemyHurtBySam ldy SPRITE_HELD dey lda #2 sta VIC_SPRITE_COLOR,y .NoEnemyHeld .EnemyWasHurt ;restore sprite index ldx PARAM6 jmp .NotFirePushed .EnemyHurtBySam lda #0 sta PLAYER_SHOT_PAUSE,x ldx SPRITE_HELD dex lda #0 sta VIC_SPRITE_COLOR,x dec SPRITE_HP,x bne .EnemyWasHurt .EnemyKilledBySam lda #5 jsr IncreaseScore ldx SPRITE_HELD dex jsr KillEnemy ldx PARAM6 lda #0 sta SPRITE_HELD jmp .NotFirePushed .SamNotFirePushed lda #0 sta SPRITE_HELD sta PLAYER_SHOT_PAUSE,x sta PLAYER_FIRE_PRESSED_TIME,x jmp .NotFirePushed step38.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 37

Currently the player is simply vanishing when getting killed. Kinda looks odd, doesn't it? We add a nice new animation and then send the player back in.




First we remove the previous player kill code (which removed the sprite and displayed a GET READY message) and replace it with this.
Note that we keep the sprite object but set the state to invincible (>= 128). .PlayerCollidedWithEnemy ;player killed ldx PARAM6 lda #129 sta SPRITE_STATE,x lda #SPRITE_PLAYER_DEAD sta SPRITE_POINTER_BASE,x lda #0 sta SPRITE_MOVE_POS,x
On top of the PlayerControl routine we add this part. Note that the routine is jumped into PlayerControl, but .PlayerIsDying is above. The reason for this is the limited range of branch mnemonics. They only allow for -128/127 byte jumps. Just by rearranging the code the branch can be kept.
A different solution would be to replace the branch by a reverse branch with a followup jmp.
  .PlayerIsDying inc SPRITE_MOVE_POS,x lda SPRITE_MOVE_POS,x cmp #64 beq .PlayerRespawn and #$03 bne .NoUpMove jsr MoveSpriteUp .NoUpMove rts .PlayerRespawn dec PLAYER_LIVES jsr DisplayLiveNumber ;game over? lda PLAYER_LIVES bne .RestartPlayer jmp CheckForHighscore .RestartPlayer lda SPRITE_ACTIVE ;refill shells ldy #0 .RefillShellImage lda #2 sta SCREEN_COLOR + 23 * 40 + 19,y lda #7 sta SCREEN_COLOR + 24 * 40 + 19,y iny cpy PLAYER_SHELLS_MAX bne .RefillShellImage lda PLAYER_SHELLS_MAX sta PLAYER_SHELLS ;respawn at correct position lda PLAYER_START_POS_X sta PARAM1 lda PLAYER_START_POS_Y sta PARAM2 ;PARAM1 and PARAM2 hold x,y already jsr CalcSpritePosFromCharPos ;enable sprite lda BIT_TABLE ora VIC_SPRITE_ENABLE sta VIC_SPRITE_ENABLE ;initialise enemy values lda #SPRITE_PLAYER sta SPRITE_POINTER_BASE lda #0 sta PLAYER_FAST_RELOAD sta PLAYER_INVINCIBLE sta SPRITE_STATE ;look right per default lda #0 sta SPRITE_DIRECTION lda #0 sta SPRITE_JUMP_POS sta SPRITE_FALLING rts PlayerControl lda SPRITE_STATE,x cmp #129 bne .NotDying jmp .PlayerIsDying .NotDying
Now that the whole dying sequence is handled by PlayerControl directly the full routine DeadControl can be removed. This change also helps a lot in the next step step37.zip
Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 36

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



For the smoke/explosion we'll add a new object type: SPRITE_EXPLOSION_1 = SPRITE_BASE + 47 SPRITE_EXPLOSION_2 = SPRITE_BASE + 48 SPRITE_EXPLOSION_3 = SPRITE_BASE + 49 TYPE_EXPLOSION = 9
Now, once the enemy loses its last hit point, we don't simply remove the object but rather replace it with an explosion. This time we don't call RemoveObject/SpawnObject but rather put the required values in place right there. lda #TYPE_EXPLOSION sta SPRITE_ACTIVE,x lda #15 sta VIC_SPRITE_COLOR,x lda BIT_TABLE,x ora VIC_SPRITE_MULTICOLOR sta VIC_SPRITE_MULTICOLOR lda #SPRITE_EXPLOSION_1 sta SPRITE_POINTER_BASE,x lda #0 sta SPRITE_ANIM_DELAY,x sta SPRITE_ANIM_POS,x
The explosion behaviour itself is quite simple. Move upwards slowly and animate. Once the end of the animation is reached remove itself. ;------------------------------------------------------------ ;explosion ;------------------------------------------------------------ !zone BehaviourExplosion BehaviourExplosion jsr MoveSpriteUp inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #3 beq .UpdateAnimation rts .UpdateAnimation lda #0 sta SPRITE_ANIM_DELAY,x inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x cmp #4 beq .ExplosionDone clc adc #SPRITE_EXPLOSION_1 sta SPRITE_POINTER_BASE,x rts .ExplosionDone jsr RemoveObject rts For the hit back of the enemies we'll add this code at the beginning of all participating enemies. The SPRITE_HITBACK counter is checked, if it's set, it's decreased and the object is moved in the hit back direction. lda SPRITE_HITBACK,x beq .NoHitBack dec SPRITE_HITBACK,x lda SPRITE_HITBACK_DIRECTION,x beq .HitBackRight ;move left jsr ObjectMoveLeftBlocking rts .HitBackRight jsr ObjectMoveRightBlocking rts .NoHitBack
The current HitBack methods are enhanced with the following snippet. Set the hit back counter (to 8) and use the direction from the player as the hit back dir. Since the player shots are instant this correctly holds the direction away from the player. lda #8 sta SPRITE_HITBACK,x ;hitback dir determined from player dir (equal shot dir) lda SPRITE_DIRECTION sta SPRITE_HITBACK_DIRECTION,x   step36.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 35

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




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

Add new constants: SPRITE_SPIDER_STAND = SPRITE_BASE + 44 SPRITE_SPIDER_WALK_1 = SPRITE_BASE + 45 SPRITE_SPIDER_WALK_2 = SPRITE_BASE + 46 TYPE_SPIDER = 8
Add the behaviour code: ;------------------------------------------------------------ ;run left/right, jump off directional ;------------------------------------------------------------ !zone BehaviourSpider BehaviourSpider ;animate spider inc SPRITE_ANIM_DELAY,x lda SPRITE_ANIM_DELAY,x cmp #2 bne .NoAnimUpdate lda #0 sta SPRITE_ANIM_DELAY,x inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x and #$3 sta SPRITE_ANIM_POS,x tay lda SPIDER_ANIMATION_TABLE,y sta SPRITE_POINTER_BASE,x .NoAnimUpdate lda SPRITE_JUMP_POS,x bne .NoFallHandling jsr UpdateSpriteFall sta SPRITE_FALLING,x .NoFallHandling lda #3 sta PARAM6 .MoveStep dec PARAM6 beq .MoveDone lda SPRITE_DIRECTION,x beq .MoveRight ;move left lda SPRITE_JUMP_POS,x ora SPRITE_FALLING,x bne .OnlyMoveLeft jsr ObjectWalkOrJumpLeft beq .ToggleDirection jmp .MoveStep .MoveDone lda SPRITE_JUMP_POS,x beq .NotJumping jsr UpdateSpriteJump .NotJumping rts .OnlyMoveLeft jsr ObjectMoveLeftBlocking beq .ToggleDirection jmp .MoveStep .MoveRight lda SPRITE_JUMP_POS,x ora SPRITE_FALLING,x bne .OnlyMoveRight jsr ObjectWalkOrJumpRight beq .ToggleDirection jmp .MoveStep .OnlyMoveRight jsr ObjectMoveRightBlocking beq .ToggleDirection jmp .MoveStep .ToggleDirection lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x jmp .MoveStep
The behaviour of this spider enemy: Walk. If a gap below is encountered, jump. If the way is blocked, turn. This behaviour is put in a general subroutine, since it might come in handy for other enemies later (and it will): ;------------------------------------------------------------ ;walk object left if could fall off jump if blocked turn ;x = object index ;------------------------------------------------------------ !zone ObjectWalkOrJumpLeft ObjectWalkOrJumpLeft lda SPRITE_CHAR_POS_X_DELTA,x beq .CheckCanMoveLeft .CanMoveLeft dec SPRITE_CHAR_POS_X_DELTA,x jsr MoveSpriteLeft lda #1 rts .CheckCanMoveLeft jsr CanWalkOrJumpLeft beq .Blocked cmp #1 beq .WalkLeft ;jump lda SPRITE_JUMP_POS,x bne .WalkLeft lda #1 sta SPRITE_JUMP_POS,x .WalkLeft lda #8 sta SPRITE_CHAR_POS_X_DELTA,x dec SPRITE_CHAR_POS_X,x jmp .CanMoveLeft .Blocked rts ;------------------------------------------------------------ ;checks if an object can walk or jump left (jump if would fall off) ;x = object index ;returns 0 if blocked ;returns 1 if possible ;returns 2 if jump required (not blocked, but in front of hole) ;------------------------------------------------------------ !zone CanWalkOrJumpLeft CanWalkOrJumpLeft ldy SPRITE_CHAR_POS_Y,x dey lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 ldy SPRITE_CHAR_POS_X,x dey lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .BlockedLeft tya clc adc #40 tay lda (ZEROPAGE_POINTER_1),y jsr IsCharBlocking bne .BlockedLeft ;is a hole in front ldy SPRITE_CHAR_POS_Y,x lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_BACK_LINE_OFFSET_TABLE_HI,y sta ZEROPAGE_POINTER_1 + 1 lda SPRITE_CHAR_POS_X,x clc adc #39 tay lda (ZEROPAGE_POINTER_1),y jsr IsCharBlockingFall bne .NoHole lda #2 rts .NoHole lda #1 rts .BlockedLeft lda #0 rts

To get and show annoyed behaviour a new state array SPRITE_ANNOYED is created. Enemies are supposed to change color to see once they get angry. For this we add a new color table: TYPE_ANNOYED_COLOR !byte 0 ;dummy !byte 10 ;player !byte 2 ;bat diagonal !byte 2 ;bat up down !byte 4 ;bat 8 !byte 2 ;mummy !byte 13 ;zombie !byte 2 ;bat vanish !byte 2 ;spider
Add a little change to the common HitBehaviourHurt routine to increase the annoyance state:
  !zone HitBehaviourHurt HitBehaviourHurt inc SPRITE_ANNOYED,x ldy SPRITE_ACTIVE,x lda TYPE_ANNOYED_COLOR,y sta VIC_SPRITE_COLOR,x rts
Voila! In a later step we'll add some enemies that change behaviour once they get angry. step35.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 34

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



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

We'll start by defining the new constants for the items: ;item types ITEM_FAST_RELOAD = 2 ITEM_INVINCIBLE = 3 ;effect counters PLAYER_FAST_RELOAD !byte 0 PLAYER_INVINCIBLE !byte 0 ;item images ITEM_CHAR_UL !byte 4,8,16,20 ITEM_COLOR_UL !byte 7,2,1,1 ITEM_CHAR_UR !byte 5,9,17,21 ITEM_COLOR_UR !byte 4,2,2,7 ITEM_CHAR_LL !byte 6,10,18,22 ITEM_COLOR_LL !byte 7,2,2,7 ITEM_CHAR_LR !byte 7,11,19,23 ITEM_COLOR_LR !byte 4,2,2,4

The actual collect item code is simple, we're just setting some new variables. Note the SPRITE_STATE, which is set to 0x80. .EffectFastReload lda #200 sta PLAYER_FAST_RELOAD jmp .RemoveItem .EffectInvincible lda #200 sta PLAYER_INVINCIBLE lda #128 sta SPRITE_STATE jmp .RemoveItem
Thus the CheckCollision adds a check for the highest bit in SPRITE_STATE, to allow for player invincibility.
  CheckCollisions lda SPRITE_ACTIVE bne .PlayerIsAlive rts .PlayerIsAlive lda SPRITE_STATE cmp #128 bmi .IsVulnerable rts .IsVulnerable ldx #1 .CollisionLoop


To visualize the invincibility state we add this code at the start of PlayerControl. If PLAYER_INVINCIBLE is set, the players color is cycled. lda PLAYER_INVINCIBLE beq .NotInvincible ;count down invincibility inc VIC_SPRITE_COLOR dec PLAYER_INVINCIBLE bne .NotInvincible lda #0 sta SPRITE_STATE lda #10 sta VIC_SPRITE_COLOR .NotInvincible ... previous code

In the players reload code we add the double speed reload check: ... lda PLAYER_FAST_RELOAD beq .NoFastReload dec PLAYER_FAST_RELOAD inc PLAYER_STAND_STILL_TIME .NoFastReload ...

At the location, where we previously always spawned an item we add this: ;only spawn item randomly lda GenerateRandomNumber and #02 beq .CreateItem jmp .ShotDone ... .CreateItem jsr SpawnItem jmp .ShotDone

To get things running clean we change a few spots:

On restarting the temporary states need to be reset. lda #0 sta PLAYER_FAST_RELOAD sta PLAYER_INVINCIBLE sta SPRITE_STATE
step34.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 33

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


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





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

The diagonal moving bat is added. This is quite simple, we add a second direction variable (SPRITE_DIRECTION_Y) for the vertical direction. Copy over the left/right moving code and adjust it to vertical movement. Done! ;------------------------------------------------------------ ;simply move diagonal ;------------------------------------------------------------ !zone BehaviourBatDiagonal BehaviourBatDiagonal lda DELAYED_GENERIC_COUNTER and #$3 bne .NoAnimUpdate inc SPRITE_ANIM_POS,x lda SPRITE_ANIM_POS,x and #$3 sta SPRITE_ANIM_POS,x tay lda BAT_ANIMATION,y sta SPRITE_POINTER_BASE,x .NoAnimUpdate lda SPRITE_DIRECTION,x beq .MoveRight ;move left jsr ObjectMoveLeftBlocking beq .ToggleDirection jmp .MoveY .MoveRight jsr ObjectMoveRightBlocking beq .ToggleDirection jmp .MoveY .ToggleDirection lda SPRITE_DIRECTION,x eor #1 sta SPRITE_DIRECTION,x .MoveY lda SPRITE_DIRECTION_Y,x beq .MoveDown ;move up jsr ObjectMoveUpBlocking beq .ToggleDirectionY rts .MoveDown jsr ObjectMoveDownBlocking beq .ToggleDirectionY rts .ToggleDirectionY lda SPRITE_DIRECTION_Y,x eor #1 sta SPRITE_DIRECTION_Y,x rts
For the mummy to react to the player we add this snippet in front of its behaviour code. Detecting the player is done the naive way. The player has to be at the same height (the same char y pos), and the mummy has to look in its direction.

If all these conditions are matched the mummy assumes its attack stance and hurries towards the player.
  lda SPRITE_CHAR_POS_Y,x cmp SPRITE_CHAR_POS_Y bne .NoPlayerInSight ;player on same height ;looking at the player? lda SPRITE_DIRECTION,x beq .LookingRight lda SPRITE_CHAR_POS_X,x cmp SPRITE_CHAR_POS_X bpl .LookingAtPlayer jmp .NoPlayerInSight .LookingRight lda SPRITE_CHAR_POS_X,x cmp SPRITE_CHAR_POS_X bmi .LookingAtPlayer jmp .NoPlayerInSight .LookingAtPlayer lda #SPRITE_MUMMY_ATTACK_R clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x lda SPRITE_DIRECTION,x beq .AttackRight ;attack to left jsr ObjectMoveLeftBlocking jsr ObjectMoveLeftBlocking beq .ToggleDirection rts .AttackRight ;attack to left jsr ObjectMoveRightBlocking jsr ObjectMoveRightBlocking beq .ToggleDirection rts .NoPlayerInSight ... old behaviour
Allowing the zombie to jump is pretty easy as well. We already have jumping code for the player. In this step we however just copy the code over. The actual jump code starts at the .jump label.

Increase the jump cointer, fetch the Y movement from a jump table and move the sprite upwards. Once the zombie reaches the end of the table (the peak of the jump) we simply let gravity set in.
  !zone BehaviourZombie BehaviourZombie lda SPRITE_JUMP_POS,x bne .IsJumping jsr ObjectMoveDownBlocking bne .Falling .IsJumping lda DELAYED_GENERIC_COUNTER and #$3 beq .MovementUpdate rts .Falling rts .MovementUpdate lda SPRITE_JUMP_POS,x bne .UpdateJump lda SPRITE_STATE,x bne .OtherStates jsr GenerateRandomNumber cmp #17 beq .Jump jmp .NormalWalk .OtherStates ;collapsing? cmp #128 beq .Collapsing1 cmp #129 beq .Collapsing2 cmp #131 bne .NotWakeUp1 jmp .WakeUp1 .NotWakeUp1 cmp #132 bne .NotWakeUp2 jmp .WakeUp2 .NotWakeUp2 cmp #130 bne .NotHidden jmp .Hidden .NotHidden rts .Jump ;start jump lda #SPRITE_ZOMBIE_JUMP_R clc adc SPRITE_DIRECTION,x sta SPRITE_POINTER_BASE,x .UpdateJump inc SPRITE_JUMP_POS,x lda SPRITE_JUMP_POS,x cmp #JUMP_TABLE_SIZE bne .JumpOn ;jump done lda #0 sta SPRITE_JUMP_POS,x rts .JumpOn ldy SPRITE_JUMP_POS,x lda JUMP_TABLE,y bne .KeepJumping rts .KeepJumping sta PARAM5 .JumpContinue jsr ObjectMoveUpBlocking beq .JumpBlocked dec PARAM5 bne .JumpContinue rts .JumpBlocked lda #0 sta SPRITE_JUMP_POS,x rts ... common zombie behaviour
step33.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 32

A completely unspectacular step, but necessary every now and then anyhow: Bug fixing.


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

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




On restarting the player we add these snippets:
The first half removes all active items, the second fills up the players' shells to the max. The last four lines set the player location to the level start position.
  ;remove all items ldy #0 .RemoveItem lda ITEM_ACTIVE,y cmp #ITEM_NONE beq .RemoveNextItem lda #ITEM_NONE sta ITEM_ACTIVE,y jsr RemoveItemImage .RemoveNextItem iny cpy #ITEM_COUNT bne .RemoveItem ;refill shells ldx #0 .RefillShellImage lda #2 sta SCREEN_COLOR + 23 * 40 + 19,x lda #7 sta SCREEN_COLOR + 24 * 40 + 19,x inx cpx PLAYER_SHELLS_MAX bne .RefillShellImage lda PLAYER_SHELLS_MAX sta PLAYER_SHELLS ;respawn at correct position lda PLAYER_START_POS_X sta PARAM1 lda PLAYER_START_POS_Y sta PARAM2

The item spawn code is enhanced with a simple check for the position to stay inside the wanted bounds.
  !zone SpawnItem SpawnItem ;find free item slot ldy #0 .CheckNextItemSlot lda ITEM_ACTIVE,y cmp #ITEM_NONE beq .FreeSlotFound iny cpy #ITEM_COUNT bne .CheckNextItemSlot rts .FreeSlotFound jsr GenerateRandomNumber and #$1 sta ITEM_ACTIVE,y sta PARAM1 lda #0 sta ITEM_TIME,y lda SPRITE_CHAR_POS_X,x sta ITEM_POS_X,y ;keep item in bounds cmp #37 bmi .XIsOk lda #37 sta ITEM_POS_X,y .XIsOk lda SPRITE_CHAR_POS_Y,x sec sbc #1 sta ITEM_POS_Y,y cmp #21 bne .YIsOk lda #20 sta ITEM_POS_X,y .YIsOk stx PARAM5 tya tax jsr PutItemImage ldx PARAM5 rts

To be able to respawn the player at the level start pos we now store the object location during level creation: ... lda PARAM3 sta SPRITE_ACTIVE,x cmp #TYPE_PLAYER bne .IsNotPlayer lda PARAM1 sta PLAYER_START_POS_X lda PARAM2 sta PLAYER_START_POS_Y .IsNotPlayer ;PARAM1 and PARAM2 hold x,y already jsr CalcSpritePosFromCharPos ...
step32.zip

Previous Step Next Step

Endurion

Endurion

 

A C64 Game - Step 31

And another rather unspectacular step: Just a bunch of new stages. Things do get prettier once the editor is used. Currently stages are assembled manually





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

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

Adding the level pointer:
  SCREEN_DATA_TABLE !word LEVEL_1 !word LEVEL_2 !word LEVEL_3 !word LEVEL_4 !word LEVEL_5 !word LEVEL_6 !word LEVEL_7 !word LEVEL_8 !word 0
The level data itself is built by hand (for now).
  LEVEL_4 !byte LD_AREA,1,1,38,21,2,13 !byte LD_LINE_H_ALT,1,5,20,96,13 !byte LD_LINE_H_ALT,25,5,14,96,13 !byte LD_LINE_H_ALT,1,8,10,96,13 !byte LD_LINE_H_ALT,15,8,24,96,13 !byte LD_LINE_H_ALT,1,11,18,96,13 !byte LD_LINE_H_ALT,23,11,16,96,13 !byte LD_LINE_H_ALT,1,14,33,96,13 !byte LD_LINE_H_ALT,38,14,2,96,13 !byte LD_LINE_H_ALT,6,17,33,96,13 !byte LD_LINE_H_ALT,12,20,6,96,13 !byte LD_OBJECT,3,21,TYPE_PLAYER !byte LD_OBJECT,3,4,TYPE_MUMMY !byte LD_OBJECT,33,4,TYPE_ZOMBIE !byte LD_OBJECT,23,7,TYPE_ZOMBIE !byte LD_OBJECT,10,10,TYPE_ZOMBIE !byte LD_OBJECT,30,13,TYPE_ZOMBIE !byte LD_OBJECT,20,16,TYPE_ZOMBIE !byte LD_OBJECT,35,21,TYPE_ZOMBIE !byte LD_END LEVEL_5 !byte LD_LINE_H_ALT,5,7,4,96,13 !byte LD_LINE_H_ALT,5,10,9,96,13 !byte LD_LINE_H_ALT,4,13,3,96,13 !byte LD_LINE_H_ALT,1,16,3,96,13 !byte LD_LINE_H_ALT,10,19,6,96,13 !byte LD_LINE_H_ALT,16,10,4,96,13 !byte LD_LINE_H_ALT,22,10,4,96,13 !byte LD_LINE_H_ALT,24,7,15,96,13 !byte LD_LINE_H_ALT,24,13,11,96,13 !byte LD_LINE_H_ALT,24,16,11,96,13 !byte LD_LINE_H_ALT,28,19,4,96,13 !byte LD_OBJECT,13,18,TYPE_PLAYER !byte LD_OBJECT,18,5,TYPE_BAT_LR !byte LD_OBJECT,34,8,TYPE_BAT_LR !byte LD_OBJECT,9,11,TYPE_BAT_LR !byte LD_OBJECT,15,14,TYPE_BAT_LR !byte LD_OBJECT,25,17,TYPE_BAT_LR !byte LD_END LEVEL_6 !byte LD_LINE_H_ALT,1,10,5,96,13 !byte LD_LINE_H_ALT,1,13,9,96,13 !byte LD_LINE_H_ALT,1,16,13,96,13 !byte LD_LINE_H_ALT,1,19,17,96,13 !byte LD_LINE_H_ALT,34,10,5,96,13 !byte LD_LINE_H_ALT,30,13,9,96,13 !byte LD_LINE_H_ALT,26,16,13,96,13 !byte LD_LINE_H_ALT,22,19,17,96,13 !byte LD_OBJECT,19,21,TYPE_PLAYER !byte LD_OBJECT,5,5,TYPE_BAT_LR !byte LD_OBJECT,15,5,TYPE_BAT_LR !byte LD_OBJECT,25,5,TYPE_BAT_LR !byte LD_OBJECT,35,5,TYPE_BAT_LR !byte LD_END LEVEL_7 !byte LD_LINE_H_ALT,1,5,5,96,13 !byte LD_LINE_H_ALT,1,8,5,96,13 !byte LD_LINE_H_ALT,1,11,5,96,13 !byte LD_LINE_H_ALT,1,14,5,96,13 !byte LD_LINE_H_ALT,1,17,5,96,13 !byte LD_LINE_H_ALT,1,20,5,96,13 !byte LD_LINE_H_ALT,34,5,5,96,13 !byte LD_LINE_H_ALT,34,8,5,96,13 !byte LD_LINE_H_ALT,34,11,5,96,13 !byte LD_LINE_H_ALT,34,14,5,96,13 !byte LD_LINE_H_ALT,34,17,5,96,13 !byte LD_LINE_H_ALT,34,20,5,96,13 !byte LD_LINE_V_ALT,6,8,11,128,9 !byte LD_LINE_V_ALT,33,8,11,128,9 !byte LD_OBJECT,19,21,TYPE_PLAYER !byte LD_OBJECT,15,5,TYPE_BAT_LR !byte LD_OBJECT,20,5,TYPE_BAT_LR !byte LD_OBJECT,25,5,TYPE_BAT_LR !byte LD_OBJECT,17,9,TYPE_BAT_LR !byte LD_OBJECT,23,9,TYPE_BAT_LR !byte LD_END LEVEL_8 !byte LD_LINE_H_ALT,1,5,5,96,13 !byte LD_LINE_H_ALT,10,5,20,96,13 !byte LD_LINE_H_ALT,34,5,5,96,13 !byte LD_LINE_H_ALT,1,8,5,96,13 !byte LD_LINE_H_ALT,10,8,20,96,13 !byte LD_LINE_H_ALT,34,8,5,96,13 !byte LD_LINE_V_ALT,10,6,3,2,13 !byte LD_LINE_V_ALT,16,6,3,2,13 !byte LD_LINE_V_ALT,23,6,3,2,13 !byte LD_LINE_V_ALT,29,6,3,2,13 !byte LD_LINE_H_ALT,5,11,7,96,13 !byte LD_LINE_H_ALT,28,11,7,96,13 !byte LD_LINE_H_ALT,10,14,20,96,13 !byte LD_LINE_H_ALT,5,17,7,96,13 !byte LD_LINE_H_ALT,28,17,7,96,13 !byte LD_LINE_H_ALT,10,20,20,96,13 !byte LD_OBJECT,19,21,TYPE_PLAYER !byte LD_OBJECT,15,5,TYPE_BAT_LR !byte LD_OBJECT,20,5,TYPE_BAT_LR !byte LD_OBJECT,25,5,TYPE_BAT_LR !byte LD_OBJECT,17,9,TYPE_BAT_LR !byte LD_OBJECT,23,9,TYPE_BAT_LR !byte LD_END

step31.zip

Previous StepNext Step

Endurion

Endurion

 

A C64 Game - Step 30

And a new cosmetic refinement, high score name entry. Now the entry is done on a title screen like layout, with a blinking cursor as well.

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




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


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


Highscore entry:
  jsr InitTitleIRQ ldy PARAM3 ldx #0 stx PARAM3 jmp .ShowChar .GetNextChar sty PARAM4 ;blink cursor jsr WaitFrame lda PARAM3 clc adc #6 tay lda (ZEROPAGE_POINTER_4),y eor #123 sta (ZEROPAGE_POINTER_4),y ;restore Y ldy PARAM4 ;use ROM routines, read char jsr KERNAL_GETIN beq .GetNextChar ;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 jsr SaveScores jmp TitleScreenWithoutIRQ

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

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

Previous StepNext Step

Endurion

Endurion

 

A C64 Game - Step 29

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






First setup up the fade counter in front of the TitleLoop: ;init color fade counter lda #0 sta COLOR_FADE_POS

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

For every line the fade pos is offset by one so the effect is applied diagonally. Read the color value from the table, write to color RAM, repeat for all lines, done.
  ;apply color fade inc COLOR_FADE_POS lda COLOR_FADE_POS and #( COLOR_FADE_LENGTH - 1 ) sta COLOR_FADE_POS lda #0 sta PARAM1 .FadeLine lda PARAM1 clc adc #10 tay lda SCREEN_LINE_OFFSET_TABLE_LO,y sta ZEROPAGE_POINTER_1 lda SCREEN_LINE_OFFSET_TABLE_HI,y clc adc #( ( ( SCREEN_COLOR - SCREEN_CHAR ) & $ff00 ) >> 8 ) sta ZEROPAGE_POINTER_1 + 1 ldy #6 lda COLOR_FADE_POS clc adc PARAM1 and #( COLOR_FADE_LENGTH - 1 ) tax .FadeColorNextChar lda COLOR_FADE_1,x sta (ZEROPAGE_POINTER_1),y iny cpy #35 beq .FadeColorLineDone inx cpx #COLOR_FADE_LENGTH bne .FadeColorNextChar ldx #0 jmp .FadeColorNextChar .FadeColorLineDone inc PARAM1 lda PARAM1 cmp #8 bne .FadeLine .. common TitleLoop code


And here's the used constants. You can play around with these, but keep in mind, COLOR_FADE_LENGTH needs to be a power of 2. Also, since the charset mode is set to multi color you can only safely use color indices below 8. COLOR_FADE_LENGTH = 16 COLOR_FADE_1 !byte 0,0,6,6,3,3,1,1,1,1,1,1,3,3,6,6
step29.zip

Previous Step Next Step

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!