Jump to content

Guerrilla Game #5.3 -- The Final Puzzle Piece

Posted by vidak, 03 December 2017 · 77 views

Okay! So I think I have mastered how to draw the kernel that SpiceWare outlined in StayFrosty. If make sure you read all of the blog posts under the #5 heading so you understand what I am talking about.
Remember this his how the main player character is drawn in the kernel:

        ldy Heights+.SEC             


        lda (FrostyImagePtr+.SEC*2),y
        and (FrostyMaskPtr+.SEC*2),y 
        sta GRP0                     
        lda (FrostyColorPtr+.SEC*2),y
        sta COLUP0    

I     The "Heights" Variable


I asked SpiceWare for some help in understanding how the Heights variable worked. It turned out my analysis was more or less correct, despite some small errors. This is how the Heights variable is set:
                       ldy #SECTIONS*DATA_BYTES_PER_SECTION-4-1+2+2
                       ; -4 for the 4 unused platform bytes
                       ; +2 for fireball size info
                       ; +2 for level control init
LINE 2200:     .plLoop        
                       lda (LevelDataPtr),y
                       sta Heights,y
                       bpl .plLoop
There are 55 bytes of data in ROM, and the Y Register is loaded with the number 54. The BPL instruction for .plLoop causes the loop at line 2200 to loop through the number zero, so we go through the loop 55 times. This loop transfers 55 bytes of data into 55 bytes of ROM. It transfers the 5 bytes of the heights of the different bands into Heights, but then it also transfers the other 50 bytes of level data into the RAM variables after heights. My analysis in #5.2 was correct.

II     Preparing the Pointers for Sections 1 to 4


It was easy enough to understand how the pointers for the graphics were set up for Section 0 of the screen. This is because the bottom of the screen is the designated origin of the Y position of the main player character. But we need to adjust the Y positions of all the copies of the main player character up the screen. We will mask out all but one image of the main player character, but we still need to make the kernel attempt to draw the image 5 times up the screen before we mask it out.
So this is how we prepare the pointers for the above purpose:
LINE 2006:

        ldx #0
        ldy #0
        lda FrostyImagePtr,y    
        adc Heights,x           
        iny ; 1                 
        iny ; 2                 
        sta FrostyImagePtr,y   
        dey ; 1                
        lda FrostyImagePtr,y   
        adc #0
        iny ; 2
        iny ; 3
        sta FrostyImagePtr,y   

<Repeated Process for the Mask and Colours here>

        dey ; 2
        cpx #SECTIONS-1
        bcc pfLoop
What is happening in the above code? This is what is happening:
  • We are adding a 1 byte number to a 2 byte number. The process for doing this requires you to add the 1 byte number to the lower byte of the 2 byte number, and add any overflow to the higher byte. I did not realise this was happening! So if Heights (1 bytes) + ImagePtr(Lower byte) = 256, we add the carry put in the Program Counter to ImagePtr (Highter Byte).
  • So, what the code achieves overall is this:


    FrostyImagePtr1 = FrostyImagePtr0 + Height0
    FrostyImagePtr2 = FrostyImagePtr1 + Height1
    FrostyImagePtr3 = FrostyImagePtr2 + Height2
    FrostyImagePtr4 = FrostyImagePtr3 + Height3

  • This makes perfect sense. We are setting the minimum Y position of each image pointer as the top height of the band of the screen below it.

III     The Final Component of the Kernel: (.SEC*2)


The final element of drawing the main player character in the kernel is understanding what .SEC*2 does in the following code:
        lda (FrostyImagePtr+.SEC*2),y
This is what is happening:
  • .SEC is the argument passed into the kernel macro.
  • This argument is converted into a pseudo-variable in the code.
  • It is not a real variable because .SEC is not a piece of memory that contains data. .SEC is a set of characters that are mapped into the code depending on the value of the argument passed into the macro.
  • .SEC therefore specifies a number between 0 and 4 - which tells the kernel to draw a specific band of graphics up and down the screen.
What does " *2 " achieve though? This is simple. The image pointer is a 2 byte variable, so we need to tell the macro to specify in the code that the variable we want to specify in the final code is a 2 byte memory location. Simple.

IV     But What About the Mask and the Colour?


The colour pointer works in exactly the same way as the image pointer. It is like a second part of the image pointer that needs to be set up separately with its own code. The process of setting a colour pointer for a multi-band screen is perfectly isomorphic with setting the graphics pointer.
This is the data in ROM which sets out the mask used to let the main player character graphics through onto the correct band of the screen:
LINE 865:

        align 256
        repeat 50 ; 148-26 
        .byte 0
        repeat 26        
        .byte $ff
        repeat 50 ; 148-26
        .byte 0
The data is set out in macro code, but it is easy to interpret. It is 50 bytes (scanlines) of zeroes, followed by 26 bytes (scanlines) of $FF, followed by 50 bytes (scanlines) of zeroes. The origin of the pointer is the label FrostyMask, so we need to count down 26 bytes of memory address in order to draw the main player character, and count up 50 bytes in order to blank out the main player character. Once the kernel has counted down 26 bytes and drawn the character, there are then 50 bytes of zeroes, which will blank out drawing the graphics.
The idea is to have the 26 bytes of $FF set to the correct scanline of the screen, so that when we bitwise AND the player graphics on the first line for them to show, the first like of the $FF of the mask is loaded.

V     The End


That's it! Now I can get to writing my own kernel!

A minor comment on the use of masking.  You don't have to use masking, a typical sprite drawing routine like DoDraw will work just as well:
        lda #HUMAN_HEIGHT-1   ; 2  2 - height of the humanoid graphics, subtract 1 due to starting with 0
        dcp HumanDraw         ; 5  7 - Decrement HumanDraw and compare with height
        bcs DoDrawGrp0        ; 2  9 - (3 10) if Carry is Set, then humanoid is on current scanline
        lda #0                ; 2 11 - otherwise use 0 to turn off player0
        .byte $2C             ; 4 15 - $2C = BIT with absolute addressing, trick that
                              ;        causes the lda (HumanPtr),y to be skipped
DoDrawGrp0:                   ;   10 - from bcs DoDrawGrp0
        lda (HumanPtr),y      ; 5 15 - load the shape for player0
        sta GRP0              ; 3 18 - update player0 to draw Human
        lda (HumanColorPtr),y ; 5 23 - load color for player0
        sta COLUP0            ; 3 26 - update color for player 0

The reason I used masking was because it saved 5 cycles in the time critical kernel (at the expense of using more RAM for the MaskPtr, as well as ROM to hold the mask itself):
        lda (FrostyImagePtr+.SEC*2),y ; 5  5
        and (FrostyMaskPtr+.SEC*2),y  ; 5 10
        sta GRP0                      ; 3 13
        lda (FrostyColorPtr+.SEC*2),y ; 5 18
        sta COLUP0                    ; 3 21

I cover that in Stay Frosty, Part 3, where I also mention:

In hindsight I probably should have looked into using SwitchDraw to eliminate the Mask's extra RAM and ROM requirements as the Y register isn't used to count down the scan lines for the entire screen; instead, Y counts down the height of each platform zone. This means the Y's value is always between 0 and 127, which is one of the prerequisites for SwitchDraw. You can get a good overview of a number of sprite drawing options in this post by vdub_bobby.

  • Report

Yeah! I did notice that! You're totally right.


Thanks man!!

  • Report

December 2017

17 181920212223