Flying shark is a 1987 port of the Konami arcade game of the same name. It is vertical scrolling shooter in which the player pilots a bi-plane ( the eponymous "shark" ) across four long stages, attacking a variety of air, ground and sea based targets. Like many games in this genre, the player can pick-up weapon upgrades to assist them.
Flying shark is notable for being coded by Dominic Robinson - a developer well known at the time for succeeding at the "impossible" task of creating a well regarded port of the C64 game "Uridium". Given this pedigree it is unsurprising that this title is a cut above the standard for this genre: a solid 25hz, smooth scrolling, large numbers of objects, solid collision detection and ... some sound. Unlike many vintage games, it remains a solid playable game today.
See here for files (game, instructions, etc).
But smooth scrolling, sprite based games are not easy to achieve on the zx spectrum, which unlike its contemporaries has no hardware support for sprites or scrolling. All the graphics has to be generated by the 3.5 Mhz z80 CPU and fit into the 41k of usable memory. So how was this done?
Overview
Below is a memory map taken from a snapshot of the game. I have labelled these with their usage, in so far as I have been able to determine it and will discuss what these all mean as we go along. A listing exported from Ghidra as HTML is here .
$5b00-5b28 Pattern partial pointer array
$5b29-5b54 Wave partial pointer array
$5b54-5bae Object definitions
$5baf-5cf3 Patterns
$5cf4-5ee9 Wave definitions
$5eea-5f85 Stage 1 Events Stage 1's event track
$5f87-603d Stage 2 Events Stage 2's event track
$6040-60f0 Stage 3 Events Stage 3's event track
$60f1-61e0 Stage 4 Events Stage 4's event track
$61e1-61e7 Stage Colour Attributes
$61e8-6237 Stage Definitions
$6238-6479 Strings 0xff terminated strings
$647a-6497 Unused screen clear
$6498-6b3f Game code
$6b40-6c10 Sprite Definitions
$6c11-6cd0 Super Tank Image
$6cd1-6d70 Sprites Images Statics
$6d71-6d91 Stage 1 Map
$6d82-6dc2 Stage 2 Map
$6dc3-6dde Stage 3 Map
$6ddf-6e06 Stage 4 Map
$6e07-78ff Macro tiles
$7900-79ff Top Left Cells
$7a00-7aff Top Right Cells
$7b00-7bff Bottom Left Cells
$7c00-7cff Bottom Right Cells
$7d00-7dff Init code / Decompression buffer
$7e00-7e98 Init code
$7e99-7eff Unused?
$7e00-7eff Vertical collision mask - 256
$7f00-7fff Horizontal collision mask - 256
$8000-87ff Cell Images ( $8100-$81ff are "foreground" )
$8800-99ff back buffer back buffer arranged as 18 columns of 256
$9a00-9aff byte reflect look-up
$9b00-9b3b Cell Ptr Row Addresses
$9b3c-9b4e Frame Descriptors
$9b4f-9b5e Wave instances
$9b5f-9b9e Sin look-up table
$9b9f-9baf Variables
$9bb0-9bf7 Tile assembly buffers
$9bf8-9d1d Constants and Variables
$9d1e-9fa1 Game Objects - up to 20 objects
$9fa2-9fff Unused?
$a000-a926 Game code ( mainly drawing ) 2.3k
$a927-aa8d Music code
$aa8e-ac8c Music data
$ac8d-b72f Game code ( mainly drawing ) 2.7k
$b730-ca28 Game Code and Data ( mainly gameplay ) 4.8k
$ca29-f498 Sprite Image Data 10.8k
$f665-fa75 Cell Ptr Array - 864
$fa79-fdfc Background Ptr Array - 900
$fdfd-fdff Isr
$fe00-ff00 Interrupt Vector Table - 257
$ff01-ffff Stack - 255
Very roughly, the game uses about 12k for code, 9k for non-sprite static ( e.g. maps ) static data, 9k for mutable data structures ( back buffer, etc ) and 11k for sprites. About 5k of the code is related to drawing. The relatively large size is due to the large number of unrolled loops.
Logically enough, we will start our examination of the code from the entry point:
EntryPoint
LD SP ,0x0
CALL Init
AND A
LAB_ram_a909
CALL NC,Frontend
LAB_ram_a90c
CALL NC,ShowHighScoreTable
CALL NC,DoNothing
JR NC,LAB_ram_a909 ; frontend loop
CALL NewGame
LAB_ram_a917
CALL RestartStage
CALL Play
CALL EndStage
JR NC,LAB_ram_a917
CALL TransitionToFromFrontend
JR LAB_ram_a90c
This is the outer loop. The stack is initialised to 0 to use the last page of memory. The call to Init sets up the vector table, generates pre-shifted copies of the game sprites and creates the left and right borders of the screen. The interrupt routine is set at $fdfe. This code is only run once and the memory here is subsequently reused for the background decompression buffer.
All the sprites are pre-shifted here. Some games use more complex systems where shifted versions of sprites are only created when play starts on the level on which the sprite appears - Elite's Ghosts 'n' Goblins is one such game. But here it's probably not worth it - there are relatively few sprites and they are almost all present on every stage.
The first half of this routine makes calls into the various frontend routines. These are not especially interesting and cover the usual activities of selecting controls, defining keys, choosing the number players.
These run until the player elects to start a new game, after which control falls out of the frontend loop and calls NewGame (sets up player state - initial lives, bombs, etc.) and enters the game outer loop. This manages starting a new stage (either a new stage, or the current stage after losing a life). We then enter Play where we remain until either completing the stage or losing a life. Once both players have lost all their lives, EndStage will set the carry and we return to the frontend loop.
This is a pretty neat outermost loop and very tidy compared to code in early games. Play is similarly compact.
Play
CALL WaitForKey
CALL ClearSfx
LD HL ,0xff00
LD (DirtyFlags ),HL
LD HL ,DirtyFlags
SET 0x2 ,( HL => DirtyFlags )
LD A,0x0
LD (EndStageReason ),A
LD (EndStageReasonCopy ),A
XOR A
LD (EndOfStageReached ),A
LD (FireDepressedCounter ),A
DEC A
LD (PlayeDeadRestartTimer ),A
DI
CALL ClearCollisionMasks
CALL DoCollisions
EI
CALL GenerateMacroTile
LD HL ,MainGameIsr
LD (IsrVector+1 ),HL
LAB_ram_a8f5
CALL DoLowPriorityTasks
LD A,(EndStageReasonCopy )
AND A
JR Z,LAB_ram_a8f5
LD A,(EndStageReason )
RET
What is most interesting here is that very little seems to be going on in the inner game loop ( LAB_ram_a8f5 ) - just a call to the routine DoLowPriorityTasks. This is because the main game loop runs under IM2 - the 50hz timer interrupt. All the game code runs from there with the exception of some latency tolerant tasks which are scheduled inside DoLowPriorityTasks:
DoLowPriorityTasks
LD HL,DirtyFlagsMask ; tasks that can run
LD A,(DirtyFlags) ; tasks that want to run
AND (HL)
RET Z ; nothing to do
LD B,(HL)
PUSH BC
LD B,0xff ; find the index of the
; highest set bit
LD C,A ; ( in reverse, so 0 will
LD A,B ; be bit 8 )
LAB_ram_a148
INC B
RRA
CP C
JR NC,LAB_ram_a148
AND C ; clear the bit
LD (DirtyFlags ),A
LD A,B
LD HL ,LowPriorityTasks
CALL JpHL ; jp to routine in table
POP AF
LD (DirtyFlagsMask),A
SCF ; did something
RE
This looks like an early version of the scheduling logic that ultimately became part of O.O.P.S - the thinking here is very similar to approach described by Andrew Braybook in his blog http://uridiumauthor.blogspot.com/. Only four tasks are used in flying shark.
LowPriorityTasks
NullOp
NullOp
NullOp
NullOp
GenerateTilesLO ; decompress some background tiles
RefreshStatusLO ; refresh stautus ( bombs and lives )
RefreshScoreLO ; refresh score
PlaySfxLO ; play some sound
These tasks update status information (score, bombs, etc.), play some sound and supply the main game loop with decompressed tiles. The Interrupt service routine is very busy running the game, so these routines only get to run occasionally. The simple priority system ensures the most important pending task is executed. For things like score updates this doesn't matter - the player won't notice if the score update is spread across multiple frames. The background decompression task is not quite as tolerant - the main loop does need to be supplied with a new row of tiles every 16 frames - so a 256 byte circular buffer sits between the decompression task and the main loop. This buffer overwrites the initialisation code.
The main ISR itself, once you strip out register saves/restores and some other minor bookkeeping, consists of the following routines:
CALL ApplyAttributes
CALL WriteCollisionMask
CALL DoCollisions
CALL ClearCollisionMasks
CALL DrawBuffer
CALL UpdateWater
CALL ScrollBackground
CALL CopyBackgroundToBackBuffer
CALL ProcessEvents
CALL TickObjects
CALL DrawObjects
CALL CheckForEndStage
These are:
ApplyAttributes. Set the attributes (colour) of the play area. This used for the smartbomb effect.
WriteCollisionMask, DoCollisions, ClearCollisionMasks. Do Collision Detection.
DrawBuffer. Copy the back buffer to display memory
UpdateWater. Animate water.
ScrollBackground. Scroll the background playfield and generate new tiles.
CopyBackgroundToFrontTileMap. Clear the back buffer by copying background tiles to the back buffer.
ProcessEvents. Update the event track for the stage and process any new events (e.g. spawning new waves of enemies)
TickObjects. Update the logic for all the active objects.
DrawObjects. Draw the sprites for all visible active objects.
CheckForEndStage. Check whether the stage has finished (due to completion or death).
I'll cover these in a future post.
Comments