Crazy Pong

We learned basics about VERA graphics settings as well and went pretty deep into Sprites in Sprites I - III tutorials. It is time to write the first game and set up the game framework, learn how to put it all together and wrap it in a game loop.



Let’s take a classic game of Pong and dress it up a bit with Sprites and colors and a bit more advanced physics and of course we will write it completely in 65C02 Assembly.

Project Specs

  • Running in tile (text) mode 40x30
  • No custom tiles
  • Default palette
  • Ball and paddles implemented using sprites
  • Use Interrupts for
    • Game timing - VSYNC
    • Collision control - SPRCOL
  • Game starts at 0:0 and player reaching 9 “goals” wins
  • Keyboard controls: Ctrl (up) and Alt (down) for left paddle Arrows Up and Down for right paddle
  • No sound
  • No AI

Game Structure

Since this is our first complete Assembly game we will look at some of the core components more closely. But in principle the structure of the game is pretty standard and should serve as a possible template for future more complex games. Of course the same result can be achieved in many ways, this is just one of them.

Main Program

Initializing
Game loop
        Read the player input
        Check for score changes
        Check for Game Over

Interrupt handler

VSYNC - triggered once every frame, 60 times per second
        Move entities (ball, paddles)
SPRCOL - triggered every time there is a collision between two sprites (paddle and ball)
        React to collisions by bouncing ball from paddle


Initializing

Every program and especially game has several loops that we can unpeel like onions. However there is part of the initialization that only happens once at the beginning of the program. Here most often we load resources that will be used throughout the game, make necessary system configurations and take control of interrupts. Very often we also disable system functions and interrupts that could potentially interfere with our game and throw timings out of whack. Our first game is so simple we don’t need to go that far.

So the main actions we perform are:

Load Assets

We will use three sprites, one for the ball and two for paddles. Both paddles have the same appearance so we only need to load two sprite graphics to Video memory.


All sprites are in 256 colors and use the default palette. That of course is overkill but in this simple example we have so much memory available we don't need to worry about it and can just use whatever we need from 256 colors in default palette.

Configure Sprites

Each Sprite has 8 registers that need to be set in order to put it in correct configuration, to point to correct graphics in VRAM and to set its position.


Other two sprites are configured in a similar way. Note that I skip Sprite 0 because it is pre configured as a mouse pointer and since we have enough Sprites available I simply skip it to avoid any unforeseen interferance.

Insert custom IRQ handler

Interrupt handlers are extremely important in game programming, especially in Assembly. So I deliberately include them even in this simple game. We could easily get away without them and simply do everything in the main loop and just make a simple wait loop so the game would run at constant speed but I believe the sooner we get used to using them the better we will be off later when we tackle more complex games.

As mentioned above we will use two types of IRQ triggers. Besides the actual game functionality that we will look at later we have to take care of two things:

Saving VERA registers

One of the issues of using interrupts is that unless we temporarily disable them we neve know when they might get triggered and therefore because we are accessing VERA from main program too we have to make sure that we save the CPU registers and state of VERA registers when entering IRQ handler:

and restore them before exiting like this:

It is best practice to use stack for that since the IRQ handler should be reentrant, meaning that in theory it could be called again while already processing a previously triggered interrupt and thus overwriting same locations if we would use memory to store those values.

Variables

Let’s take a look at the variables that we will be using. Of course variables in Assembly are nothing else than just memory locations but Assembler keeps track of them for us so we don’t need to work with actual memory addresses and our program is easier to read.

Ball related variables

BallX Horizontal position of ball in pixels*16
BallY Vertical position of ball in pixels*16
DirectionX Ball direction: -1 moving left +1 moving right
DirectionY Ball direction: -1 moving up, +1 moving down
SpeedX Horizontal change per frame
SpeedY Vertical change per frame
Angle Ball can move at five different angles so allowed values are 0-4:
0 - 0°
1 - 15
2 - 30°
3 - 45°
4 - 60°

