Saturday, September 14, 2019

TIA Sprite Implementation

Now that we have a sprite class for the internal processing of the TIA chip Player Missile Graphics (which I will refer to as sprites), we need to implement the TIA registers that handle the various sprite activity and adjust our sprites to reflect the register settings. First, we create the 5 sprites that the TIA uses. This is done in an array class variable with constants set up in the TIAPIARegs singleton class that hold the index values of each sprite.

val sprites = arrayOf(PlayerMissileGraphic(1, 30570), // Ball
PlayerMissileGraphic(8, 15423), // Player 1
PlayerMissileGraphic(1, 1023), // Missile 1
PlayerMissileGraphic(8, 28377), // Player 0
PlayerMissileGraphic(1, 24007) ) // Missile 0

The most important thing for a sprite is to be able to be shown. The ball and missiles have an enable register which when bit 1 is set will show the sprite otherwise will hide the sprite. For the player sprites, set the byte that represents the image to 0 for the sprite not to be drawn and fill it with the display data to have the sprite draw something.

TIAPIARegs.ENABL -> sprites[TIAPIARegs.ISPRITE_BALL].drawingBits = if ((value and 2) == 2) 1 else 0
TIAPIARegs.ENAM0 -> sprites[TIAPIARegs.ISPRITE_MISSILE0].drawingBits = if ((value and 2) == 2) 1 else 0
TIAPIARegs.ENAM1 -> sprites[TIAPIARegs.ISPRITE_MISSILE1].drawingBits = if ((value and 2) == 2) 1 else 0
TIAPIARegs.GRP0 -> sprites[TIAPIARegs.ISPRITE_PLAYER0].drawingBits = value and 255
TIAPIARegs.GRP1 -> sprites[TIAPIARegs.ISPRITE_PLAYER1].drawingBits = value and 255

Being able to see the sprite is nice, but we need to control where on the scan-line the sprite will be displayed. It would be nice if we just had a register that held the position information of the sprites, but for some reason that is beyond me this is not how sprite positioning works on the TIA. To position a sprite you simply write anything into the RESXX register and it will position the sprite at the current color clock position or position 0 if called in the horizontal blanking interval.

TIAPIARegs.RESBL -> sprites[TIAPIARegs.ISPRITE_BALL].x = column
TIAPIARegs.RESP0 -> sprites[TIAPIARegs.ISPRITE_PLAYER0].x = column
TIAPIARegs.RESP1 -> sprites[TIAPIARegs.ISPRITE_PLAYER1].x = column
TIAPIARegs.RESM0 -> sprites[TIAPIARegs.ISPRITE_MISSILE0].x = column
TIAPIARegs.RESM1 -> sprites[TIAPIARegs.ISPRITE_MISSILE1].x = column
TIAPIARegs.RESMP0 -> sprites[TIAPIARegs.ISPRITE_MISSILE0].x = sprites[TIAPIARegs.ISPRITE_PLAYER0].x
TIAPIARegs.RESMP1 -> sprites[TIAPIARegs.ISPRITE_MISSILE1].x = sprites[TIAPIARegs.ISPRITE_PLAYER1].x

The problem with this is that each CPU cycle is 3 color clock cycles and a STA instruction takes at least 3 cycles to complete. This means that we need to adjust the horizontal position using the move delta. This is done at the start of the scanline by writing to HMOVE with any value. The amount each sprite moves is a signed nibble set by HMxx registers with a HMCLR register that sets all the HMxx registers to zero. We have a utility method to convert a nibble into a signed integer as follows

private fun convertNibbleToSignedInt(nibble:Int):Int {
val nib = nibble and 15
return if (nib > 7) nib - 16 else nib
}

And within the writeRegister when statement we have the following registers implemented:

TIAPIARegs.HMP0 -> sprites[TIAPIARegs.ISPRITE_PLAYER0].deltaX = convertNibbleToSignedInt(value shr 4)
TIAPIARegs.HMP1 -> sprites[TIAPIARegs.ISPRITE_PLAYER1].deltaX = convertNibbleToSignedInt(value shr 4)
TIAPIARegs.HMM0 -> sprites[TIAPIARegs.ISPRITE_MISSILE0].deltaX = convertNibbleToSignedInt(value shr 4)
TIAPIARegs.HMM1 -> sprites[TIAPIARegs.ISPRITE_MISSILE1].deltaX = convertNibbleToSignedInt(value shr 4)
TIAPIARegs.HMBL -> sprites[TIAPIARegs.ISPRITE_BALL].deltaX = convertNibbleToSignedInt(value shr 4)
TIAPIARegs.HMOVE -> {
for (cntr in 0..4)
sprites[cntr].hmove()
}
TIAPIARegs.HMCLR -> {
for (cntr in 0..4)
sprites[cntr].deltaX = 0
}

Sprite scaling and mirroring is done through the NUSIZx register with the copy/scale mode of the player sprites being the lower 3 bits and the scaling of missile for that player being two to the power of the two bit number occupying bits 4 and 5. In other words, we take the last three bits to form a number between 0 and 7 which is used to set the player sprite scaling and copies via the setePlayerScaleCopy method that we wrote in the previous article. The missile scaling is bits 4 and 5 which when shifted right become a number between 0 and 3, this is used as a power of two which can be done by left-shifting a 1 by the given number which would result in 1, 2, 4, or 8.

TIAPIARegs.NUSIZ0 -> {
sprites[TIAPIARegs.ISPRITE_PLAYER0].setPlayerScaleCopy(value and 7)
sprites[TIAPIARegs.ISPRITE_MISSILE0].scale = 1 shl ((value shr 4) and 3)
}
TIAPIARegs.NUSIZ1 -> {
sprites[TIAPIARegs.ISPRITE_PLAYER1].setPlayerScaleCopy(value and 7)
sprites[TIAPIARegs.ISPRITE_MISSILE1].scale = 1 shl ((value shr 4) and 3)
}

Finally, mirroring is simply setting the sprite mirror variable based on the state of bit 3 of the value passed to the REFx register.

TIAPIARegs.REFP0 -> sprites[TIAPIARegs.ISPRITE_PLAYER0].mirror = (value and 8) == 8
TIAPIARegs.REFP1 -> sprites[TIAPIARegs.ISPRITE_PLAYER1].mirror = (value and 8) == 8

That just about covers all the write registers for manipulating sprites, but there are also a set of eight readable registers that are used for handling collision detection. This is a bit complex of a subject, though is a lot easier to use than one would expect and will be covered next time.

No comments:

Post a Comment