Pobtastic / 180 / Dartboard Reveal

Created Wed, 04 Dec 2024 13:33:09 +0100 Modified Thu, 12 Dec 2024 14:35:47 +0000

Introduction

I think that, it’s so easy to overlook a simple visual effect these days; Full-Motion Video (FMV) makes it so that we tend to see something nifty and “overlook it” as for FMV anyway, it’s rendered on a much more powerful computer and just played back as video. However, on the ZX Spectrum … the lack of storage space (and other limitations) led to some crazy inventive programming!

This is the “zipper” reveal when moving between the main menu and the game menu. It’s simply … STUNNING:

Dartboard Reveal

And lasts for such a brief amount of time (and isn’t used anywhere else in the game either). In a way, it’s so unnecessary! No-one would be complaining if it wasn’t there - it kind of reminds me a little of the “demoscene” type stuff, where programmers are showing off their skills making nifty animations and cut-scenes (which aren’t pre-rendered FMV) to really push the limits of what’s possible. Okay, maybe this isn’t quite that - but oh wow, it is so pretty! For me personally, this really is digitial art.

…And let’s just remember too, this is a budget game. There’s no requirement for it to be anything other than “budget” standard.

Routine: Main Menu

The starting point!

Main Menu

Obviously there’s no need to go through all the code which manages the main menu. I’ll just describe the relevant parts right after the player presses “start game”.

On entry here, the A register contains the selected item reference:

Byte Meaning
$00 1 Keyboard
$01 2 Kempston Joystick
$02 3 Interface Two
$03 4 Cursor Joystick
; First calculate the co-ordinates of the selected menu item.
 $9408 ADD A,A                    ; {The menu items are spaced two rows apart, so double the value in
 $9409 ADD A,$05                  ; A and also they start five rows from the top of the screen so add
 $940B LD D,A                     ; that too and store the result in D.}
 $940C LD E,$01                   ; The dart graphic is indented by one column - store this in E.
; Initialise the animation counter.
 $940E LD B,$3E                   ; Set a counter in B of the number of loops to complete the
                                  ; animation ($3E).
; The main animation loop. Each iteration reveals one more column of the transition.
@label=RevealDartboard_AnimationLoop
*$9410 PUSH BC                    ; {Stash the reveal loop counter and dart pointer co-ordinates
 $9411 PUSH DE                    ; on the stack.}
; Synchronise with the screen refresh so the animation is smooth.
 $9412 HALT                       ; Halt operation (suspend CPU until the next interrupt).
 $9413 CALL $9305                 ; Call MainMenu_PrintDartPointer.
; Update the dart pointer position.
 $9416 POP HL                     ; Restore the dart pointer co-ordinates from the stack into HL.
 $9417 PUSH HL                    ; But keep a copy on the stack for later.
 $9418 INC L                      ; Move the dart pointer right one position.
; Draw the dart pointer at the new position.
 $9419 CALL $9457                 ; Call PrintDartPointer.
; Process one step of the reveal.
 $941C POP DE                     ; Restore the dart pointer co-ordinates from the stack into DE.
 $941D PUSH DE                    ; But keep a copy on the stack for later.
 $941E CALL $9253                 ; Call RevealDartboard.
 $9421 POP DE                     ; Restore the dart pointer co-ordinates from the stack.
 $9422 INC E                      ; Move the dart pointer right one position.
 $9423 POP BC                     ; Restore the reveal loop counter from the stack.
 $9424 DJNZ $9410                 ; Decrease the reveal loop counter by one and loop back to
                                  ; RevealDartboard_AnimationLoop until the dartboard is fully revealed.

This loop is basically driving the dartboard pointer from the left, over to the righthand side of the screen. The magic happens below…

Routine: Reveal Dartboard

And this is what we’re uncovering.

Dartboard

The routine below utilises DE (which is tracking the dart pointer), and stores two pointers; ZipperPosition_Upper and ZipperPosition_Lower. It uses these pointers to create the “zipper” and simply moves up and down on each pass of the loop until it detects that one is out-of-bounds.

