Peng
The goal is to write pure assembly version of Pong clone and learn basics of writing a fully playable game in assembly for Commodore 64. It is not 6502/6510 tutorial, there are plenty of those out there.
In this first tutorial we will use simple character graphics, in fact we will not even use any special characters or design custom fonts, just basic black and white blocks. We will learn about game loop, how to draw to the screen, how to read keyboard, how to generate sound and other useful techniques like lookup tables, etc. All these techniques will help us later with development of more complex games.
To make program more readable we will define few constants. They should be fairly self explanatory. The default memory location to text screen and color attributes and VIC II Raster counter - a memory location where current location of display "beam" is located. Then we have two CIA locations where status of Joysticks can be read from. Followed by few custom values to determine the behavior of the game like X location for both rackets and ball speed in frames and duration of three sound effects we will generate.
Next we will need several variables to store position of rackets, scores, etc: First group is all about the ball. Its location, speed and background character containing PETSCII code of character that ball is at that point covering. Very important variable is also FrameCount because it contains the counter that determines the speed of ball. Second group of variables are to store players Y positions and scores. The last group contains the GameOver flag and sound duration counters and at the end the general purpose Store to keep values temporarily when the registers are not enough.
Initialization
To initialize screen we have to clear its previous content, set new color attributes and draw the playing field.
Lookup tables is one of the most important techniques in 8 bit programming. We will find many uses for them in the games we will be developing. In short lookup tables are used in situations where we can calculate results for some time critical operations in advance and during run time use simple index to retrieve result instead of doing calculations in real time. For example, if need to multiply numbers 1-10 with 5 we could simply store those values in memory like this:
That would store these 11 bytes in consecutive bytes in memory.
To multiply we now simply put value to be multiplied to index register X or Y retrieve result in Accumulator:
And that is it. In total in most cases this uses 6 cycles (2 cycles for ldx and 4 cycles for lda). There might be some exceptions when crossing bank boundaries etc.
Screen size for text mode is exactly 1000 (40 columns x 25 rows) bytes which means we have to be able to address 1000 bytes of memory as quickly as possible and we can’t use a single index register for indirect addressing.
Screen memory is organized sequentially row after row and the most logical approach to drawing to it would be to use formula:
Screen Memory Start + (Row * 40) + Column
Problem with this approach is that multiplication, especially beyond just 8 bit values is extremely slow and if we want to draw to screen many times per second this will simply not work.
One of the ways to solve this problem is of course using lookup tables and constructing a pointer to screen memory in Zero Page and use indirect addressing to access any location on screen. Since we are dealing with 16 bit addresses we have to construct two lookup tables, one for Low byte and the other for High byte.
First row of text by default starts at $0400, second row starts 40 bytes later at $0428, third row starts at $0450 and so on.
Because columns in character mode can only have values between 0 and 39 we can easily store them in 8 bit Index register so we only need to take care constructing the address to the beginning of each row or Y coordinate on the screen.
Two lookup tables would therefore look like this:
ScreenLo: $00, // Low byte of Row 0
$28, // Low byte of Row 1
$50, // Low byte of Row 2
$78, // Low byte of Row 3
$A0, // Low byte of Row 4
$C8, // Low byte of Row 5
$F0, // Low byte of Row 6
$18, // Low byte of Row 7
...,
$C0 // Low byte of Row 24
ScreenHi: $40, // High byte of Row 0
$40, // High byte of Row 1
$40, // High byte of Row 2
$40, // High byte of Row 3
$40, // High byte of Row 4
$40, // High byte of Row 5
$40, // High byte of Row 6
$41, // High byte of Row 7
...,
$43 // High byte of Row 24
Of course instead of typing in 50 numbers we can use macro to construct it. In Kick Assembler something like this can be used to construct both tables:
With this in place, it is very simple (and fast) to calculate any position on the screen. If we put row number into X register we construct the address of the beginning of that row in Zero page location for example $FB and $FC, then simply use Indirect Indexed addressing to add Column in Index Y to the address.
Reading Joysticks on Commodore 64 is very easy. We simply read two memory locations that are linked to CIA and check individual bits for directions and fire buttons. In this game we only need to read positions Up and Down for moving paddles up and down the playing field and we will use fire buttons to start the game.
- bounce off the racket
- bounce off the top and bottom wall
- goal scored
To make our life even simpler we will each one of three sound channels for each of these effects. Effects themselves will be almost identical. They will only differ in pitch and duration.
We will define all three sound effects during the initialization of the game and then just start them when needed.
For better understanding we can use following illustration:
We can define the duration of Attack, Decay and Release. However we can't define the duration of Sustain, we can only define the volume of Sustain. The duration has to be controlled programmaticaly. It is also important to note that we can't choose any time in milliseconds because we only have 4 bits for each and 16 values are not enough in many cases. Therefore each value has predefined duration. For example Attack value 0 is 2 milliseconds, Attach value 4 is 38 milliseconds. All these values can be found in Programmer's Reference Guide.
As you can see we are using some labels and variables for timings. Since the whole game is run in fixed frame rate based on the display refresh rate (60 FPS for NTSC and 50 FPS for PAL) we define duration of sounds in FPS. Both bounces are short 4 PFS and the goal scored is double at 8 FPS.
On Commodore 64 we can read the location of the beam in VIC II register $D012 also known as Raster Counter. We can use it to update the screen on regular intervals and even run the game loop at the speed of screen updates.
However please note, that PAL and NTSC systems use different refresh rate and therefore the game will run at slightly different speed. If we use this technique PAL games will run at 50 frames per second and NTSC will run at 60 frames per second.
For more complex games and to solve this problem more advanced techniques are used to make sure the game runs at predetermined speed regardless of the screen updates. We will look at these techniques during later projects.
Our game is so simple we can take care of this problem with very simple technique using before mentioned Raster Counter. The solution is to simply wait for the display beam to finish drawing content to display and during blank time update the screen (i.e. redraw objects that moved since previous frame. Of course modern displays do not use beams to draw but they still render based on the output coming from the video circuitry that was designed with CRTs in mind.
• change the size of the racket to 5 characters high,
• add new angles of ball flight,
• increase the speed of the ball flight as the game progresses or add option in the beginning to pick the skill level,
• improve physics/behavior of the ball bouncing off the racket,
• control racket with joystick,
• add colors to the game
• replace ball with sprite and move with pixel precision,
• etc.
In this first tutorial we will use simple character graphics, in fact we will not even use any special characters or design custom fonts, just basic black and white blocks. We will learn about game loop, how to draw to the screen, how to read keyboard, how to generate sound and other useful techniques like lookup tables, etc. All these techniques will help us later with development of more complex games.
Project specs:
- Character mode
- No kernal calls
- No sprites
- Black and white
- Simple ball movement in only three angles
- Racket is 3 characters high, with following physics:
- Bounce of ball from top character is 45 degrees up
- Bounce from middle character if 180 degrees back
- Bounce from bottom character is 45 degrees down
- Different sound effects for bounce off the wall, off the racket and for scored point
Program Structure
We will approach the program in four logical sections. We will start with the most extensive component, which is display/graphics, second will be reading controls and moving things around, third will be sound and at last we will bind everything together by writing a game loop.To make program more readable we will define few constants. They should be fairly self explanatory. The default memory location to text screen and color attributes and VIC II Raster counter - a memory location where current location of display "beam" is located. Then we have two CIA locations where status of Joysticks can be read from. Followed by few custom values to determine the behavior of the game like X location for both rackets and ball speed in frames and duration of three sound effects we will generate.
Next we will need several variables to store position of rackets, scores, etc: First group is all about the ball. Its location, speed and background character containing PETSCII code of character that ball is at that point covering. Very important variable is also FrameCount because it contains the counter that determines the speed of ball. Second group of variables are to store players Y positions and scores. The last group contains the GameOver flag and sound duration counters and at the end the general purpose Store to keep values temporarily when the registers are not enough.
Display/Graphics
Since this game will be using very basic character mode, there is not much in terms of graphics required here. For learning purposes many of the same rules apply as for high resolution or sprite based games so we will learn fundamentals that we will be able to use later when writing more complex games.Initialization
To initialize screen we have to clear its previous content, set new color attributes and draw the playing field.
Clear screen
Commodore 64 in text mode has 25 rows of 40 characters so 1000 characters in total to draw. By default the location where the screen data is stored is in memory starting at $0400. There are many subtly different ways to clear the screen but in principle they narrow down to writing space character 1000 to the screen memory. The routine below does it in single loop by writing 4 times per loop that goes around 250 times (Index X is used as counter from 250 to 1 and exits when it reaches 0).SetColor
Setting color is very similar to deleting the screen in text mode. The function we write is almost identical to the CLS with two differences. Of course the starting memory address is different but we should also be able to define the color attribute outside the function. We will use Accumulator to pass the value to the function.
Lookup Tables
Lookup tables is one of the most important techniques in 8 bit programming. We will find many uses for them in the games we will be developing. In short lookup tables are used in situations where we can calculate results for some time critical operations in advance and during run time use simple index to retrieve result instead of doing calculations in real time. For example, if need to multiply numbers 1-10 with 5 we could simply store those values in memory like this:That would store these 11 bytes in consecutive bytes in memory.
To multiply we now simply put value to be multiplied to index register X or Y retrieve result in Accumulator:
And that is it. In total in most cases this uses 6 cycles (2 cycles for ldx and 4 cycles for lda). There might be some exceptions when crossing bank boundaries etc.
Writing to screen using X,Y coordinates
Screen size for text mode is exactly 1000 (40 columns x 25 rows) bytes which means we have to be able to address 1000 bytes of memory as quickly as possible and we can’t use a single index register for indirect addressing.
Screen memory is organized sequentially row after row and the most logical approach to drawing to it would be to use formula:
Screen Memory Start + (Row * 40) + Column
Problem with this approach is that multiplication, especially beyond just 8 bit values is extremely slow and if we want to draw to screen many times per second this will simply not work.
One of the ways to solve this problem is of course using lookup tables and constructing a pointer to screen memory in Zero Page and use indirect addressing to access any location on screen. Since we are dealing with 16 bit addresses we have to construct two lookup tables, one for Low byte and the other for High byte.
First row of text by default starts at $0400, second row starts 40 bytes later at $0428, third row starts at $0450 and so on.
Because columns in character mode can only have values between 0 and 39 we can easily store them in 8 bit Index register so we only need to take care constructing the address to the beginning of each row or Y coordinate on the screen.
Two lookup tables would therefore look like this:
ScreenLo: $00, // Low byte of Row 0
$28, // Low byte of Row 1
$50, // Low byte of Row 2
$78, // Low byte of Row 3
$A0, // Low byte of Row 4
$C8, // Low byte of Row 5
$F0, // Low byte of Row 6
$18, // Low byte of Row 7
...,
$C0 // Low byte of Row 24
ScreenHi: $40, // High byte of Row 0
$40, // High byte of Row 1
$40, // High byte of Row 2
$40, // High byte of Row 3
$40, // High byte of Row 4
$40, // High byte of Row 5
$40, // High byte of Row 6
$41, // High byte of Row 7
...,
$43 // High byte of Row 24
Of course instead of typing in 50 numbers we can use macro to construct it. In Kick Assembler something like this can be used to construct both tables:
With this in place, it is very simple (and fast) to calculate any position on the screen. If we put row number into X register we construct the address of the beginning of that row in Zero page location for example $FB and $FC, then simply use Indirect Indexed addressing to add Column in Index Y to the address.
PutChar, GetChar
With using this technique we build PutChar, GetChar functions below. We simply use row position in index X to build 16 bit address to the beginning of that row in Zero Page location $fb and $fc and use column in index Y to draw content of Accumulator to the exact screen position.DisplayMsg
We use very similar code to write messages to the screen. In this case it is very rudimentary function with fixed location in the middle of the screen. We will display two different messages that will be formatted in C style - meaning they end with 0. All we need to do is to pass the starting location of message to be displayed. The memory location when messages start is hard coded in the function. This is not very efficient and we can develop much more flexible version in the future.RacketOn / RacketOff
With function PutChar we can write very simple and easy readable functions for displaying and removing rackets:InitGraphics
Before we can start moving object around the screen we need to take care of initializing it. We already have all the necessary functions ready so we can put all together and initialize the screen. The steps are in grouped in code in following order:
- clear screen and initialize colors
- draw horizontal line on the top
- draw horizontal line on the bottom
- draw dotted vertical line in the middle
- draw initial score
- draw paddles
All the drawing is done with simple loops and calls to functions we wrote before. The only one a bit different is the middle line. We use binary AND function to skip over odd rows and we use two characters (PETSCII 117 and 118) to draw it.
- draw horizontal line on the top
- draw horizontal line on the bottom
- draw dotted vertical line in the middle
- draw initial score
- draw paddles
All the drawing is done with simple loops and calls to functions we wrote before. The only one a bit different is the middle line. We use binary AND function to skip over odd rows and we use two characters (PETSCII 117 and 118) to draw it.
Display Score
To display scores we will use PutChar function but first we have to design the looks of numbers and store them in memory in such a way so it is easy to draw them.One of the smallest clearly readable numbers can be stored in 3 x 5 pixel matrix. The design is as follows:
There are many ways to store these definitions. I chose to use three bytes for each digit with first byte representing first row of the digit, second byte representing second row and third one the third. Because we only need five bits we will only use top five bits in each bytes and ignore the lowest three:
DrawDigit
To draw the digit we have to go through three columns and loop through each byte where top five bits represent full block character or empty, blank space character. Because each digit is represented with three bytes we have to multiply the Index by 3 and we use Lookup table for that. Then we just simply call another function to draw vertical line three times. Instead of loop we use a simple version of loop unrolling. We will look at this technique and when to use it in later tutorials.DrawDigitLine
To draw each vertical line of five characters represented by top five bits passed in Accumulator we create a simple loop and extract the bit we are processing by shifting it left into Carry. We then check draw either blank space or full block depending on the stat of Carry:Control
We will control paddles with joysticks. For this type of game it would be much more convenient to use paddles but very few users actually own paddles so we would limit the audience by only supporting them. We might add support for paddles later.Reading Joysticks on Commodore 64 is very easy. We simply read two memory locations that are linked to CIA and check individual bits for directions and fire buttons. In this game we only need to read positions Up and Down for moving paddles up and down the playing field and we will use fire buttons to start the game.
Joystick Input
Since reading Joystick positions is so simple we will not even bother writing functions for them and instead just read them in main loop using following code:PlayerUp / PlayerDown
With Joystick movements read we can move both paddles up and down. We will write separate functions for left and right paddle for readability. That is of course not very efficient but since this game is so simple and short we don't need to worry too much about the space and it is more important for code to be understandable for beginners.
The logic should be very straight forward. First we read current vertical and horizontal location and delete the paddle at the current location.
Next we increment or decrement the vertical position or Y and check if we hit bottom or top of playing field. If we did we revert to previous position and draw the paddle otherwise we store new position and draw. The (very redundant code) is below:
WaitForFireButton
We still need to write a function that will wait for the fire button being pressed on either Joystick. We can achieve this with very few lines of code like so:Sound
Commodore 64 has a very powerful sound capabilities and in many ways it set standards for sound and there is still music being composed used C64 SID chips. In our first game we will create very simple sound effects for:- bounce off the racket
- bounce off the top and bottom wall
- goal scored
To make our life even simpler we will each one of three sound channels for each of these effects. Effects themselves will be almost identical. They will only differ in pitch and duration.
We will define all three sound effects during the initialization of the game and then just start them when needed.
ADSR Envelope
ADSR Envelope is very important concept in sound generation. ADSR is an acronym that stands for Attack, Decay, Sustain, Release. There is huge amount of information available for it in general as well as specific to Commodore 64 and the topic is so broad it could fill several blog posts by itself.For better understanding we can use following illustration:
We can define the duration of Attack, Decay and Release. However we can't define the duration of Sustain, we can only define the volume of Sustain. The duration has to be controlled programmaticaly. It is also important to note that we can't choose any time in milliseconds because we only have 4 bits for each and 16 values are not enough in many cases. Therefore each value has predefined duration. For example Attack value 0 is 2 milliseconds, Attach value 4 is 38 milliseconds. All these values can be found in Programmer's Reference Guide.
InitSound
Our function to initialize all three sound effects first defines frequencies three different frequencies and then sets the same ADSR envelope for all:SoundOn / SoundOff
We can now write some very simple functions to start and stop each sound effect. This is done to writing to SID registers and setting certain bits for voice numbers (channels) and by picking the type of sound wave. I picked sawtooth but you can of course experiment with others.As you can see we are using some labels and variables for timings. Since the whole game is run in fixed frame rate based on the display refresh rate (60 FPS for NTSC and 50 FPS for PAL) we define duration of sounds in FPS. Both bounces are short 4 PFS and the goal scored is double at 8 FPS.
CheckSound
All that needs to be done is a function that checks these counters and stops playing effect if the counter reached 0.Game Loop
This is fundamental concept of every computer game. Programming in higher level programming languages and in complex operating systems we very often focus on events triggered by user or operating system but in case of game programming we want to ensure that game lay runs at predictable and predetermined speed. We control that in game loop. Game loop in its simplest form is just continues loop that is responsible for checking for player input, moving objects on screen, checking collisions and drawing on screen.Timing
Our game is very simple and processor is more than capable of processing many frames per second so we can use very common technique to limit the speed of game play. On 8 bit systems the video output was central to the whole operation and CRTs were standard displays. The light beam in CRT has to be timed very precisely and therefore controlling it was very high priority and therefore very reliable timing mechanism.On Commodore 64 we can read the location of the beam in VIC II register $D012 also known as Raster Counter. We can use it to update the screen on regular intervals and even run the game loop at the speed of screen updates.
However please note, that PAL and NTSC systems use different refresh rate and therefore the game will run at slightly different speed. If we use this technique PAL games will run at 50 frames per second and NTSC will run at 60 frames per second.
For more complex games and to solve this problem more advanced techniques are used to make sure the game runs at predetermined speed regardless of the screen updates. We will look at these techniques during later projects.
Screen Tearing
Screen tearing is very undesirable effect that is result of poor timing of the displaying of game objects. It happens when the movements to objects happen during drawing on the display which is resulting that part of the screen or object is displaying previous position or stage of animation and part the new frame resulting in some object being teared or split.Our game is so simple we can take care of this problem with very simple technique using before mentioned Raster Counter. The solution is to simply wait for the display beam to finish drawing content to display and during blank time update the screen (i.e. redraw objects that moved since previous frame. Of course modern displays do not use beams to draw but they still render based on the output coming from the video circuitry that was designed with CRTs in mind.
Movement and Collision detection
Practice/Improvements
There is plenty of interesting challenges to improve the included source code and learn by doing it. Some ideas• change the size of the racket to 5 characters high,
• add new angles of ball flight,
• increase the speed of the ball flight as the game progresses or add option in the beginning to pick the skill level,
• improve physics/behavior of the ball bouncing off the racket,
• control racket with joystick,
• add colors to the game
• replace ball with sprite and move with pixel precision,
• etc.
Greetings,
ReplyDeleteIs the full source available so we can see the game in action?
Thank you.
I have been focusing on Commander X16 lately so I have to check where I left this project so hopefully I can finish it and port it to X16 in next couple of months. It all depends on how much time the work that actually pays the bills will allow me to spend on these projects :-(
DeleteWill be possible to change backgorund to white and text to black, it's killing my eyes :). nice article.
ReplyDelete