Jump to content





Finishing Up (Minikernel Developer's Guide)

Posted by Karl G, in Minikernel Developer's Guide 01 November 2018 · 60 views

Now that we have a minikernel that can display a life icon for each player and be positioned correctly, we need to display the correct number of lives for each player.  We will do this by setting the NUSIZx register for each player to a value that displays the correct number of copies of the player object that corresponds to their remaining number of lives.  Before we do that, we need to talk about minikernel variables.
 
Minikernel Variables
 
Most minikernels will need some variables that will hold their value from frame to frame without getting overwritten by the display kernel or the user.  In our case, we will want to keep track of the number of lives for each player.  The documentation says that aux3 through aux6 are reserved strictly for minikernel use, but this isn't really true, as you can see looking at the standard kernel memory map:
 
Attached Image
 
While our sample game doesn't use pfheights or pfcolors, and won't use pfscore bars at the end, we will still avoid those variables to leave them free for projects that do make use of these options.  Since we only need two variables, we will use aux2 and aux6.
 
To make it easier, we will give these two variables another name in our minikernel, which will make our code more readable.  Place the following at the top of the minikernel before the "minikernel" label:

p0lives = aux2
p1lives = aux6

Note: Our minikernel in its current form does not need any temporary variables, but if we needed them, we could use temp1 through temp7, and kernelpointers through kernelpointers+5.  These can also be given alternate names for readability as we are doing for aux2 and aux6.

 
Setting NUSIZx
 
We will next need to set the appropriate NUSIZx register to the correct value for each player object to the appropriate value to display 1, 2 or 3 lives.  A NUSIZx value of 0 will display one copy of the player object, a NUSIZx value of 1 will display two copies, close spacing, and a NUSIZx value of 3 will display 3 copies, close spacing.  There is no way to display 0 copies with a NUSIZx register, so that case will be handled separately later.
 
We will create a lookup table that gives the appropriate NUSIZx based on the number of lives, as follows:
lives_nusiz
    .byte 0
    .byte 0
    .byte 1
    .byte 3
The lives for each player will be used as an offset for the data, so the first entry will be used for 0 lives, the second value will be used for 1 life, etc.  To do this, we load the appropriate lives variable into an index register, and use it as an offset when reading from our lives_nusiz table.  The Y register is already in use as a loop counter, so we will load it in X instead:
   ldx p0lives
   lda lives_nusiz,x
   sta NUSIZ0
 
   ldx p1lives
   lda lives_nusiz,x
   sta NUSIZ1
Here is the new version of the minikernel with our changes:
p0lives = aux2
p1lives = aux6

minikernel
    ldy player0height
    lda #0
    sta NUSIZ0
    sta NUSIZ1
    sta COLUBK
    sta WSYNC ; Start a new scanline at cycle 0
    sleep 21 ; Wait 21 cycles
    sta RESP0 ; Set player 0 position to current location
    sleep 34
    sta RESP1
    lda #$40
    sta HMP0
    lda #$60
    sta HMP1
    sta WSYNC
    sta HMOVE
    ldx p0lives
    lda lives_nusiz,x
    sta NUSIZ0
    ldx p1lives
    lda lives_nusiz,x
    sta NUSIZ1
    
GraphicsLoop
    sta WSYNC
    lda (player0pointer),y
    sta GRP0
    lda (player1pointer),y
    sta GRP1
    dey 
    bpl GraphicsLoop
 
    sta WSYNC
    lda #0
    sta GRP0
    sta GRP1

    rts

lives_nusiz
    .byte 0
    .byte 0
    .byte 1
    .byte 3
 
Modifying cannons.bas
 
The cannons game will need to be modified as well to make use of these changes.  Previously, the game used pfscore bars to track lives, so the variables pfscore1 and pfscore2 were used to track lives.  Furthermore, the lives were decremented by dividing by for (bit-shifting two places).  Instead now we will be using the p0lives and p1lives variables, initializing each of them to 3, and decrementing lives by subtracting 1 each time.  Here is the modified version of cannons.bas with these changes.
 
Attached File  cannons.bas (5.28KB)
downloads: 6
 
Compile the new version of the game, and launch it:
 
Attached Image
 
Success!  All 3 life icons appear as expected.  Some experimentation shows that the life icons decrement as expected as well, with the exception of zero icons being displayed if zero lives are left.
 
Displaying Zero Lives
 
There is no NUSIZx value that displays no copies of the player graphics, so we need another method to display no icons if the number of lives is zero.  Since we have already explicitly set the background color to black, we can set the appropriate COLUPx register to black to hide the life icon in this case.  First we load the appropriate lives variable into a register, and then skip the code that changes the color to black if the value is not zero.  If not skipped, we set the color to black.  Repeat for the other player.
 
Since we already load the p0lives and p1lives into register X, we can put the code to detect that case right after that code.  E.g. for player 0 right now we have:
    ldx p0lives
    lda lives_nusiz,x
    sta NUSIZ0
This will be changed to:
    ldx p0lives
    bne ____skip_hide_p0_icon ; goto the ____skip_hide_icon label if p0lives = 0
    stx COLUP0  ; Set the color of p0 to black to match the background color
____skip_hide_p0_icon
    lda lives_nusiz,x
    sta NUSIZ0
Similarly for player 1, we have:
    ldx p1lives
    lda lives_nusiz,x
    sta NUSIZ1
This will be changed to:
    ldx p1lives
    bne ____skip_hide_p1_icon ; goto the ____skip_hide_p1_icon label if p1lives = 0
    stx COLUP1  ; Set the color of p0 to black to match the background color
____skip_hide_p1_icon
    lda lives_nusiz,x
    sta NUSIZ1
After compiling the changes, the zero lives condition is properly displayed at the end of a game:
 
Attached Image
 
Consistent Height
 
If you have played around with the cannons game, you may have noticed that it exposes an issue with this minikernel: if player 0 is hit, the minikernel area for both players shrinks temporarily.  This is because our minikernel uses the value of the player0height variable as a loop counter, and the cannons game decreases the value of this variable when the player is hit to show the cannon vanish.
 
How can we fix this?  One way would be to compare player0height to player1height, and use whichever is greater for our loop counter instead.  Right now we have:
    ldy player0height
We want to compare the value in player0height to player1height, and use whichever is greater.  First, we will load player0height into register A first (the accumulator is used for comparisons like this), then we will compare to player1height, skipping some code to use its value instead if it is greater:
    lda player0height
    cmp player1height ; compare player0height to player1height
    bcs ____skip_use_player1height ; goto ____skip_use_player1height if not greater
    lda player1height
____skip_use_player1height
    tay ; transfer result to the Y register
Assembler Directives and Maximum Height
 
We don't need it for the cannons game, but some games may need the lives icons to only show part of the player graphics if they are above a certain height.  We can add some code to allow the user to add a const in their bB code that sets a maximum height for the lives icons, e.g. "const lives_max_height=8".  We can check for a constant being defined by using the assembler directive "ifconst".  This isn't an actual CPU instruction, but something that tells the assembler to only assemble the code following it if a symbol is defined (a constant in bB in this case).
 
Adding the following code below the code we added in the last step will use this constant only if it is defined.  Since the height is still in the accumulator at this point, we can skip a step and compare the defined maximum height to the value already in the accumulator:
    ifconst lives_max_height
     cmp #lives_max_height
     bcc ____skip_max_height ; Skip if the max height is taller than the player graphics
     ldy #lives_max_height ; Use lives_max_height instead
____skip_max_height
    endif
Unflipping Player Graphics
 
The cannons game doesn't use the REFPx registers to flip the direction of the players sprites, but some games using this minikernel may do so.  We will want to set the REFP0 and REFP1 registers to zero to avoid having the lives icons getting flipped as well.  We could do this anywhere before the GraphicsLoop label, but we will add it to where we are zeroing out other registers to save a step.  Right now we have:
    lda #0
    sta NUSIZ0
    sta NUSIZ1
    sta COLUBK 
We can add to this to use the same zero value in register A to clear out the REFPx registers:
    sta NUSIZ0
    sta NUSIZ1
    sta COLUBK
    sta REFP0
    sta REFP1
Summary and Final Code
 
We used a lookup data table to convert player lives to the appropriate NUSIZx value to display the correct number if life icons.  We set the color of the appropriate graphics to zero to hide it in the case of zero lives.  We added some code to use the taller of the player graphics to set our loop, and added some code to optionally allow the user to set a maximum height.  Finally, we cleared out the REFPx registers to our life icons would not be flipped if used in a project that flips the player graphics.
 
I am attaching the final versions of the code for the cannons game as well as the minikernel.  I hope this has been useful!
 
Attached File  cannons.bas (5.28KB)
downloads: 6
 
Attached File  2playerlives.asm (1.57KB)
downloads: 6