; Reveal Dartboard
; 
; Creates a "zipper" transition effect that reveals the dartboard from a central point,
; expanding both upwards and downwards simultaneously.
;
; ------------------------------
; | Input                      |
; ------------------------------
; |DE|Dart pointer co-ordinates|
; ------------------------------
;
@label=RevealDartboard
; The zipper trails behind the dartboard pointer.
c$9253 DEC E         ; Move one position left to start the reveal.
 $9254 LD ($9AB9),DE ; Store this position to *ZipperPosition_Upper.
 $9258 INC D         ; Move one position down and store this
 $9259 LD ($9ABB),DE ; Store this position to *ZipperPosition_Lower.

; Process the upper half of the reveal.
@label=RevealDartboard_Loop
*$925D LD DE,($9AB9) ; Fetch the *ZipperPosition_Upper value again.
 $9261 LD A,E        ; {Jump to RevealDartboard_Done if E becomes negative
 $9262 AND A         ; (if it's gone past the left edge).
 $9263 JP M,$92A6    ; }
 $9266 PUSH DE       ; Stash the upper posision on the stack.
 $9267 LD A,D        ; {Jump to UpperReveal_Done if D becomes negative (if
 $9268 AND A         ; it's gone past the top of the screen).
 $9269 JP M,$927C    ; }

; Set up the right-shifting mask for the upper reveal.
; Self-modifying code;
@label=RevealDartboard_ModifyMaskForUpper
 $926C LD HL,$92C7   ; {Set the mask value to $FF at *Reveal_MaskValue.
 $926F LD (HL),$FF   ; }
@label=RevealDartboard_ModifyToShiftRight
 $9271 LD HL,$92CD   ; {Write "SRL C" ($CB+$39) to *Reveal_ShiftCommand.
 $9274 LD (HL),$CB   ;
 $9276 INC HL        ;
 $9277 LD (HL),$39   ; }
 $9279 CALL $92A7    ; Call ProcessRevealLine.
@label=UpperReveal_Done
*$927C POP DE        ; Restore the original upper position from the stack.
 $927D DEC D         ; {Move up one line and left one column for the next upper
 $927E DEC E         ; reveal position.}
 $927F LD ($9AB9),DE ; Store the updated upper position at *ZipperPosition_Upper.
; Process the lower half of the reveal.
 $9283 LD DE,($9ABB) ; DE=*ZipperPosition_Lower.
 $9287 LD A,D        ; {Jump to RevealDartboard_Done if D is greater than or equal
 $9288 CP $18        ; to $18 (if it's gone past the bottom of the screen).
 $928A JR NC,$92A6   ; }

; Set up the left-shifting mask for the lower reveal.
 $928C PUSH DE       ; Stash the lower position on the stack.
; Self-modifying code;
@label=RevealDartboard_ModifyMaskForLower
 $928D LD HL,$92C7   ; {Set the mask value to $01 at *Reveal_MaskValue.
 $9290 LD (HL),$01   ; }
@label=RevealDartboard_ModifyToShiftLeft
 $9292 LD HL,$92CD   ; {Write "SLL C" ($CB+$31) to *Reveal_ShiftCommand.
 $9295 LD (HL),$CB   ;
 $9297 INC HL        ;
 $9298 LD (HL),$31   ; }
 $929A CALL $92A7    ; Call ProcessRevealLine.
 $929D POP DE        ; Restore the original lower position from the stack.
 $929E INC D         ; {Move down one line and left one column for the next lower
 $929F DEC E         ; reveal position.}
 $92A0 LD ($9ABB),DE ; Store the updated lower position at *ZipperPosition_Lower.
 $92A4 JR $925D      ; Jump back to RevealDartboard_Loop.

; All finished for this frame, so return.
@label=RevealDartboard_Done
*$92A6 RET           ; Return.

Routine: Process Single Line Zipper Reveal

Lastly, the reveal routine itself - this uses SLL or SRL to create the masked area (which is flipped by self-modifying code in the previous routine).

; Process Single Line Zipper Reveal
;
; --------------------------------
; | Input                        |
; --------------------------------
; |DE|Current reveal co-ordinates|
; --------------------------------
;
; Handles the pixel manipulation for one line of the reveal effect,
; including both straight copying of already-revealed areas and masked reveal
; of transition areas.
;
@label=ProcessRevealLine
c$92A7 PUSH DE       ; Stash the reveal co-ordinate on the stack.
; First handle the already-revealed portion.
 $92A8 DEC E         ; Decrease E by one for the check below.
; Is the X coordinate within the screen boundaries?
 $92A9 LD A,E        ; {Jump to ProcessRevealLine_Skip if ($00 <= E < $20).
 $92AA CP $20        ;
 $92AC JR NC,$92B5   ;
 $92AE AND A         ;
 $92AF JP M,$92B5    ; }
 $92B2 CALL $92E2    ; Call CopyRevealLine.
@label=ProcessRevealLine_Skip
*$92B5 POP DE        ; Restore the reveal co-ordinate from the stack.
 $92B6 LD A,E        ; {Return if X is past the right-hand edge of the screen.
 $92B7 CP $20        ;
 $92B9 RET NC        ; }
 $92BA PUSH DE       ; Stash the reveal co-ordinate on the stack.
; On return from CalculateDartBoardAddress HL will contain the dart board graphic
; destination (i.e. DartBoard onwards).
 $92BB CALL $A8BD    ; Call CalculateDartBoardAddress.
; Set up the source and destination addresses.
 $92BE LD D,H        ; {Copy the dart board graphic location from HL into
 $92BF LD E,L        ; DE.}
 $92C0 LD A,H        ; {H-=$20.
 $92C1 SUB $20       ;
 $92C3 LD H,A        ; }

; Process $08 pixel rows with the reveal mask.
 $92C4 LD B,$08      ; Set a counter in B for the number of pixels rows to
                     ; process.
; Mask value. Altered to either;
; $01 at RevealDartboard_ModifyMaskForLower.
; $FF at RevealDartboard_ModifyMaskForUpper.
@label=Reveal_MaskValue
 $92C6 LD C,$FF      ; Set the initial mask value.
@label=RevealDartboard_MaskLoop
*$92C8 LD A,(DE)     ; Get the dartboard graphic byte.
 $92C9 OR C          ; Apply the reveal mask.
 $92CA LD (HL),A     ; Write the result to the screen.
 $92CB INC H         ; Move to the next screen line.
 $92CC INC D         ; Move to the next dartboard graphic line.
; Shift command. Altered to either;
; "SLL C" at RevealDartboard_ModifyToShiftLeft.
; "SRL C" at RevealDartboard_ModifyToShiftRight.
@label=Reveal_ShiftCommand
 $92CD SRL C         ; Shift the mask value.
 $92CF DJNZ $92C8    ; Decrease the pixel row counter by one and loop back to
                     ; RevealDartboard_MaskLoop until all $08 lines have been processed.

; Work out which attribute byte to apply.
 $92D1 POP HL        ; Restore the original reveal co-ordinate from the stack.
 $92D2 LD A,L        ; {Is the co-ordinate in the menu or dartboard area?
 $92D3 CP $08        ; }
; Default with the dartboard attribute value.
 $92D5 LD A,$70      ; A=INK: BLACK, PAPER: YELLOW (BRIGHT).
 $92D7 JR NC,$92DB   ; Jump to RevealDartboard_SetAttribute if the X
                     ; co-ordinate is greater than or equal to $08.
; The X co-ordinate was less than $08 so use the menu attribute value.
 $92D9 LD A,$00      ; A=INK: BLACK, PAPER: BLACK.
; Stash the attribute byte temporarily as CalculateAttributeBuffer needs the A
; register.
@label=RevealDartboard_SetAttribute
*$92DB EX AF,AF'     ; Temporarily switch the attribute byte with the shadow AF
                     ; register.
; CalculateAttributeBuffer converts given co-ordinates into an attribute buffer
; location (into HL).
 $92DC CALL $A862    ; Call CalculateAttributeBuffer.
 $92DF EX AF,AF'     ; Restore the attribute byte back from the shadow AF
                     ; register.
 $92E0 LD (HL),A     ; Write the colour attribute byte to the attribute buffer.
 $92E1 RET           ; Return.