At this point, we have engaged in enough yak shaving - that is, we have covered some assembly coding and made a minikernel do different things, but haven't made visible progress towards our ultimate goal: a minikernel that will display separate life icons for two players.
In this lesson, that will change: we will cover drawing player graphics, and use that in our minikernel. Before we get to that, there's a couple more simple assembly commands I want to cover that I didn't get to in the last lesson (it was already long enough). I will then explain indexed addressing and pointers, which will be needed to load our graphics.
Register Transfer
These are simple instructions that copy the contents of one register to another. Note that there are no instructions to directly copy from X to Y or vice versa, however:
TAX (Transfer A to X): Transfer the contents of the A register to the X register.
TXA (Transfer X to A): Transfer the contents of the X register to the A register.
TAY (Transfer A to Y): Transfer the contents of the A register to the Y register.
TYA (Transfer Y to A): Transfer the contents of the Y register to the A register.
JMP, JSR, and RTS
JMP (JuMP): Go to the specified address/label
JSR (Jump to SubRoutine): Go to the specified address/label, and store the return address in the stack.
RTS (ReTurn from Subroutine): Go back to the address stored in the stack.
JMP is fully equivalent to a goto, JSR is fully equivalent to a gosub, and RTS is fully equivalent to a return statement in batari Basic.
Note - There is no way to specify a destination bank with a JMP or JSR command. Bankswitching is a more advanced topic that we will not cover in this guide.
Batari Basic Code:
goto CoolNewPlace . . .CoolNewPlace (more code) gosub MySubroutine . . .MySubroutine (more code) return
Equivalent assembly code:
jmp CoolNewPlace . . .CoolNewPlace (more code) jsr Subroutine . . .MySubroutine (more code) rts
Defining and Accessing Data
Defining data can be done by marking the beginning with a label, and defining the bytes. It can be in decimal, hex, or binary just like in bB, and either multiple bytes defined per line separated by commas, or one byte per line.
shoot_sound .byte 8,8,2 .byte 1 .byte 8,8,8 .byte 1 .byte 8,8,12 .byte 1 .byte 8,8,19 .byte 1 .byte 8,8,23 .byte 1 .byte 2,8,27 .byte 4 .byte 255rainbow_fruit .byte %00011100 .byte %00111110 .byte %00111110 .byte %01111111 .byte %01111111 .byte %00111110 .byte %00010100rainbow_fruit_colors .byte $6A .byte $7A .byte $8A .byte $CA .byte $1A .byte $3A .byte $4A
As mentioned in the previous lesson, the X and Y registers are referred to the index registers, as they can be used as an offset to an address when loading and saving values.
ldy #0 lda my_data,y ; load first byte of my_data ldy #5 lda my_data,y ; load 6th byte of my_data
Comparison to batari Basic
Here's an example of data being defined and accessed in bB vs how we would do the same in assembly code:
batari Basic Code:
data sound_data 12,5,24 . . .end temp1=0 AUDC0=sound_data[temp1] temp1=temp1+1 AUDV0=sound_data[temp1] temp1=temp1+1 AUDF0=sound_data[temp1]
Equivalent assembly code:
sound_data .byte 12,5,24 . . . ldy #0 lda sound_data,y iny sta AUDC0 lda sound_data,y sta AUDV0 iny lda sound_data,y sta AUDF0
Pointers and Player Data
Finally we can talk about drawing player objects to display lives. One way to do it would be to define data for each of the player objects, and then access that in our minikernel to draw the player lives. However, we already defined the player graphics in our batari Basic code, so it may make more sense to just use that instead of repeating the same data.
So, where is this data stored? The answer is not quite as simple as you might think. In a batari Basic program, you can change the player graphics as often as you want. Since this is stored in ROM, and ROM can't change, it stands to reason that each version of the player graphics is stored separately as data. How does the program know which copy of the player graphics to use?
This is where pointers come in handy. Pointers are two adjacent bytes of RAM that store a memory address. When you read from a pointer, you read from the address that is stored in the pointer instead of what is stored in the pointer variables themselves.
In the case of a game with player graphics that change (e.g. for animation frames), each of the animation frames would be stored as data, and two bytes are set aside to store the address of the current frame of the animation. Every time the player graphics changes, the pointer is updated to contain the address of the new frame.
bB uses pointers for player graphics "under the hood" as well. For our purposes right now, I am just going to cover how to access this data in order to use it in our minikernel.
player0pointer and player1pointer are the names of the pointer variables bB uses to store the current player graphics. The player0height and player1height variables contain the heights of the current player graphics. Here is an example of how we would access this data:
ldy #0 lda (player0pointer),y ; Load first byte of player0 graphic into A. ldy player1height dey lda (player1pointer),y ; Load last byte of player1 graphic into A.
Note - When accessing static data, the X and Y registers can be used interchangeably as indexes, but when accessing data via pointers, they have different functions. I will not cover the form of pointer data access that uses the X register, as it is much less commonly used.
Displaying Player Graphics
We have covered how to access player graphics, but how do we display them? There are two TIA registers for this purpose: GRP0 and GRP1 that hold the player 0 and player 1 graphics, respectively. Note that each of these registers only hold 1 byte of data, enough to display 1 scanline of graphics. A simple example of code to display a player from static data might look something like this:
ldy #player_height - 1PlayerLoop sta WSYNC lda (player0pointer),y sta GRP0 dey bpl PlayerLoop
Back to Our Minikernel
Now that we know how to display player graphics from the existing pointers in bB, let's try putting these in our minikernel. Everything we have done up to this point has just been for demonstration purposes, and won't be part of our final minikernel. Let's discard that, and leave just this:
minikernel rts
We have the label that the bB kernel goes to (via a JSR command) and the command that returns back to the bB kernel at the end (RTS). We will add our new code in between these.
First, we need a loop counter. As previously mentioned, bB stores the height of each of the player graphics in variables: player0height and player1height. For purposes of this minikernel, we will assume that both player graphics are the same height.
We will first load player0height into Y:
ldy player0height
Now, let's add a loop label again; maybe we will call this one GraphicsLoop, and start with a "sta WSYNC" again at the start of the loop to start at the beginning of a scanline:
GraphicsLoop sta WSYNC
We can then add the commands to load data from the bB-defined player graphics pointers, and store that data in the player graphics registers:
lda (player0pointer),y sta GRP0 lda (player1pointer),y sta GRP1
Finally, we decrement Y, and branch back to GraphicsLoop as long as Y is 0 or above:
dey bpl GraphicsLoop
Putting it all together, we now have this:
minikernel ldy player0heightGraphicsLoop sta WSYNC lda (player0pointer),y sta GRP0 lda (player1pointer),y sta GRP1 dey bpl GraphicsLoop rts
We can put that in out minikernel file, save it, recompile cannons.bas and launch it in Stella:
What do we see now? The good news is that the player graphics do display in the minikernel area. However, there are a few oddities:
- The minikernel graphics are "squished" compared to in the game itself.
- The background color carries over into the minikernel area.
- There are graphic artifacts at the top of the screen above each player.
-
The minikernel graphics stay in the same horizontal position as the players, even as they move.
For the first issue, you may recall from the first lesson that each line of graphics in a batari Basic screen is drawn using two scanlines, whereas some minikernels use one scanline per line of graphics. We have just created such a minikernel!
In our case, we have a simple solution for the squished-looking graphics. In out cannons game, we set the players to double-width via the NUSIZx registers, and this carries over to the minikernel. This is easily fixed by resetting each of these to 0 before our loop. While we're at it, we can also take care of the second issue by setting the background color to 0 as well:
lda #0 sta NUSIZ0 sta NUSIZ1 sta COLUBK
Let's save, recompile, and take a look:
Much better! Now how about the graphic artifacts at the top of the screen?
When the player graphics registers (GRP0 and GRP1) have any value other than 0, they will keep displaying their contents on every visible scanline until the contents are cleared. To fix this, we will zero out these after our loop. We will put a "sta WSYNC" first so that the last scanline has a chance to display before the graphics registers get cleared out:
sta WSYNC lda #0 sta GRP0 sta GRP1
Let's see how it looks with our changes after we recompile again:
All fixed! Now, how about the horizontal position issue? That is a more complex topic that we will cover in the next lesson.
Here is our minikernel source as it should look at the end of this lesson:
minikernel ldy player0height lda #0 sta NUSIZ0 sta NUSIZ1 sta COLUBKGraphicsLoop sta WSYNC lda (player0pointer),y sta GRP0 lda (player1pointer),y sta GRP1 dey bpl GraphicsLoop sta WSYNC lda #0 sta GRP0 sta GRP1 rts
Summary and Next Lesson
Data can be defined in a way that is similar to what you are probably used to in batari Basic. We can access data by loading it using the label at the start of the data with an offset that is stored in one of the index registers (X or Y). Pointers are two adjacent bytes in memory that store an address, such as player graphics data. Pointers are useful because they can be updated as needed, such as pointing to different frames of animation for player graphics data.
The GRP0 and GRP1 registers store one byte of graphics data for player0 and player1, respectively, which is enough for a single scanline. Player graphics are displayed by loading and displaying graphics data one scanline at a time.
In our next lesson, we will cover scanline timing, and horizontal positioning of graphics.
2 Comments
Recommended Comments