Saturday, August 31, 2019

Sprites are Player Missile Graphics

To plan out the classes that I would use for creating the player and missile sprites I put together the following table which shows the various registers used for the different player and missiles as well as how the different objects compare to each other. Table 1 is a handy chart to have for programming the 2600 and wish I would have created it when I first started playing around with the 2600!


The original plan was to have a base class that held the common functionality and have the player class and missile/ball classes override this class. This is probably the “proper” way to do this but the classes are so similar that having them combined into a single class seemed like the easiest solution. A missile or ball is essentially a sprite that is only 1 pixel so by simply tracking the number of pixels as size, all the different player missile graphics can be incorporated.

Sprites have a collision mask that will be used for determining which collision flags they are part of. This will be explained later when we implement collisions but for now it can be thought of as simply a magic number with each bit in the number representing a particular pair of objects that can collide with each other.

Scale is how many pixels to draw for each pixel in the sprite. The 2600 supports scales of 1, 2, 4, and 8 with player sprites limited to a maximum scale of 4. The number of copies is how many times to draw the sprite while the gap is the distance (in multiples of the size) between the copies. The 2600 supports distances of 2, 4 and 8.

The sprite is placed on the screen at the x location with the deltaX variable being how much to alter the x position when an HMOVE command is issued. Normally we do not want the sprite to move so the default is 0.

The drawing bits are the byte used to represent the image in the sprite at that horizontal scanline. It can be considered a bit of a kludge when using missiles and balls as only the one bit is used. Mirroring is used to indicate which order the pixels should be drawn. For balls and missiles it will mirror the single pixel but as the result of that is the original image there is not much use for size 1 sprites.

class PlayerMissileGraphic(var size:Int, var collisionMask:Int) {
var scale = 1
var copies = 1
var distanceBetweenCopies = 0   // close = 2, med = 4 far = 8
var x = 0
var deltaX = 0
var drawingBits = 0
var delayedDraw = false
var mirror = false

The heart of the sprite class is the isPixelDrawn method which simply determines if the current state of the sprite would result in a pixel being drawn at the indicated location. This is simply a matter of looping through the number of copies of the sprite and for each copy being drawn figure out where it will be drawn. We know the number of pixels that a copy will occupy as it is simply the size times the scale so if the column being drawn is within that range we figure out which pixel in the image is at that location and if that bit is 1 then we are drawing otherwise we are not. Calculating which bit in the drawing data to draw depends on whether we are mirroring the sprite and the scale of the pixels.

fun isPixelDrawn(col:Int):Boolean {
if (col < x) return false
if (delayedDraw) return false

var pixelAtCol = false
for (copyCount in 1..copies) {
val copyStart = x + (copyCount - 1) * size * scale * distanceBetweenCopies
val copyEnd = copyStart + size * scale
if ((col >= copyStart) and (col < copyEnd)) {
val bitPos = (col - copyStart) / scale
val bit = if (mirror) 1 shl bitPos else (1 shl (size-1)) ushr bitPos
if ((bit and drawingBits) > 0) pixelAtCol = true
}
}

return pixelAtCol
}


The HMOVE command is processed simply by adjusting the x variable by the movement delta with a bit of clamping code to make sure it is within the range of the display. My clamping is done by nesting a max function within a min function by having the highest column be the minimum to compare against and 0 and the target column being the max to pick from. As a result the max will clip away negative values making them 0 while the min will clip away off-right-edge values making them the right edge.

fun hmove() {
x = min(159, max(0, x + deltaX))
}


The TIA player sprites have 8 different modes as shown by the chart below. While this could be handled by the TIA register calls, there would be duplication of code so I wrote a simple utility method that will set the sprite scale, copies, and distance based on a player sprite mode. This method is just a simple when statement which sets the parameters appropriately based on the indicated mode. The different play modes that the TIA recognizes is in table 2.



fun setPlayerScaleCopy(scmode:Int) {
when (scmode) {
TIAPIARegs.PMG_NORMAL ->
{scale = 1; copies = 1; distanceBetweenCopies = 1}
TIAPIARegs.PMG_TWO_CLOSE  ->
{scale = 1; copies = 2; distanceBetweenCopies = 2}
TIAPIARegs.PMG_TWO_MEDIUM  ->
{scale = 1; copies = 2; distanceBetweenCopies = 4}
TIAPIARegs.PMG_THREE_CLOSE ->
{scale = 1; copies = 3; distanceBetweenCopies = 2}
TIAPIARegs.PMG_TWO_WIDE  ->
{scale = 1; copies = 2; distanceBetweenCopies = 8}
TIAPIARegs.PMG_DOUBLE_SIZE  ->
{scale = 2; copies = 1; distanceBetweenCopies = 1}
TIAPIARegs.PMG_THREE_MEDIUM  ->
{scale = 1; copies = 3; distanceBetweenCopies = 4}
TIAPIARegs.PMG_QUAD_PLAYER  ->
{scale = 4; copies = 1; distanceBetweenCopies = 1}
}
}

We now have a class that can handle the sprites in the TIA but we still have to handle the TIA registers related to drawing the sprites and perform the drawing of the sprites when the color clock reaches them. This will be what we do next.

No comments:

Post a Comment