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


Retro Gaming Hacks, Part 2: Add Paddles to Pong

by Josh Glover, contributor to Retro Gaming Hacks
12/21/2005

In this hack, I will show you how, with the help of sprites, you can create and animate player-controlled paddles for the SDL Pong clone you built in part one of this three-part series (part three is also online). For the uninitiated to the spidery language of video game programmers, a sprite is "a small graphic that can be moved independently around the screen, producing animated effects." Some might argue that the SDL_Rect-based "sprites" we will use to represent the two paddles in our game are not proper sprites at all, but as I could not think of a better way to refer to them, sprites they are.

Sprites a Go-Go

The first thing you will need to do is add some macros to control the initial positions of the sprites. It will once again be handy to grab your old geometry book, as well as a heap of scratch, or even graph, paper. The tricky part about 2D computer graphics is that you must make use of a so-called left-handed coordinate system. This sounds daunting, but it is as simple as taking a normal X-Y graph, throwing away all but the upper-right quadrant, and then having the audacity to suggest that Y=0 is not in the bottom left-hand corner of the quadrant, but rather in the top left. And to top it all, Y values increase as you move down in the quadrant. Revolutionary, I know.

If you are having trouble wrapping your head around this, simply imagine it's 1996 and your monitor is running in 640 by 480 resolution. Point your finger to the upper left-hand corner. You are now at (0,0). Move your finger halfway down the screen, but keep it flush with the left side of the screen. This is (0,240). Move all the way down and you are at (0,479) (not 480, since the screen is only 480 pixels tall and we started counting, like all good coders do, at 0). Move all the way right and you are at (639,479). All the way up from there is (639,0). Finally, put your finger right in the middle of the screen. That is (320,240) and that is where we want to place our ball.

Right, so back to the coding. Add these lines to the section where you are declaring macros, right beneath the screen width and height:

// Macro definitions
#define SCREEN_WIDTH  640 // width of the screen, in pixels
#define SCREEN_HEIGHT 480 // height of the screen, in pixels

// Width, height, and starting x and y coordinates of ball
#define BALL_W 4
#define BALL_H 4
#define BALL_X (SCREEN_WIDTH  / 2) - 2
#define BALL_Y (SCREEN_HEIGHT / 2) - 2

// Width, height, and starting x and y coordinates of player 1's paddle
#define P1_W 5
#define P1_H 50
#define P1_X 0
#define P1_Y (SCREEN_HEIGHT / 2) - (P1_H / 2)

// Width, height, and starting x and y coordinates of player 2's paddle
#define P2_W 5
#define P2_H 50
#define P2_X SCREEN_WIDTH - P2_W
#define P2_Y (SCREEN_HEIGHT / 2) - (P1_H / 2)

Does all of this make sense, within the context of the devious left-handed coordinate system? The ball is a 4-by-4 rectangle (since X comes before Y in a coordinate set, I will always talk about width before height, to avoid confusion and brain malfunctions), and it is sitting right in the middle of the screen. We will track all three of our sprites by their upper left-hand corners, so positioning the upper left-hand corner of the ball at (318,238) results in it being centered. Player 1's paddle starts at (0,215), and extends five pixels to the right and 50 down. Player 2's paddle starts at (635,215), and also extends five pixels to the right and 50 down, putting its right edge against the right edge of the screen, just as the left edge of Player 1's paddle is flush with the left edge of the screen.

The next step is to create a structure that will hold all of our sprite-related information, as well as all of our game state. If you decide to write a game in C++, you would probably use a class for this, but we shall forge on bravely in pseudo object-oriented C. Add the following code just beneath the Structure definitions comment:

typedef struct {

  int running; // is the game running?
  
  SDL_Surface *screen; // main window

  SDL_Rect ball; // ball

  SDL_Rect p1; // player 1's paddle
  SDL_Rect p2; // player 2's paddle

  int      num_rects; // number of rectangles to update
  SDL_Rect rects[16]; // rectangles to update

  Uint32 black; // the colour black
  Uint32 white; // the colour white

} GameData;
Retro Gaming Hacks

Related Reading

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

Since we have moved the *screen pointer into the new GameData structure, change the variable declarations from the top of the main() function from:

  SDL_Event    event;  // SDL events
  SDL_Surface *screen; // main game window

to:

  GameData   game;  // game data
  SDL_Event  event; // SDL events