Paddle related variables

LPaddleX Horizontal position of left paddle
LPaddleY Vertical position of left paddle
LMoving Is left paddle moving up (-1) or down (+1) or stationary (0)
LScore Left Player score
RPaddleX Horizontal position of right paddle
RPaddleY Vertical position of right paddle
RMoving Is right paddle moving up (-1) or down (+1) or stationary (0)
RScore Right Player score

Other Supporting Variables

Joy Value of Joystick presses
Score Indicator if left or right player scored
AngleX Lookup table for horizontal speed of ball based on the angle
AngleY Lookup table for vertical speed of ball based on the angle
Temp1 Temporary working variable
Temp2 Temporary working variable


Game Loops

Since this game is fairly simple we still want to include some of the main components of “real” commercial games like title screen multiple lives (balls) and of course the main game loop so in our case we have three loops nested inside each other. Let’s look at the code right away:

The inner loop is the main game loop and as we see it doesn’t do much. It only read the controls and saves them to the Joy variable. It then checks if there was a change in score and reacts if it was. If there was none it goes back to the beginning of the loop.

One layer higher is a new ball loop which we can see in the context of new life, so scoring doesn’t change but the game starts from the initial position with new ball (life).

The outer loop is a new game loop. This happens any time one of the players reaches a winning score of 9. So really all of the game mechanics is happening in our IRQ handler. But before we look at it let’s cover one more important part, which is the math that we use.


Math

Yes we have to talk about math. Video game programming does require a certain amount of math to be applied but it is fairly straightforward and once understood becomes very easy. In a game of bouncing ball we will obviously have to calculate some angles but before we start we have to make few decisions.

Data format

First and most important one is to decide how are we going to store data. When dealing with angles we know we will have to use sine and cosine math and therefore we will need to use fractions.

Since we know that integer arithmetic on 65C02 is pretty straightforward even when dealing with multibyte values and that floating point arithmetic is notoriously slow we can opt for a compromise and use Fixed Point Notation or Fixed Point Arithmetic.

Fixed point notation is internally just using integer values but it is scaled by some practical value. For assembly programming it is most convenient if we scale values by a power of 2. I experimented a bit and at the end decided to use two bytes for each X and Y coordinate and that 4 bits is precise enough for fractions. That means that out of 16 bits 12 bits will be used to store whole values (0 - 4095 unsigned) and 4 bits for fractions (0 - 15). If we need more precision we can of course move this around and even use three bytes or more.



The above notation is also convenient because it is easily readable in hexadecimal. If we want to increment by one we simply add 16 or $10. In our case we want to move by two pixels per frame so if the ball is moving in straight line then we need to add 31 or $20 and so forth. Of course all numbers smaller than that will mean a fraction of the 2 pixel move.


Angle transformations

Now that we know how the data is stored we can calculate angles. We have made following decisions:

  • Ball will move at 2 pixels per frame or 120 pixels per second. This is fairly slow but you can adjust these calculations and make it move faster for more challenging game
  • The ball can move at five different angles and 15 degrees increments

Because we use fixed point notation at bit 4 we know that increments of 32 would make the ball move by 2 pixels. We can use simple trigonometry to calculate X and Y components for each possible angle.

dX = cos(angle)*32

dY = sin(angle)*32

So we get a simple table

Angle X Speed (dX) Y Speed (dY)
32 0
15° 28 8
30° 23 16
45° 16 23
60° 8 28

And those are exactly the values we have in lookup tables AngleX and AngleY.

Game mechanics (IRQ Handler)

It is finally time to tackle the core of our program. As mentioned before this part could be easily included in the main game loop but we want to learn some techniques that will help us later with more complex games.

After saving the CPU and VERA registers our first task is to determine why the IRQ handler was called. Because we configured the Interrupt Enable (IEN) register of VERA in a way to allow VSYNC and SPRCOL we have to detect which of those two possible events triggered it. We do that by reading register ISR ($9F27).

