Saturday, August 17, 2019

The 2600 Play-field

The play-field is a 40-bit image that is stretched across the 160 pixels that make up a scan-line. This means that each play-field bit occupies 4 pixels. The original purpose behind the play-field is to create the map or maze that the player occupies. It is often used for other things such as drawing scores, background images or title screens. Anything that can be represented using crude graphics is fine.

The play-field is managed using the CTRLPF, PF0, PF1 and PF2 registers. The CTRLPF register controls how the play-field will be displayed controlling the display order of the play-field, whether the bits should be repeated or mirrored, and if the play-field should be drawn using the play-field color or drawn using player 0 color on the left and player 1 color on the right. This register also handles scaling of the ball which will be covered later.

The PF0, PF1 and PF2 registers hold the 20 bits that are to be drawn. These bits will either be repeated or mirrored based on the settings in CTRLPF. As with color, you can change the play-field bits in the middle of drawing so that the right side of the screen is different from the left side of the screen. The order of the bits is weird which is likely a by-product of keeping the transistor count of the chip down. The image below shows the order of the pixels that make up the display.



Essentially the play-field drawing starts with the 4 bits in the PF0 register drawn in low to high order. This is followed by the eight bits from PF1 in high to low order. Finally the PF2 bits are drawn in low to high order.  This is repeated or mirrored for the right half of the screen. As many games used symmetrical maps this made it easy to set up the play-field bits and simply mirror them. Asymmetric maps are a bit trickier as you need to set up the PF0, PF1, and PF2 registers for the left half of the screen then wait until the beam is past PF0 to change PF0 to the right side PF0, wait until the beam is past PF1 to change to the right side PF1 and wait until the beam is past PF2 to change to the right side PF2. While you could theoretically do this while mirroring, asymmetrical images are easier to do with repeated mode unless you don’t need to change PF2.

The hard part about implementing the play-field is dealing with the different orientations of the data. My internal representation of the play-field data is simply a 20 bit integer with the bits being the play-field bits from left to right. When one of the PF registers is called, the appropriate bits of this play-field value are set. This requires that we flip the bits, but this is a simple process of using and to see if a bit is set or to build the reversed byte and shifts to get the bits aligned properly.

fun reversePFBits(value:Int):Int {
var reversed = 0
var testBit = 1
for (cntr in 0..7) {
reversed *= 2
reversed += if ((testBit and value) > 0) 1 else 0
testBit *= 2
}
return reversed
}

The calls to the playfield register simply take the value that is to be written to the register and, if necessary, reverse the bits. It then masks out the bits in the playfield that the register represents, shifts the bits to the appropriate part of the playfield then uses a logical or to put the new value in place. The playfield control register CTRLPF simply sets flags to indicate how the playfield should be rendered. The flags are mirror to indicate if mirroring should be taking place, scoreMode to determine if we are drawing using the playfield color or the player 0 color on the left and the player 1 color on the right. The priorityPlayfield flag determines if the playfield should be drawn first or last.

TIAPIARegs.PF0 -> {
var bits = reversePFBits(value)
playfieldBits = (playfieldBits and 0xFFFF) or (bits shl(16))
}
TIAPIARegs.PF1 -> {
var bits = value
playfieldBits = (playfieldBits and 0xF00FF) or (bits shl(8))
}
TIAPIARegs.PF2 -> {
var bits = reversePFBits(value)
playfieldBits = (playfieldBits and 0xFFF00) or bits
}
TIAPIARegs.CTRLPF -> {
mirror = (value and 1) == 1             // bit 0 handles mirror mode
scoreMode = (value and 2) == 2          // bit 1 handles score mode
priorityPlayfield = (value and 4) == 4  // bit 2 Places playfield above sprites
}

To draw the play-field we simply determine which play-field pixel should be processed. The left is simply the column being drawn divided by 4. The right is the same (less 80) if repeated and the inverse if mirrored. Once we know the bit to be checked we can determine if there is a pixel there and if so set the color based on the scoreMode. We then adjust our collision mask which will be explained in the collisions section later but is essentially a set of bits indicating which collisions have happened.


val pfCol = if (column < 80) column / 4 else
if (mirror) (159 - column) / 4 else (column - 80) / 4
var pfPixelMask = 0x80000 shr pfCol
val shouldDrawPlayfield =(playfieldBits and pfPixelMask) > 0
val pfColor = if (scoreMode) {if (column < 80) playerMissile0Color else
playerMissile1Color } else playfieldBallColor
if ( ! shouldDrawPlayfield) collisionMask = collisionMask and 31668


At the start of the drawing of the pixel, we check to see if the playfield is normal priority and if so draw it.

if ((shouldDrawPlayfield) and ( ! priorityPlayfield )) pixelColor = pfColor
// TODO sprite drawing code here 
if ((shouldDrawPlayfield) and (priorityPlayfield)) pixelColor = pfColor


Finally, at the end of the drawing of a pixel, we see if the playfield is priority and if so draw it. Between these two checks is the sprite drawing which is the next thing to be implemented.

No comments:

Post a Comment