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()
|
Related Reading Retro Gaming Hacks |
|
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. Pong, with moving ball
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 2. Pong, with a scoring system
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.
Allow the user to set the various bits of configuration data that exist in the GameData structure by passing arguments on the command line.
The collision detection algorithm I've used for figuring out when the ball has hit the paddle is not so great, as evidenced by the damage done to the paddle by the ball. This is simply because the ball moves between one and five pixels at a time in the x direction, and the paddles are five pixels wide, and happen to present an edge to the ball that starts at an x coordinate that is not usually divisible by the change-in-x component of the slope. Add math until it works properly.
This is related to collision detection: fix the bug that allows the ball to bounce off of the inside of a paddle, if it hits the top or bottom edge of the paddle just right.
Monochrome is so '70s. Colorize Pong.
Using flood-filled rectangles for graphics is also so '70s. Investigate SDL_image and make Pong use PNG, JPG, or GIF graphics. Better yet, let the user select the paddle and ball graphics, and scale them to the appropriate size dynamically.
My physics model is simplistic and boring. Add "english" to the ball depending on which direction, and maybe even how fast, the paddle is moving when it hits the ball.
Any game with power-ups and specials is better. Add effects like exploding balls, Hyper Power Zero Shots, time stopping, and so on. Better yet, make these power-ups obtainable between rounds by trading in "style points" or some such scoring invention.
Sound: SDL_mixer will let you provide an Ogg Vorbis soundtrack to the game, so you can add popping sound effects when the paddle strikes the ball. And maybe some crowd noise when a point is scored.
Add network play using SDL_net. Make sure to come up with some way to find games. For a challenge, encapsulate your network protocol in HTTP (it might take a week to play a point, but it would be cool).
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.