top of page
Search
Writer's pictureTom Grove

Flying Shark

Updated: Jan 20, 2021

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.




83 views0 comments

Recent Posts

See All

Flying Shark : Object Logic

Every object has a type field ( 0x5 ) that selects a tick routine. The tick routine for a bomb power-up is shown below (link to video)....

Comments


bottom of page