Design Overview
I wanted to start with a basic overview of the game design, so using the reversed engineered code for CBM Prg Studio as mentioned in part 1, the basic flow of the game is as follows:
- Initialise (Entry point $1000)
- Menu_DisplayTitleScreen
- Menu_DisplayHiScores
- Menu_DemoMode
- If ‘F1’ pressed
- Menu_DisplayTitleScreen
- If ‘F1’ pressed
- If ‘Space’ pressed
- Menu_SelectControls
- Level_InitialiseGame
- Level_GetReady
- Level_GameLoop
- If level complete
- Level_Complete
- Level_GetReady
- If princess rescued
- Quasi_RescueEsmerelda
- Level_GetReady (Game continues)
- If life lost
- Level_LifeLost
- If lives remaining
- Level_GetReady
- Else
- Player_GameOver
- Player_EnterHiScoreName (if hi-score)
- Menu_DisplayTitleScreen
For the key presses in the title screen and demo mode, it sets up a simple IRQ:
sei
lda #<IRQ_MenuOptionSelect
sta sysIntVectorLo
lda #>IRQ_MenuOptionSelect
sta sysIntVectorHi
cli
IRQ_MenuOptionSelect
lda sysKeyPress
cmp #KEY_F1
beq .DisplayInstructionsSelected
cmp #KEY_SPACE
beq .StartGameSelected
jmp krnINTERRUPT
.DisplayInstructionsSelected
jmp Menu_DisplayInstructions
.StartGameSelected
jmp Menu_SelectControls
Level Builder
The levels are derived from a list of bits which define the type of level and the type of obstacles on the level:
;-------------------------------------------------------------------------------
; LEVEL DATA
;
; Level Types:
; $01 = Knight Pit
; $02 = Empty Pit
; $04 = Row of Bells
; $08 = Rope Pit
; $10 = Esmerelda's Tower
; $20 = Wall
;
; Obstacle Types:
; $01 = Rope
; $02 = Hi Fireball R-L
; $04 = Lo Fireball L-R
; $08 = Lo Fireball R-L
; $10 = Double Arrow HiLo R-L
; $20 = Double Arrow HiLo L-R
; $40 = Double Fireball Lo L+R
; $80 = Double Arrow HiLo L+r
;-------------------------------------------------------------------------------
tbl_LevelType byte $20, $28, $22, $21, $21, $22, $28, $21
byte $21, $2c, $22, $22, $21, $21, $2c, $31
byte $21, $21, $2c, $28, $22, $21, $21, $22
byte $21, $22, $21, $2c, $22, $28, $21, $31
byte $21, $22, 208, $21, $21, $21, $21, $2c
byte $2c, $22, $21, $22, $28, $2c, $21, $31
tbl_LevelObstacleType byte $08, $01, $00, $00, $02, $04, $01, $08
byte $10, $08, $20, $40, $20, $40, $80, $40
byte $20, $10, $08, $01, $20, $20, $40, $40
byte $20, $20, $40, $80, $40, $01, $40, $40
byte $20, $40, $01, $10, $20, $08, $10, $08
byte $80, $20, $10, $40, $01, $80, $40, $40
So for level 01 (index 0), tbl_LevelType will be $20 (Wall) and tbl_LevelObstacleType = $08 (Lo Fireball R-L)
These bits can be combined, so level 02 for instance is $28, so has a Wall ($20) and a Rope Pit ($08).
When building the level it has a list of Lo-Hi pointers to the drawing routine for the relevant Level Types:
;-------------------------------------------------------------------------------
; CHARACTER BLOCK & COLOUR DATA
;-------------------------------------------------------------------------------
tbl_LevelCharBlockLo byte <Screen_BuildKnights, <Screen_BuildPits, <Screen_BuildRowOfBells
byte <Screen_BuildRopePit, <Screen_BuildEsmereldaTower, <Screen_BuildWall
tbl_LevelCharBlockHi byte >Screen_BuildKnights, >Screen_BuildPits, >Screen_BuildRowOfBells
byte >Screen_BuildRopePit, >Screen_BuildEsmereldaTower, >Screen_BuildWall
The screen builder will get the tbl_levelType for the current level and setup the zero page pointers to call the correct screen drawing routine, very nice:
lda tbl_LevelCharBlockLo,Y
sta zpLow
lda tbl_LevelCharBlockHi,Y
sta zpHigh
jmp (zpLow)
For most levels it builds the Wall first and has char block drawing routines to replace certain parts of the screen e.g. the pits (some empty, some with knights), and these blocks are defined in memory and once the rows (ldy), height (ldx) and char pointers have been setup, the block drawing routine is called.
For example to draw an empty pit (note wall is already drawn):
ldx #4
ldy #3
lda #20
stx charRows
sty charWidth
sta colourOffset
ldx #<tbl_EmptyPitChars
ldy #>tbl_EmptyPitChars
lda #<scn_TemporaryScreen+$0199
sta zpScnPtrLo
lda #>scn_TemporaryScreen+$0199
sta zpScnPtrHi
jsr Screen_DrawCharBlocks
It uses a temporary off screen memory buffer to build the screen, as this eliminates any flicker and also used when scrolling the intro. It actually builds the screen as it goes, so the intro builds level 15, shows it, builds the next level (9), scrolls it and so on:
;-------------------------------------------------------------------------------
; SELECT SCREENS TO SHOW DURING INTRO SEQUENCE
;-------------------------------------------------------------------------------
Screen_IntroScrollSelect
ldx #9
stx currentLevel
jsr Screen_BuildScreen
jsr Screen_IntroScroll
ldx #8
stx currentLevel
jsr Screen_BuildScreen
jsr Screen_IntroScroll
ldx #0
stx currentLevel
jsr Screen_BuildScreen
jsr Screen_IntroScroll
rts
This screenshot below from the debugger shows the off-screen buffer of the next screen to be scrolled, ignore the colours as these can’t be buffered:
Scrolling
As I mentioned earlier the scrolling is jerky and that’s because it scrolls at 1 character at a time, then feeds the next column from the off-screen buffer, and not to be confused with double buffer scrolling, we get onto that later.
Sprites
Nothing exciting to mention here, Quasi is sprite 0 and the other sprites are used for the obstacles like arrows, fireballs and even the rope. The animated rope is made up of two sprites across 16 frames (8 swinging left, 8 right) so a large chunk, but done quite well I think. Only on the Esmeralda level are all 8 sprites used.
Quasi is a little blocky and I’ve seen in others games where they have used two sprites to create a hi-res outline and a multi-colour body, so would like to attempt this in another chapter.
Collision
The collision detection makes use of the hardware collision registers, so if a sprite-sprite collision occurs it checks the relevant bits to see if Quasi was involved and generally that means he dies.
For sprite-character collision it varies between level types, but checks if a collision has occurred and because the character objects are static can quite easily cross-reference the x position of Quasi to see what he has collided with.
Enemies
The enemies are defined by the obstacle type for each level, and at the start of each level loads the relevant sprites into place. The ‘Enemy_Select’ routine decides which type of objects need to be updated per frame, like moving an arrow, and made quite flexible by using the tbl_LevelObstacleType table, so different levels can reuse a type of enemy/obstacle.
A simple IRQ runs to update the enemies:
;-------------------------------------------------------------------------------
; IRQ TO UPDATE ENEMY AND OBSTACLES DURING GAME
;-------------------------------------------------------------------------------
IRQ_EnemyUpdate
jsr Enemy_Select
jsr Level_DisplayHeart
jsr Enemy_MoveKnight
jsr Level_UpdateRope
jmp krnINTERRUPT
That’s a very high level view of the game and shows the dynamic drawing mechanism of displaying the levels which I think was very clever back in 1983, it could have quite easily handled hundreds of different levels using this technique, but I imagine other games must have used this approach.
Now the basics are out of the way, let’s move on to making some changes.