Checking if we are in SPRCOL then bit 2 has to be set:

CheckSPRCOL:
    lda ISR
    and #SPRCOL
    bne :+
    jmp CheckVSYNC

And for VSYNC bit 0 has to be set:

CheckVSYNC:
    lda ISR
    and #VSYNC
    bne ProcessVSYNC
    jmp irq_exit

Now that we separated two different ways that the IRQ handler was called let’s dive into what happens in each.

Collision Event (SPRCOL)

In our game collisions can only happen when the ball hits one of the paddles so we have to determine which one it was. However either way we have to flip the X direction of the ball flight meaning that if the value was +1 it has to change to -1 and vice versa. We can do that with following four instructions:

:   lda DirectionX            ; Flip X Direction
    eor #$FF
    inc
    sta DirectionX

Next we check if the ball hit left or right paddle. Because we configured both paddle Sprites with different collision masks we can use the value returned ISR Register to determine the correct one using just few instructions:

    lda ISR                    ; Read ISR
    and #$F0
    cmp #$20
    bne RCollision
    jmp LCollision

The rest of the code is just calculating the Y (vertical) component of the bounce. Let’s look at the logic in pseudocode at what happens after bounce first. Remember that both ball and paddles can have three directions -1 moving up, +1 moving down and 0 not moving in Y direction.

  • Move ball out of collision
  • If paddle is not moving
    • then Yspeed does not change and we can exit the routine
  • If paddle is moving and the ball is coming at it straight
    • then Ball must move in same direction at 15 degrees

  • If paddle and ball are moving in the same direction
    • then Increase angle by 15 degrees until it reaches 60 degrees
  • If paddle and ball are moving in opposite direction
    • then Decrease angle by 15 degrees until it reaches 0 degrees

Let’s check the above in code:

Note that we are cheating here in two ways.

  • We are not taking in consideration how much overlap we had at collision time. We simply push the ball out to the edge of the paddle.
  • We are not checking if the ball hit the top or bottom edge of the paddle and would therefore bounce at a different angle. We simply treat every collision as hit on the face of the paddle.

The Left paddle is almost identical with just flipped some values.

Game tick (VSYNC)

Tick is a term very often used in relation to regular game updates and in our case we use VSYNC which is an interrupt that is triggered 60 times per second at the end of refreshing the screen.

This is the place where we update all of our screen objects and also take care of ball bounces from top and bottom edge. Like with collisions let's take a look at pseudocode first to make the actual source code more understandable:


  • Update Paddles
  • Is Ball moving left
    • Decrease X position
    • Check if reached left edge and set Score change indicator
  • Else
    • Increase X position
    • Check if reached right edge and set Score change Indicator
  • Is Ball moving Down
    • Increase Y position
    • Check if bottom edge reached
      • Move ball out of edge
      • Change vertical direction
  • Else
    • Decrease Y position
    • Check if top edge reached
      • Move ball out of edge
      • Change vertical direction
  • Update sprite positions in VERA

The source code for above is here:

Note that here we use a subtraction method to determine if the ball hit the edge. We then use the result to move the ball to the correct number of pixels back into the field for a more realistic bounce.

Source Code

We have written quite a lot of code for this simple program so to make it more easy to find lets organize it into two files. As a quick reference each file contains following routines:

CrazyPong.asm - main program

Main
    NewGame loop
    NewBall loop
    MainLoop
IRQHandler
    SPRCOL Handler
    VSYNC Handler
UpdatePaddles

Graphics.asm - file with all the graphics data and routines

ConfigureSprites
InitScreen
DisplayPlayfield
CLS
DisplayDigit
DisplayGameOver
DisplayTitle
Load Assets

I hope you find this tutorial useful. Full source code is available on GitHub here.

If you don’t have a cc65 setup yet, binary is available here.


Comments

Popular posts from this blog

Commander X16 Premium Keyboard