And change the:

  if ((screen =
       SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, 8, SDL_SWSURFACE ))
      == NULL) {

to:

  if ((game.screen =
       SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, 8, SDL_SWSURFACE ))
      == NULL) {'

(You just need to add a game. before the screen.) Now, right below the variable declarations, you need to initialize the GameData structure:

  // Initialise game data
  game.running   = 1;
  game.num_rects = 0;

The reason that you added the running variable to the GameData structure is so that you can check the game state from any subroutine that takes a pointer to the structure. With that in mind, let's change the handling of the SDL_QUIT event and the escape key ever so slightly, from:

      if (event.type == SDL_QUIT ||
          (event.type == SDL_KEYDOWN &&
           event.key.keysym.sym == SDLK_ESCAPE))
        return cleanUp( 0 );

to:

      if (event.type == SDL_QUIT ||
          (event.type == SDL_KEYDOWN &&
           event.key.keysym.sym == SDLK_ESCAPE))
        game.running = 0;

Change the entire last line, but nothing else, and add a new chunk of code right after the end of the event loop:

    // If we have been told to exit, do so now
    if (game.running == 0)
      break;

This has the same effect as before, since breaking out of the main loop results in the program exiting through the use of the cleanUp() function, but keeping track of whether the game is running in the GameData structure allows you to modify it in function calls, which might prove useful later.

But the point of this section is supposed to be sprites, not the refactoring of the game's code to allow for extensibility! So let's return to sprites. Add the code shown in bold right after where you set the title bar's caption and hide the mouse cursor:

  // Set window caption and turn off the mouse cursor
  SDL_WM_SetCaption( "SDL Pong", "SDL Pong" );
  SDL_ShowCursor( SDL_DISABLE );

  // Get black and white colours
  game.black = SDL_MapRGB( game.screen->format, 0x00, 0x00, 0x00 );
  game.white = SDL_MapRGB( game.screen->format, 0xff, 0xff, 0xff );
  
  // Initialise our sprite locations
  resetSprites( &game, 0 );

We use SDL_MapRGB() (see sdldoc.csn.ul.ie/sdlmaprgb.php) to grab the color values of white and black (foreground and background), as dictated by the color map of the main window. Then call the resetSprites() function to draw the sprites.

And speaking of resetSprites(), you had better get around to writing it, or it won't be doing much good. Add its definition right below that of cleanUp():

// Function definitions
int  cleanUp( int err );
void resetSprites( GameData *game, int erase );

And add the following implementation at the end of the file:

/* Function: resetSprites()
 *
 * Moves all sprites back to their starting positions.
 *
 * Parameters:
 *
 *   *game  - game data
 *    erase - if true, sprites will be erased first
 */

void resetSprites( GameData *game, int erase ) {

  // Erase sprites from current locations?
  if (erase) {

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

  } // if (erasing sprites)

  // The ball is a 2x2 sprite, centred both horizontally and vertically
  game->ball.x = BALL_X;
  game->ball.y = BALL_Y;
  game->ball.w = BALL_W;
  game->ball.h = BALL_H;

  // Player 1's paddle is a 5x50 sprite, flush left and centred vertically
  game->p1.x = P1_X;
  game->p1.y = P1_Y;
  game->p1.w = P1_W;
  game->p1.h = P1_H;

  // Player 2's paddle is a 5x50 sprite, flush left and centred vertically
  game->p2.x = P2_X;
  game->p2.y = P2_Y;
  game->p2.w = P2_W;
  game->p2.h = P2_H;
    
  // Draw the playing field
  SDL_FillRect( game->screen, &(game->ball), game->white );
  SDL_FillRect( game->screen, &(game->p1),   game->white );
  SDL_FillRect( game->screen, &(game->p2),   game->white );
  game->rects[game->num_rects++] = game->ball;
  game->rects[game->num_rects++] = game->p1;
  game->rects[game->num_rects++] = game->p2;

} // resetSprites()

The first parameter passed into resetSprites() is the GameData structure, and the second one controls whether the sprites are first erased (by filling their current locations with black, the background color) before they are reset and redrawn. Since all we are concerned with right now is drawing the sprites, and have therefore set the second parameter resetSprites() to 0, all resetSprites() has to do is put all three sprites back in their original positions by setting the x, y, w (width), and h (height) of each rectangle to the initial values that we defined as macros in the beginning of this section. SDL_FillRect() is then called, with the target surface (the main window) as the first parameter, a pointer to the SDL_Rect structure that is being filled as the second, and the fill color as the third. Since resetSprites() itself receives the GameData structure as a pointer, it must de-reference the game pointer to get at the rectangle in question. But since that is a structure and not a pointer, it must pass the address of the SDL_Rect structure to SDL_FillRect(); again, don't worry too much about this if it is making your head hurt. As long as you always use this notation within the confines of the resetSprites() function, you will be fine.

Believe it or not, calling SDL_FillRect() does not necessarily mean that the screen will be updated to display the newly filled rectangle, since SDL has all sorts of performance concerns to weigh. This is why you will find the following three crazy lines of code in the resetSprites() function:

  game->rects[game->num_rects++] = game->ball;
  game->rects[game->num_rects++] = game->p1;
  game->rects[game->num_rects++] = game->p2;

What this code is doing is adding the three rectangles that were just filled to the array of rectangles that will be updated first thing in the main loop (well, once we actually write the main loop). Note that we are incrementing the num_rects member of the GameData structure in the same line in which we do the assignment.

To actually force the screen update, add the lines of code shown in bold at the very beginning of the main loop:

  // Main loop
  while (1) {

    // (Re)draw the screen
    if (game.num_rects) {

      SDL_UpdateRects( game.screen, game.num_rects, game.rects );
      game.num_rects = 0;

    } // if (updating screen)

SDL_UpdateRects() takes as its parameters the surface to update (the main window), the number of rectangular regions to update, and the array of rectangles itself. Because only a few regions of the screen are being updated, you can achieve flicker-free graphics (for Pong, at least) without having to deal with double-buffering (which, in SDL, requires a hardware surface, and we are getting away with just a software surface).

Run gcc again to re-compile it:

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

and then run it (see Figure 1):

./sdl-pong

Figure 1
Figure 1. Paddle and ball sprites

You have drawn the sprites on the screen, and our little Pong clone is starting to look like an actual game--except for the fact that there is no actual gameplay as of yet. Let's remedy that by making the paddles movable.

You need to start by defining some concept of speed: how far should a paddle be able to move in one iteration of the main loop? Let's add some new macro definitions below the others in the sdl-pong.c file:

// Default paddle speed for players
#define P1_SPEED 3
#define P2_SPEED 3

// See the dir parameter to movePaddle()
#define DIR_UP   1
#define DIR_DOWN 2

// Number of milliseconds the game will sleep at the end of the main loop;
// the higher the number, the slower the game speed. Note that you will
// probably want to change BALL_SPEED, P1_SPEED, and P2_SPEED when you change
// this.
#define GAME_SPEED 30

GAME_SPEED is simply the number of milliseconds that we will delay at the end of the main loop, which we are already doing, but will now do properly. First, add a game_speed member to the GameData structure:

  int running; // is the game running?
  int game_speed;   // the game speed

and initialize it:

  // Initialise game data
  game.running    = 1;
  game.game_speed = GAME_SPEED;

Then change the SDL_Delay() line from:

    SDL_Delay( 30 );

to:

    SDL_Delay( game.game_speed );

The reason for changing the delay time from a hard-coded number to a macro is so that you can easily change it at the top of the file and recompile for testing different settings. This is why I have made heavy use of macros thus far in this hack.

The P1_SPEED and P2_SPEED macros define the number of pixels that Player 1 or 2 (respectively) can move his paddle every GAME_SPEED number of milliseconds. You must also add two members to the GameData structure to represent this information:

  SDL_Rect p1;       // player 1's paddle
  SDL_Rect p2;       // player 2's paddle

  int      p1_speed; // player 1's paddle speed
  int      p2_speed; // player 2's paddle speed

(Yes, I did re-indent the comments beside the p1 and p2 members to line up with the new comments. That's how I roll.)

As a rule, whenever you add new members to the GameData structure, you also need to add code to initialize them:

  // Initialise game data
  game.running    = 1;
  game.game_speed = GAME_SPEED;
  game.p1_speed   = P1_SPEED;
  game.p2_speed   = P2_SPEED;
  game.num_rects  = 0;

We will use the keyboard as our input device, which should work out quite nicely for a two-player game with only two controls per player. For Player 1's "up" and "down" controls, we pick the A and Z keys, and for Player 2, the ' and / keys. The only criteria asserted here is that the "up" key should be right above the "down" one, and that the controls for Player 1 and Player 2 be at opposite sides of the keyboard. Let's declare a new variable, *keystate, which will hold the keyboard state:

  // Declare variables
  GameData   game;     // game data
  SDL_Event  event;    // SDL events
  Uint8     *keystate; // keyboard state

We will query the keyboard state using the SDL function SDL_GetKeyState(), right after handling events in the main loop:

    // If we have been told to exit, do so now
    if (game.running == 0)
      break;

    // Move sprites about

    // Grab a keystate snapshot
    keystate = SDL_GetKeyState( NULL );

SDL_GetKeyState() returns an array, with a true value at each element that corresponds to a key that is currently being pressed. We will use the same SDLK_* macros that we used earlier to handle keyboard events, but this time as indexes into the *keystate array. Right below the call to SDL_GetKeyState(), add:

    // Has player 1 requested a move?
    if (keystate[SDLK_a])
      movePaddle( &game, 1, DIR_UP );
    else if (keystate[SDLK_z])
      movePaddle( &game, 1, DIR_DOWN );

    // Has player 2 requested a move?
    if (keystate[SDLK_QUOTE])
      movePaddle( &game, 2, DIR_UP );
    else if (keystate[SDLK_SLASH])
      movePaddle( &game, 2, DIR_DOWN );

See how reasonably the macros are named? And if you cannot guess one, you can always refer to the SDLKey documentation.

The only thing left to do is to define and implement the elusive movePaddle() function. In the function definitions section of your code, add:

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

And just before the implementation of the resetSprites() function, add:

/* Function: movePaddle()
 *
 * Moves a player's paddle.
 *
 * Parameters:
 *
 *   *game   - game data
 *    player - player number
 *    dir    - direction of the move (DIR_UP or DIR_DOWN)
 */

void movePaddle( GameData *game, int player, int dir ) {

  int new_y;
  int moved;

  SDL_Rect  tmp;
  SDL_Rect *rect = (player == 1 ? &(game->p1) : &(game->p2));

  int speed = (player == 1 ? game->p1_speed : game->p2_speed);
  
  // Compute the new y coordinate of the rectangle and the pixels moved
  new_y = (dir == DIR_UP ? (rect->y - speed) : (rect->y + speed));
  moved = (dir == DIR_UP ? (rect->y - new_y) : (new_y - rect->y));

  // If the move would take us off the top or bottom of the screen,
  // we may have to move less than speed
  if (dir == DIR_UP && new_y < 0) {

    new_y = 0;
    moved = rect->y - new_y;
    
  } // if (moving up less than speed)
  
  else if (dir == DIR_DOWN && new_y > SCREEN_HEIGHT - rect->h) {

    new_y = SCREEN_HEIGHT - rect->h;
    moved = new_y - rect->y;
    
  } // else if (moving down less than speed)

  // If we have not moved, just return
  if (moved == 0)
    return;
  
  // Erase the top or bottom line(s) of the paddle
  tmp.x = rect->x;
  tmp.y = (dir == DIR_UP ? (rect->y + rect->h - moved) : rect->y );
  tmp.w = rect->w;
  tmp.h = moved;

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

  // Apply the new y coordinate of the rectangle
  rect->y = new_y;

  // Draw the new bottom or top line(s) of the paddle
  tmp.y = (dir == DIR_UP ? rect->y : (rect->y + rect->h - moved) );
  
  SDL_FillRect( game->screen, &tmp, game->white );
  game->rects[game->num_rects++] = tmp;

} // movePaddle()

movePaddle() takes three parameters: the ubiquitous GameData structure, the number of the player that is moving (i.e., 1 or 2), and the direction of movement (either DIR_UP or DIR_DOWN). The first thing we do in the function is grab the SDL_Rect structure and speed corresponding to the player that has moved, and assign them to local variables, as shown in this excerpt from movePaddle():

  SDL_Rect *rect = (player == 1 ? &(game->p1) : &(game->p2));

  int speed = (player == 1 ? game->p1_speed : game->p2_speed);

I make use here of C's ternary operator, which can be expressed as:

<condition> ? <true_expression> : <false_expression>

I use it just so that I can accomplish in one line what would take several using a standard if/else control structure:

  SDL_Rect *rect;
  if (player == 1)
    rect = &(game->p1);
  else
    rect = &(game->p2);

If you ever get confused by a ternary operator, simply "unroll" it as above: the bit after the ? would come after the if, and the bit after the : would come after the else.

The next step is to compute the new Y coordinate of the paddle, and then decide how far it has moved, as shown in this excerpt:

  // Compute the new y coordinate of the rectangle and the pixels moved
  new_y = (dir == DIR_UP ? (rect->y - speed) : (rect->y + speed));
  moved = (dir == DIR_UP ? (rect->y - new_y) : (new_y - rect->y));

The equation for determining the new Y coordinate is quite simple. If the paddle is moving up, subtract the speed from the current Y coordinate; otherwise, the paddle is moving down, so add the speed to the current Y coordinate (remember that Y values grow downwards in a left-handed coordinate system). To determine how many pixels were moved, we just subtract the new Y coordinate from the old one. This is so that you can detect whether full movement would take the paddle off of the top or bottom of the screen (which you decidedly do not want). Here's how movePaddle() does it:

  // If the move would take us off the top or bottom of the screen,
  // we may have to move less than speed
  if (dir == DIR_UP && new_y < 0) {

    new_y = 0;
    moved = rect->y - new_y;
    
  } // if (moving up less than speed)
  
  else if (dir == DIR_DOWN && new_y > SCREEN_HEIGHT - rect->h) {

    new_y = SCREEN_HEIGHT - rect->h;
    moved = new_y - rect->y;
    
  } // else if (moving down less than speed)

If you detect that the paddle has moved too far, simply reset the new_y variable to the minimum value, if moving up, or the maximum one, if moving down. Remember, the X and Y coordinates of the paddle represent its upper left-hand corner, so when detecting movement off the bottom of the screen, we need to subtract the height of the paddle from SCREEN_HEIGHT. After resetting new_y, you must also recalculate how far the paddle has moved. It could be that the paddle has not moved at all, if it was already at the top of the screen and the player tried to move up, or if it was at the bottom and the player tried to move down. If this is the case, you can just return from the movePaddle() function:

  // If we have not moved, just return
  if (moved == 0)
    return;

Now you need to handle the movement graphically. The first thing to do is to erase the paddle, but instead of simply filling the entire rectangle corresponding to the current location of the paddle with black (the background color), fill only the bit that changed. This is to eliminate flicker, the dreaded condition where the user can actually perceive the redraw operation--it looks like an old movie that was shot at fewer frames per second than modern ones. (Another way to avoid flickering is to use double-buffering, which SDL makes quite easy, but you must use a hardware surface for your main window, which we have avoided in this hack for the sake of code clarity and also to ensure that SDL Pong runs well on a wide variety of computing machinery.)

The region of the screen that has changed can be represented by a rectangle (and this is why we declared the tmp variable at the top of the function) with the same X coordinate as the paddle (since the paddle cannot move horizontally, only vertically) and the same Y coordinate as the paddle, if moving down (since we need to erase the top few lines of the paddle); or the paddle's Y coordinate, plus the height of the paddle, minus the number of pixels moved. The width of the rectangle is the same as the width of the paddle, and the height is the number of pixels moved, whether moving up or down:

  // Erase the top or bottom line(s) of the paddle
  tmp.x = rect->x;
  tmp.y = (dir == DIR_UP ? (rect->y + rect->h - moved) : rect->y );
  tmp.w = rect->w;
  tmp.h = moved;

If this numbers game does not make sense, try sketching out the rectangles on a piece of scratch paper (or graph paper, if you have it).

To actually erase the region, we call SDL_FillRect() with the following parameters: the screen pointer, a pointer to the tmp rectangle, and the background color (black). Then, we add the rectangle to the rects member of the GameData structure--the array of rectangles that are automatically updated at the beginning of the main loop, thanks to the coding we did in the sprites section of this hack.

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

You now need to update the SDL_Rect structure corresponding to the paddle with the new Y coordinate so that movePaddle() knows where the paddle is next time it is called. We are nearing the end now; the only thing left is filling the new top or bottom region of the paddle. To do this, simply set the Y coordinate of the temporary rectangle, tmp, to the new Y coordinate of the paddle, if moving up. Or, if moving down, the new Y coordinate plus the height of the paddle, minus the number of pixels moved. You can then proceed to fill the temporary rectangle with the foreground color (white) in the normal fashion:

  // Draw the new bottom or top line(s) of the paddle
  tmp.y = (dir == DIR_UP ? rect->y : (rect->y + rect->h - moved) );
  
  SDL_FillRect( game->screen, &tmp, game->white );
  game->rects[game->num_rects++] = tmp;

} // movePaddle()

Run gcc again to re-compile it:

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

Now run the game:

./sdl-pong

You'll be able to move the paddles around (Figure 2), but you still need to add a moving ball--check back the first week of the new year, when we'll cover just how to do that.

Figure 2
Figure 2. Pong, with moving paddles

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.