Linux DevCenter    
 Published on Linux DevCenter (http://www.linuxdevcenter.com/)
 See this if you're having trouble printing code examples


Retro Gaming Hacks, Part 3: Add a Ball and Score to Pong

by Josh Glover, contributor to Retro Gaming Hacks
01/05/2006

Now that we have moving paddles for our Pong clone, the only thing standing in the way of having some real fun is the fact that the ball does not in fact move. But this is a problem easily solved once you rediscover some basic geometry.

The first geometric law we will require is: slope equals rise over run. The ball is going to move in a straight line, so all we have to do to track its movement is, for every iteration of the main loop, change the y coordinate by the rise (or numerator) of the slope, and the x coordinate by the run (or denominator). The fact that the slope, a fractional sort of number, can be easily represented by two components is a big win for our integer-loving CPU. You can start by defining two more macros at the top of the sdl-pong.c file, where all of the macros live:

#define BALL_SPEED   6 // total change in x and y in the slope
#define SLOPE_MAX_DY 4 // the maximum change-in-y allowed in the slope

The macros will make a bit more sense once we add another structure definition (right above the GameData structure in the code):

// Structure definitions
typedef struct {

  int dx;
  int dy;

} Slope;

and a few new members to the GameData structure itself:

  int game_speed;   // game speed
  int ball_speed;   // number of pixels the ball can move at once
  int slope_max_dy; // maximum value for change-in-y component of the slope

  SDL_Rect ball;  // ball
  Slope    slope; // slope of the line representing the ball's path

Just as the game uses the GameData structure to hold a collection of related data, so will it use the Slope structure to keep track of all of the data necessary to keep track of the ball's movement: namely the change-in-x (run) and change-in-y (rise) components of the slope of the line along which the ball is currently moving. As always, you will need to initialize the new members of the GameData structure:

  // Initialise game data
  game.running      = 1;
  game.game_speed   = GAME_SPEED;
  game.ball_speed   = BALL_SPEED;
  game.slope_max_dy = SLOPE_MAX_DY;

You may be wondering if I forgot to initialize the Slope. Actually, I didn't; I just cannot do it in such a straightforward fashion because I want to introduce the one ingredient that makes a good game great: the element of chance. Why not randomly generate the starting Slope (within reason, of course)? You need to include two new headers at the top of the file:

// Standard library headers
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

stdlib.h provides the srand() and rand() functions, and time.h provides the eponymous time() function, which you will need in the new genSlope() function. Add the definition of genSlope() between cleanUp() and movePaddle(), like so:

void genSlope( GameData *game );

and add the implementation between that of the same functions:

/* Function: genSlope()
 *
 * Randomly generates the slope of the vector of ball's travel.
 *
 * Parameters:
 *
 *   *game - game data
 */

void genSlope( GameData *game ) {

  // Seed the random number generator with the current Unix timestamp
  srand( time( NULL ) );

  // Generate the change-in-y component of the slope randomly
  game->slope.dy = 
    1 + (int)((float)game->slope_max_dy * rand() / (RAND_MAX + 1.0));

  // The change-in-y component of the slope is
  // whatever is left in the "budget"
  game->slope.dx = game->ball_speed - game->slope.dy;
  
  // Flip a coin for x and y directions
  if ((int)(2.0 * rand() / (RAND_MAX + 1.0)))
    game->slope.dx *= -1;
  if ((int)(2.0 * rand() / (RAND_MAX + 1.0)))
    game->slope.dy *= -1;
  
} // genSlope()
Retro Gaming Hacks

Related Reading

Retro Gaming Hacks
Tips & Tools for Playing the Classics
By Chris Kohler

Before genSlope() can generate any random numbers, it must seed the random number generator. If it does not, the first call to rand() will automatically seed the random number generator with 1, which seems OK, except for the fact that you will get the same sequence of "random" numbers every time you start the game. It does not take much thinking about this to see why that would defeat the entire purpose behind randomly generating the slope in the first place. So seed the random number generator we must, and what better to seed it with than the current timestamp? (To answer that question, a few bytes from /dev/random would be better to seed it with, but we will refrain in the interest of portability, as /dev/random does not exist on all systems.) The time() function helpfully returns the current Unix epoch time (which is the number of seconds that have elapsed since January 1, 1970), so you can just feed it directly into the gaping maw of the srand() function, as shown in this excerpt from genSlope():

  // Seed the random number generator with the current Unix timestamp
  srand( time( NULL ) );

Now that the diabolical hunger of the random number generator is sated, you can use it to generate the change-in-y portion of the slope:

  // Generate the change-in-y component of the slope randomly
  slope->dy = 1 + (game->slope_max_dy * rand() / (RAND_MAX + 1.0));

This is a lot more complicated than it should be, as rand() likes to generate numbers between 0 and the C-library-defined macro RAND_MAX, which is typically a large number like 32,767. What you really want is an integer between 1 and the maximum allowable value for dy, so you must divide the value returned by rand() by RAND_MAX plus 1 to get a floating point number between 0 and 1, then multiply that by the aforementioned slope_max_dy, and finally, add 1 to the whole bloody thing. Luckily, computing the change-in-x part of the slope is much easier:

  // The change-in-x component of the slope is
  // whatever is left in the "budget"
  slope->dx = game->ball_speed - slope->dy;

Things should start to become clear now: we want the ball to move six pixels a turn, and we will generate the number of pixels it moves in the y direction randomly, and use the rest for x-centric movement (pun intended, sorry about that). And finally, to make sure the ball does not always move down and to the right from the start, have genSlope() randomly flip the sign on one or both components of the slope:

  // Flip a coin for x and y directions
  if ((int)(2.0 * rand() / (RAND_MAX + 1.0)))
    slope->dx *= -1;
  if ((int)(2.0 * rand() / (RAND_MAX + 1.0)))
    slope->dy *= -1;

(At this point, it becomes obvious that the "slope" is not truly a slope, because in the realm of SDL Pong, a slope of -1/-1 is not the same as a slope of 1/1; that's OK, however.)

Now all you have to do is make sure the genSlope() function gets called at some point. Right after the call to resetSprites(), but before we enter the black hole that is the main loop, would seem a logical choice:

  // Initialise our sprite locations
  resetSprites( &game, 0 );
  
  // Randomly generate the starting slope
  genSlope( &game );

  // Main loop
  while (1) {

At this point, however, you have nothing to show for all of your hard work on slope-related issues, as the ball still does not move.

Adding a call to the moveBall() function right before the delay and the end of the main loop should do the trick:

    // Move the ball
    moveBall( &game );

    // Give the CPU a break
    SDL_Delay( GAME_SPEED );
    
  } // while (main loop)

That is, it should do the trick once you add a function definition to the top of the file:

// Function definitions
int  cleanUp( int err );
void genSlope( GameData *game );
void moveBall( GameData *game );
void movePaddle( GameData *game, int player, int dir );
void resetSprites( GameData *game, int erase );

and then implement the moveBall() function:

/* Function: moveBall()
 *
 * Moves the ball.
 *
 * Parameters:
 *
 *   *game - game data
 */

void moveBall( GameData *game ) {

  // Erase the current ball
  SDL_FillRect( game->screen, &(game->ball), game->black );
  game->rects[game->num_rects++] = game->ball;

  // Move the ball (reset height and width, as going off the screen seems
  // to compress them)
  game->ball.x += game->slope.dx;
  game->ball.y += game->slope.dy;
  game->ball.w  = BALL_W;
  game->ball.h  = BALL_H;
  
  // If the ball hits the top or bottom wall, bounce it
  if (game->ball.y <= 0 || game->ball.y >= (SCREEN_HEIGHT - game->ball.h)) {

    // Add a sound effect here?

    // According to my grade eight geometry class, "the angle of refraction
    // equals the angle of incidence" (thanks, Mrs. Lott!), so let's just
    // multiply the y component of our slope by -1 to change its sign
    game->slope.dy *= -1;

  } // if (ball bouncing off top or bottom wall)

  // If the ball has hit a player's paddle, bounce it
  if (((game->ball.x <= game->p1.w) &&
       (game->ball.y >= game->p1.y &&
        ((game->ball.y + game->ball.h) <= game->p1.y + game->p1.h))) ||
      (game->ball.x >= (SCREEN_WIDTH - (game->p2.w + (game->ball.w))) &&
       (game->ball.y >= game->p2.y &&
        ((game->ball.y + game->ball.h) <= game->p2.y + game->p2.h)))) {

    // Add a sound effect here?

    // Multiply the x component of our slope by -1 to change its sign; see
    // note above on elementary geometry
    game->slope.dx *= -1;

  } // if (bouncing off paddle)
  
  // If the ball hits the left or right wall, score a point for the
  // appropriate player and return the ball to the centre
  else if (game->ball.x < 0 || game->ball.x > (SCREEN_WIDTH - game->ball.w)) {

    // Return the paddles and ball to their starting positions
    resetSprites( game, 1 );
    
    // Generate a new slope
    genSlope( game );
    
  } // else if (score!)

  SDL_FillRect( game->screen, &(game->ball), game->white );
  game->rects[game->num_rects++] = game->ball;

} // moveBall()

The beginning of the function is trivial stuff to us by now: erase the ball by filling the rectangle corresponding to its current location with the background color, then adding the change-in-x slope component to the ball's x coordinate and the change-in-y to its y coordinate. (We reset the width and height of the ball here because going off of the screen seems to squash the ball in some cases, so better safe than sorry.)

It is when the ball hits the top or bottom of the screen (and you know this by the y coordinate--if it is less than or equal to 0, it hits the top, and greater than or equal to SCREEN_HEIGHT minus ball height, it is the bottom--just as the paddles worked) that we will whip out the second law of geometry: the angle of refraction equals the angle of incidence. This is easy, because all you need to do is flip the sign on the dy component of the slope, which keeps the angle the same, just reverses the direction.

You must do the same thing, except to dx, when the ball collides with a player's paddle. Instead of trying to explain in prose the complicated conditional conundrum that I use for detecting such a collision, I invite you to turn once again to your trusty scratch paper, and draw the requisite shapes. All should be made clear, including the bug in my collision detection algorithm that I dubbed a feature, since it gives enterprising readers of this hack something productive to do right away. Read the last section of this hack for more details on my laziness-slash-goodwill.

Of course, if the ball goes off of the left or right side of the screen without colliding with a paddle, a point has been scored. For now, all SDL Pong does in response to such a joyous occasion is reset the sprites (this time asking resetSprites() to erase the sprites from their current location, by virtue of setting the second parameter to a true value), randomly generate a new slope for the ball, then return.

The last thing that moveBall() must do, if a point is not scored, is to draw the new location of the ball sprite.

Run gcc again to recompile it:

gcc -g -Wall -I/usr/include/SDL -o sdl-pong sdl-pong.c -lSDL

and then run the game (see Figure 1):

./sdl-pong

Figure 1
Figure 1. Pong, with moving ball

One Last Score, and then I'm Out

This section addresses the vexing lack of scoring in SDL Pong. So let's open a whole new can of worms: SDL_TTF, which is an extra SDL library that deals with TrueType fonts--that is what the "TTF" means. To open the can, include the string.h and SDL_ttf.h headers at the top of sdl-pong.c:

// Standard library headers
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

// SDL headers
#include <SDL.h>
#include <SDL_ttf.h>

Add some new macros, as well:

#define GAME_POINTS 10 // number of points to win the game

#define MSG_FONT "/usr/share/fonts/TTF/Vera.ttf" // font for messages
#define MSG_SIZE 18                              // font size
#define MSG_TIME 1500    // display duration of messages

GAME_POINTS is self-explanatory; the other three less so. Basically, we are going to use the TrueType font defined by the MSG_FONT macro to display some messages in MSG_SIZE-point type, and will display the messages for MSG_TIME milliseconds.

Note: depending on the whims of your Linux distribution or Unix flavor's package system, your TrueType fonts may not reside in the /usr/share/fonts/TTF directory, and you may not have the Vera.ttf font on your system. That is OK; there is nothing magical about /usr/share/fonts/TTF, and any TrueType font that you have on your system will work just fine. To find your TrueType fonts, either grep your X11 configuration file (most likely /etc/X11/XF86Config or /etc/X11/xorg.conf) for "FontPath", or run: find / -name \*.ttf.

Now, you will need a new structure definition:

// Structure definitions
typedef struct {

  int p1;
  int p2;

  int game_points;
  
} Score;

And while you are at it, add two new members to the GameData structure:

  Slope    slope; // slope of the line representing the ball's path
  Score    score; // score of the game

  TTF_Font *font; // message font

Now, you need to initialize the score to 0-0, and the number of points for a win to GAME_POINTS:

  // Initialise game data
  game.running           = 1;
  game.ball_speed        = BALL_SPEED;
  game.slope_max_dy      = SLOPE_MAX_DY;
  game.p1_speed          = P1_SPEED;
  game.p2_speed          = P2_SPEED;
  game.num_rects         = 0;
  game.score.p1          = 0;
  game.score.p2          = 0;
  game.score.game_points = GAME_POINTS;

Right after initializing SDL in your code, add:

  // Initialise TTF engine and load a font
  TTF_Init();
  if ((game.font = TTF_OpenFont( MSG_FONT, MSG_SIZE )) == NULL) {

    fprintf( stderr, "Could not open font: %s\n", MSG_FONT );
    return cleanUp( 2 );
    
  } // if (could not load font)

This performs whatever initialization SDL_TTF requires, and then attempts to load the MSG_FONT font and scale it to the proper size. Calling TTF_Init() introduces a slight new wrinkle: you must tear down SDL_TTF before exiting the program. No problem; you can just add a line to the cleanUp() function:

int cleanUp( int err ) {

  TTF_Quit();
  SDL_Quit();

  return err;

} // cleanUp()

Now, all of the remaining action is set in the moveBall() function; specifically inside that else/if block that used to just reset the sprites and regenerate the slope. Let's add scorekeeping code to this block:

  // If the ball hits the left or right wall, score a point for the
  // appropriate player and return the ball to the centre
  else if (game->ball.x < 0 || game->ball.x > (SCREEN_WIDTH - game->ball.w)) {

    SDL_Color white = { 0xff, 0xff, 0xff, 0 };
      
    SDL_Rect rect_msg      = { SCREEN_WIDTH / 2 - 90, 100, 200, 50 };
    SDL_Rect rect_score_p1 = { 100,                   200, 150, 50 };
    SDL_Rect rect_score_p2 = { SCREEN_WIDTH - 200,    200, 150, 50 };

    SDL_Rect rects[3];
    
    char str_msg[32], str_score_p1[16], str_score_p2[16];
    SDL_Surface *text_msg, *text_score_p1, *text_score_p2;

    if (game->ball.x < 0)
      game->score.p2++;
    else if (game->ball.x > (SCREEN_WIDTH - game->ball.w))
      game->score.p1++;

    // Write scoring messages
    snprintf( str_msg, 32, "Player %d scores!", 
      ((game->ball.x < 0) ? 2 : 1) );

    snprintf( str_score_p1, 16, "Player 1: %d", game->score.p1 );
    snprintf( str_score_p2, 16, "Player 2: %d", game->score.p2 );

    text_msg      = TTF_RenderText_Solid( game->font, str_msg,      
        white );
    text_score_p1 = TTF_RenderText_Solid( game->font, str_score_p1, 
        white );
    text_score_p2 = TTF_RenderText_Solid( game->font, str_score_p2, 
        white );
    
    // Display scoring messages
    rects[0] = rect_msg;
    rects[1] = rect_score_p1;
    rects[2] = rect_score_p2;

    SDL_BlitSurface( text_msg,      NULL, game->screen, 
        &rect_msg      );
    SDL_BlitSurface( text_score_p1, NULL, game->screen, 
        &rect_score_p1 );
    SDL_BlitSurface( text_score_p2, NULL, game->screen, 
        &rect_score_p2 );

    SDL_UpdateRects( game->screen, 3, rects );

    // Display the score for awhile
    SDL_Delay( MSG_TIME );

    // Erase scoring messages
    SDL_FillRect( game->screen, &rect_msg,      game->black );
    SDL_FillRect( game->screen, &rect_score_p1, game->black );
    SDL_FillRect( game->screen, &rect_score_p2, game->black );
    
    SDL_UpdateRects( game->screen, 3, rects );

    // Has someone just won the game?
    if (game->score.p1 == game->score.game_points ||
        game->score.p2 == game->score.game_points) {

      // Display the final score
      snprintf( str_msg, 32, "Player %d wins!",
                ((game->ball.x < 0) ? 2 : 1) );

      snprintf( str_score_p1, 16, "Player 1: %d", game->score.p1 );
      snprintf( str_score_p2, 16, "Player 2: %d", game->score.p2 );

      text_msg      = TTF_RenderText_Solid( game->font, str_msg,      
        white );
      text_score_p1 = TTF_RenderText_Solid( game->font, str_score_p1, 
        white );
      text_score_p2 = TTF_RenderText_Solid( game->font, str_score_p2, 
        white );
    
      rects[0] = rect_msg;
      rects[1] = rect_score_p1;
      rects[2] = rect_score_p2;

      SDL_BlitSurface( text_msg,      NULL, game->screen, 
        &rect_msg      );
      SDL_BlitSurface( text_score_p1, NULL, game->screen, 
        &rect_score_p1 );
      SDL_BlitSurface( text_score_p2, NULL, game->screen, 
        &rect_score_p2 );

      SDL_UpdateRects( game->screen, 3, rects );

      // Pause for awhile
      SDL_Delay( MSG_TIME * 2 );

      // End the game
      game->running = 0;
      return;
      
    } // if (game over!)

The goal of all of this code is simply to display three messages at different locations on the screen. When a player scores, the game displays "Player X scores!" in the top center of the screen, and then each player's new score on his side of the screen. This is accomplished with the help of a slew of local variables: three SDL_Rect structures, one for each message (the in-line initialization may be new to some of you; the first field of the SDL_Rect structure is the x coordinate, followed by the y coordinate, followed by the width, followed by the height); a local array of SDL_Rect structures to feed to our old friend SDL_UpdateRects(); a string for each message; and finally, an SDL_Surface pointer for each.

The first thing to do is to add a point to the score of the player who just propelled the ball off of the other player's side of the screen. Then, write the three message strings using the standard C function snprintf(), provided by the string.h header file (see the PRINTF(3) manpage for details on how to use snprintf() if it is new to you). Now, call SDL_TTF's TTF_RenderText_Solid() function to turn these strings into graphics, draw those graphics onto the main screen using SDL_BlitSurface(), then use that old standby, SDL_UpdateRects(), to make sure the changes to the main screen are displayed to the user.

After all of this excitement, wait for MSG_TIME milliseconds to make sure the players have time to read the new score, and then erase the messages by filling their rectangles with the background color and calling SDL_UpdateRects() again.

Now, it is possible that the point that was just score was the game-winner. If so, we go through the string-printing, text-rendering, surface-blitting, rects-updating dance again, but this time, instead of saying "Player X scores!" say "Player X wins!" After this, wait twice the normal MSG_TIME to allow for the elaborate cursing that will surely ensue, and then set the running member of the GameData structure to a false value (C's only false value being 0, of course), and finally return. This, if you remember the organization of the main loop, will cause control to break out of the loop, and fall through to the end of the main() function, which cleans up and exits.

One more time, run gcc again to recompile your sdl-pong.c file, this time adding -lSDL_ttf to the command-line arguments so that the libSDL_ttf.so shared library gets linked in:

gcc -g -Wall -I/usr/include/SDL -o sdl-pong sdl-pong.c \
  -lSDL -lSDL_ttf

and then run the game:

./sdl-pong

Now you should have SDL Pong in all its glory, as shown in Figure 2.

Figure 1
Figure 2. Pong, with a scoring system

Hacking the Hack

Though it pains me to admit it, SDL Pong is not a perfect game. There is plenty of room for improvement, and I hope that some readers of this hack who are interested in playing around with SDL will pick up where I've left off.

If you're interested, here is a list, in no particular order, of suggested improvements. Use them as a jumping-off point, or not at all.

Resources

If you're looking for more information to help you become a cross-platform, game-programming wizard, be sure to check out some of the following resources:

Josh Glover has been hacking code for as long as anyone can remember. He is employed as a Unix systems administrator by Amazon.co.jp.


Return to the Linux DevCenter.

Copyright © 2009 O'Reilly Media, Inc.