A deep dive into the precision timing required to create asymmetrical playfields, how to create scoreboards, and how to use binary coded decimals.
Released:
February 17, 2024
Original Link:
https://youtube.com/watch/lUJmDX1fd18
Your support is greatly appreciated! https://www.patreon.com/8blit
Welcome to 8-Blit, a channel covering all the tips, tricks, and everything in between for programming your own games for the Atari 2600 from 1977.
In this episode we’re racing the beam with precision timing in order to create an asymmetrical playfield, and use that to create a two player scoreboard. We utilize a 6502 processor flag to use binary coded decimals to keep score, and we wrap it all up into the beginnings of a playable game… with full source code provided.
That’s all coming up on this episode of 8Blit.
This channel is supported by viewers like you.
A huge thanks to all our patrons that help keep the intrepid spirit of ATARI alive by donating a few dollars each month. You can help me produce more episodes like this one, where we dive deep into the inner workings of the ATARI 2600 hardware, the 6502 processor, and assembly language programming. Discover all the details on how it ticks, and get access to all the information and resources you need to create ATARI games.
All contributions are greatly appreciated.
Keeping score is all about enhancing a player’s overall enjoyment of a game. It’s one of the ways we can provide a reward mechanism for your players, much like achieving a goal or objective within the game. It introduces another challenge; motivation for you to top your previous record or beat your friends.
In simple games like Pong, it could be the only reward mechanism in the entire game.
Imagine removing the scoreboard from Pong, what are we left with?
You know when you’ve missed a ball… but how many? How many did you miss versus your friend? Who was better? Are you getting better?
Without a scoreboard, players will resolve to keep track of the score themselves, thus determining their own rules for the game. This could result in some difficulties. After all, we’ve all had that one friend who would keep changing the rules on us… so they always win!
In this episode we’re going to focus on creating a scoreboard using the playfield graphics, much like how classic games such as pong, combat, asteroids, space invaders, and breakout.
Sounds easy doesn’t it? You may be surprised at the obstacles we’ll need to clear in order to achieve our goal.
We’ll need to do a deep dive into how the ATARI 2600’s TIA chip works, and use some really precise timing in order to create an asymmetrical playfield for our scoreboards… but it will be worth it, because this technique will be super useful for creating interesting and elaborate non-symmetrical playfields in the future. This will also help you understand how to build multi digit displays using the player graphics objects… which we’ll cover in future episodes.
So stick around because there’s a ton of information packed into THIS episode.
In order to build our scoreboard, we first need to get really intimate with how the playfield works, and how the ATARI 2600’s Television Interface Adaptor draws the screen. We’ve touched on this in a previous episode, so I’ll include a link in this episode’s description.
Let’s zoom into a game and break down what’s going on.
We’ll highlight the playfield pixels that make our scoreboard and background. For now, we’ll exaggerate the spaces between the pixels to make it a little clearer.
You can see that the Five and Zero match up with the playfield pixels. Each using 3 columns of pixels for the digit, and 5 segments in height. ‘Segment’ isn’t an actual measure of anything on the ATARI 2600, I’m merely using this term to represent an abstract number of scan lines used for one line of the digit. It could be 1 scanline in height, or more. It depends on how tall you want your digits. The width of each playfield pixel however, can not be changed.
Let’s break down the playfield even more.
The entire playfield display on the ATARI 2600 is 40 pixels wide.
There’s a split down the middle giving you a left side, and a right side. With each side being 20 pixels. The registers that hold the graphics for the playfield are also only 2 ½ bytes, which is 20 bits. This means the TIA is only capable of storing enough graphics for a single side. The TIA uses these 20 bits again to draw the exact same thing on the opposite side of the screen.
After drawing the left side of the screen, it can draw the right side using one of two different modes.
As an exact copy of the left side… or reflected.
For our purposes in this video, we’re going to stick to the copy mode.
Both sides of the playfield together make up the Visible Screen.
Each side of the playfield is split into 3 different sections. These are the playfield registers that hold the graphics used to draw the playfield.
PF0 is 4 pixels… PF1 is 8 pixels… and PF2 is also 8 pixels.
In copy mode, the right side of the playfield would look like this… With PF0, PF1 and PF2 displayed in the same order as the left.
In reflect mode, the 3 sections are reversed… and the pixels within are also reversed. Displaying PF2, PF1 and then PF0 as a mirror image of the left side, with all their graphics reversed as well.
Let’s switch back to the first mode, copy.
What we have here is a representation of a single scan line being drawn from one side of the screen to the other. When this happens on a CRT television, it’s actually a beam of electrons being fired at the back side of the screen to illuminate phosphor beads, which form the pixels in your game.
When it reaches the right side of the screen the beam needs to be turned off and repositioned back at the left side of the screen in order to begin drawing the next scanline. The time it takes to do this, we call the Horizontal Blank. Nothing is drawn at this point, but we still need to account for this time in our scoreboard display code, and we’ll need every cycle we can get.
Often, a game will display a scoreboard for two players. 1 on each side of the screen. Up until now, we talked about how the TIA will display the same playfield graphics on both sides, either as a copy or reflection of the left side. This is really handy for so many cases, but it’s not going to work if we want to display two different scores.
If the first player has a score of 5 and the second player has a score of 0. Depending on the display mode of the playfield, it will either display a 5 on the both sides of the screen, or a 5 on the left with a backwards 5 on the right. Both cases don’t meet our needs. We need two different numbers on either side.
For this we’ll need to create an asymmetrical playfield. This is a technique to rewrite to the playfield registers while the scanline is being drawn. If we change the values right after each of the playfield registers are displayed on the left, then when they’re displayed on the right it will show the new value.
Each scan line is 76 machine cycles long… and that’s including the Horizontal Blank. Overall, 76 machine cycles… is not a lot of time to do very much
The trick to creating our scoreboard, or other more elaborate playfields all comes down to timing, and that’s what we’re going to focus on from here on.
So we’ll go over the precision timing necessary for drawing an asymmetrical playfield.
The diagram we’re using here is based on one first created by Andrew Davie and posted on the AtariAge forums. This is a really great and useful diagram, and Andrew’s original version also includes additional information, such as the tia and cpu timing.
Let’s break down exactly when each of the three registers is displayed on the screen, and when they can be safely written.
The 4-pixel PF0 is first displayed immediately after the horizontal break, when the beam makes its way back to the start of the next scanline on the left side of the screen.
In terms of how many machine cycles have elapsed since horizontal blank started, PF0 is displayed during cycle 22, though cycle 28.
This is the time when the electron beam is actually drawing the contents of PF0. In order for the beam to have something to draw, PF0 needs to be populated first. Let’s have a closer look at how that works by examining the timing of a typical load/store routine.
A load instruction with a memory offset value, like this one, uses 4 machine cycles to access the memory location, pull the graphic data, and in this example, store it into the Accumulator. Once we have our graphic, we need to store it into the playfield register which uses 3 machine cycles.
The playfield registers are also buffered, in a sense, which takes an additional 2 TIA clocks. Which is ⅔rd’s of a machine cycle.
However, in some Atari 2600 clone consoles, the TIA may not be as precise as the original, and could use an additional clock before writing to the playfield register. This means our buffer could be 1 machine cycle.
When we refer to machine cycles, we’re referencing the processor clock, in this case the 6502… or more specifically the 6507. The TIA has its own clock which runs 3 times faster than the processor. So 1 machine cycle on the processor is equal to 3 TIA Clocks…. You can also refer to them Color clocks, or even TIA cycles. I usually refer to the processor as machine cycles, and the TIA as clocks.
With the time it takes for the Store instruction to execute, including the buffer time, this means the total time to safely write to the playfield register is 4 machine cycles. Since we know the PF0 graphic is displayed at cycle 22, we need to ensure the store operation is completed before that time or the register may not display correctly, resulting in missing pixels, or displaying pixels from a previous update.
Likewise, if you update the playfield register prematurely, you can also experience unwanted pixels… or the lack of pixels.
It’s due to this concern that I prefer to think of the timing in terms of when it’s safe to write to the playfield registers, rather than when the playfield registers are displayed.
If we adjust our timing for ‘safe’ numbers based on when to execute your store instruction, we now know that we must write to the PF0 register at cycle 18 at the latest, and can safely re-write it at cycle 25. This means PF0 contains our new graphic data before it’s drawn, and we don’t overwrite that data until it’s no longer displayed on the screen.
Now that we can ensure PF0 is properly written, we need to take care of the other side of the screen. One caveat with an asymmetrical playfield is that we need to update the playfield registers twice a scanline, on every scanline… even if the graphic is the same on the next scanline. We’re using the same memory address for two different parts of the screen, and there’s a good chance that the left side is not exactly the same as the right side… otherwise it wouldn’t be asymmetrical.
After cycle 25 we have a bit of time before PF0 is displayed on the screen for the second time at cycle 49 through 54. This time we’re safe to write to PF0 on cycle 45, and can re-write it for the next scanline at cycle 51.
Now that we can write PF0, we need to slip in some time to write to the 8-pixel PF1 register. On the left side of the screen PF1 is displayed between the 28th, and 38th cycle… and can be safely written up to the 24th cycle, and re-written for the right side on the 35th cycle.
The right side is displayed from cycle 54 through 65, and can be safely written up to cycle 50, and rewritten for it to be displayed next on the right side on cycle 62.
Our last playfield register, PF2 is first displayed on the left side from cycle 38 through 49, and is safely written up to cycle 34, and rewritten at cycle 46. On the right side, it’s displayed for the second time between cycle 65 and 76, and can be written by cycle 61 and rewritten for it to be displayed next on the right side on cycle 73.
After cycle 76, we start the loop all over again with 22 machine cycles of horizontal blank before PF0 is displayed on the screen again.
Now that we know the timing required to create a stable asymmetrical playfield, let’s have a look at how we can implement this in some code. We’ll start with a basic routine that displays a single number, and work our way up to a two player scoreboard. After that, we’ll have a look at a playable demo that makes use of what we’ve learned.
Let’s start by looking at how we’re defining the graphics we’ll use for the digits of our scoreboard.
As we know from previous episodes, our graphics are generally stored in data tables made up of bytes… each line of our graphic is one byte, and each of the 8 bits in the byte will be 1 pixel.
This particular file defines 16 hexadecimal digit graphics, from zero through F.
If we have a close look at a portion of the table, you can see the bits are either zero or one. If the bit is one, that means a pixel will be drawn on the screen. You can see here how the one’s define the shape of two sixes.
We’re doubling up our digits in pairs because each individual digit is only 4 pixels wide (including a blank spacer bit to keep a separation between digits.)
All the graphics are stored in the same manner. As you can see, the graphic above is made up of 2 five digits
At this point you might be thinking our scoreboard is going to be lame if every number is a double digit… and you’d be right. Thankfully, we have some logical operators that can help us mix these up to give us 256 combinations of hexadecimal digits.
By using the bitwise AND instruction, ANDing our graphic with Zero F, we effectively place a mask over the second 5 and remove the first.
We can do the same to the next graphic, but this time using F, Zero to mask the first digit and clearing the second.
Now it’s a simple matter to use the bitwise OR instruction to add the two graphics together to give us a combined graphic for 65. angle two
It takes a bit of juggling to make it work, but that’s why there’s a ton of example code for this episode, which we’ll have a look at now. Later, we’ll discuss how to do decimal arithmetic, instead of being stuck with a hexadecimal scoreboard.
We’re going to build this simple screen. A single digit drawn in the second playfield register, PF1. In this example we’re not going to attempt an asymmetrical playfield until we have our score displaying correctly for the left side. There’s a bit of work we need to do that’s not immediately obvious.
In our code we’re going to be working primarily with 2 variables. Score, which will be the number displayed at the top of the screen, and Digit Index which will be the offset position in our Digits data table for the graphic we want to display.
A third variable called Temp will also be used several times throughout this code and in the other examples. This variable will be used for different purposes to hold a value for a brief moment and it’s not dedicated to one single purpose.
Now we come to our first hurdle, we have our score, which was hard coded as 6, but we need to determine what index position in our graphic data table the 6 digit graphic starts. We know that each graphic is 5 bytes in height, so we just need to multiply our score of 6 by the height. Which is easier said than done because the 6502 instruction set doesn’t include a multiplication or division instruction. The closest we can get is the shift left, and shift right instruction which will either multiply by 2 or divide by 2, by shifting all the bit over one position.
First we’ll load our score into the accumulator. Since we’re only going to be displaying a single digit in this example, we need to ensure that our score does not go above the maximum value we can display. In this case, our digits are hexadecimal, so F is the largest number we can display. All we need to do is mask the first position in the hexadecimal number with a bitwise AND instruction and we’ll be left with whatever digit was in the first position.
Now we need to do our multiplication by 5 for our graphic index position. We do this by first copying our score into the Temp variable. Then we use the shift left instruction to multiply our number by 2, and then again for a multiplication by 4, and finally we add the score we saved into the Temp variable back into the accumulator, which brings this to a multiplication by 5, and a final index position of 30, which we’ll store into our Digit Index variable.
Now we can display our digit’s graphic. First we load the Digit Index variable into the X register, and then load the first line of the graphic from the data table.
As you will remember, the digit graphics hold two duplicate digits in each. We’ll use our bitwise AND instruction to mask the first nibble so we only have one digit.
After a WSYNC to ensure we start off at the beginning of a scanline, we’ll store our score into the PF1 register. Since we’re not attempting to have two different scores on either side of the screen, we don’t have an asymmetrical playfield and there’s no need to change PF1. So we can initiate a couple more WSYNC’s before moving on to the next line of the graphic. This will continue until we draw all 5 lines.
As you can see, the score on each side of the screen is the same because we didn’t clear or change the PF1 register. Each WSYNC used in the routine resulted in the scoreboard using 3 scanlines for each line of the digit, for a total height of 15 scanlines.
Let’s add a second digit to the left side of the screen, and keep the right side blank.
This time we’re going to use a value of Three Cee for the score. Obviously hexadecimal isn’t the best use for a scoreboard… but it is great for debugging your code. We will switch to decimal later, so bear with me on this.
Just like in the first example, we’re using the same variables with one exception. Our Digit Index variable is now 2 bytes. We’ll need that second byte because we’re going to have two different digit indexes. One for the first nibble of the score, and one for the second.
When finding the index position of the first digit we want to display, we’re basically doing the same thing… mask off the nibble, and then multiply by five. It’s done.
However, for the second nibble we have to shift everything over into the first nibble. We can do this by dividing our value by 16 with 4 bitwise shift left instructions, by taking the value after the first 2 left shifts, and adding it to the result of the final two left shifts; it acts the same as multiplying the result by 5. This gives us the second of our Digit Indexes.
Now it’s time to create the graphics for our scoreboard. Again, the first part is basically the same as our first example. We load the graphic for the first nibble and mask it off and store it into the Temp variable. Then we do the same for the second nibble. The only new part here is that we’ve going to use a bitwise OR instruction to OR the graphic we stored in Temp with the graphic that’s in the accumulator. This will combine the two graphics together giving us our double digit scoreboard.
Our kernel loop is a little different here. We’re actually not displaying anything in PF1 on the first loop. The time it takes to get the graphics and combine them brings us to a point where PF1 can no longer be written on the left side of the screen, so we use the first part of the loop to handle the right side of the screen by clearing out the PF1 register by setting it to Zero.
We then initiate a WSYNC to bring us back to the left side of the screen where we’ll determine if we need to move on to the next line of the graphic or to use the currently loaded line. If you remember, we’re using 3 scanlines for each line of graphic.
Now all we have to do is write it to the PF1 playfield register.
On the last scanline of our scoreboard, we need to clean up the right side of the screen again by setting the PF1 register to zero. Since this would have been handled in the first part of our kernel, that code is not going to get executed again because we’ve terminated the loop. At this point it’s only been a few machine cycles since we updated PF1 with the final scanline’s graphic, so we’re going to have to wait a bit before we can clear it out again. For this we’re using the sleep macro to burn 20 cycles. Then we just set PF1 to 0.
Let’s move on to a real asymmetrical playfield with a 2 player scoreboard. The way this works is no different than the last two examples. While the first one demonstrated a single digit, the second expanded on that to show two digits. This one further expands on the concept to show two digits, twice. This time it’s just a matter of cramming all this into a single scanline.
Prior to the kernel where we’re going to draw the scoreboards, we’ve already calculated the indexes to the graphic data for the 4 digits we’re going to display.
This kernel is a Two Line kernel, which means that for every loop in the kernel, we will output two scanlines. The scanlines are shown in the source code as dashed lines so we can easily keep track of them. One being split between the bottom and top of the kernel loop, and the other executing in the middle.
In this context, I think it’s helpful to think of the middle scanline as the first scanline, and the top of the loop is the end of the second scanline which is essentially front loading the graphics to be displayed on the next scanline.
We can see here where we’re loading up the digits graphics for the left score and then combining them together. Exactly how we did in the previous example.
On the first scanline we’re simply writing the graphics to the PF1 register to display on the left side of the screen. After that, we’re going to save this graphic to draw again on the second scanline. Then we immediately load the digit graphics for the right side, but we can’t update PF1 yet because it’s still being drawn on the left side of the screen. We throw in a few no-operation instructions to kill a little time until it’s safe to update PF1. We save this graphic for the next scanline by storing it into another temporary variable just like we did on the right side of the screen.
On scanline 2 we’re going to increase our digit graphic indexes in preparation for loading the next line of graphic a bit further on, but we have these instructions mixed up a little just so we can conveniently fit them into the tight timings of the playfield.
For the remainder of the loop we’re pulling the graphics we saved from the first scanline on the left side and writing them to PF1. Again, before we update PF1 for the right side we need to wait until it’s drawn on the left, so we burn some cycles before updating PF1.
We then continue the loop at the top and start loading the new graphics for the next scanline. This cycle continues until the scoreboard is drawn completely to the screen.
Up until now we’ve been using hexadecimal digits for our scoreboard. This is pretty handy when we’re debugging and we want some visual feedback on the screen, but our players aren’t going to appreciate us keeping score in base 16 hexadecimals. So let’s switch these to base-10 decimals.
For our score, we’ve been splitting the 8-bit byte containing our score into two 4-bit nibbles. One nibble per digit, then using the value in each nibble to calculate the index to the graphic to use for that digit. This works well and we want to continue doing it this way.
The problem with this approach is that the maximum value a single nibble can represent is 15, which is great to display digits 0 through F in hexadecimal, but with decimal we need the maximum value of each nibble to be limited to 9, for displaying digits 0 through 9. Lucky for us, the 6502 processor can help us out with this.
We can use binary coded decimals by setting the Decimal flag. This changes the way that arithmetic is performed on the 6502.
In decimal mode, the Binary Coded Decimal is the same until we go past 9. At that time the first nibble rolls over to zero, and the second increases to 1, while in binary mode, the first nibble continues to increase to 10 and only rolls over past 15.
You can set decimal mode by using the SED instruction, and turn it off using the CLD instruction… and the mode works along with the Add With Carry, and Subtract With Carry instructions. Unfortunately, it does not work with any of the Increase or Decrease instructions.
Let’s have a look at how this works in practice and fast forward to the area of our example code where we increase the score.
First we set the decimal flag by using the SED instruction… Next we need to clear the carry flag. This is necessary because we’re going to use the Add With Carry instruction to increase the score, due to the ‘increase’ instructions not working how we want in decimal mode. We’ll do the same with the second score, and then use the Clear Decimal instruction to exit Decimal mode… and that all comes together to look like this…
This episode already has a decent amount of example code available to you, but let’s not stop there. I’ve included a rough draft of a new game I call Purr-balls.
Many of the previous episodes have been leading up to this moment, as the game is made entirely with the example code provided with each episode.
Future episodes will introduce new concepts which will be incorporated into the game, like paddle controls and much more. We may also have mini episodes where we debug, or add special features to the gameplay. Over time we’re going to refine the game into a solid release.
The full source code for Purr-balls is available along with this episode’s example code on our github page.
Score keeping is a great way to add excitement to your games. It creates tension and introduces additional challenging aspects to your game, like breaking your personal record or beating your friends score. Each with their own reward mechanism that brings the players back to the game over and over again. With a few tricks up your sleeve, you can implement a scoreboard for your game that uses the playfield graphics which can require machine-cycle level precision in order to produce a stable board. You can also use the board while debugging and choose to display certain values in a hexadecimal or decimal format. As a demonstration of a live scoreboard, we’ve introduced our project Purr-balls which we’ll use again in future episodes where we’ll keep refining the gameplay with each new concept we introduce. More episodes will be coming soon so remember to subscribe if you haven’t already done so.
Remember that all the code for this episode is available from our Github. We’ll provide a link in the episode description.
That’s all for now, thanks for watching, and I’ll catch you later!
Back to top of page