How to Program a Pong Game for Linux
using Weaver
Pong is one of the earliest arcade video games, and is a tennis sports game featuring simple two-dimensional graphics. The aim is to defeat an opponent—either computer-controlled or a second player—by earning a higher score. The game was originally manufactured by Atari Incorporated (Atari), who released it in 1972.
Since it's one of the simplest and oldest computer games, it's a good exercise to learn game programming creating a Pong clone.
Part 1: Planning the Game
It's not wise to start coding without know what you re doing. Before start to code, spend some time planning how should be your game. What a player should see in the screen while playing it? If possible, draw something in a piece of paper. Write all your ideas and also how should you implement them.
In this example, our plan is to create a pong clone. Almost the whole screen will be occupied by the game area. The game area is the scenario where a ball will be bouncing and the two paddles controlled by the player(s) should prevent the ball from going out the screen. In the top of the screen should be the scores. A player marks a score if the ball go out the other side of the screen. The game ends when someone marks 12 points. If the ball goes out from the screen and nobody has 12 points, it appears again in the center of the screen.
A paddle should interfere with the ball's movement. If the ball hits the center of the paddle, it should bounce horizontally. If the ball hits the paddle far from it's center, it should bounce in a more acute angle.
This should be a graphical game. It should have a background and beautiful sprites for the ball and paddles.
Part 2: Creating a New Project
Now that we know what we should do, open a terminal and type:
weaver pong
Assuming there's no file called "pong" in the current directory, after this command, a new one will be created. Enter in the new directory, try to compile our game and execute it:
cd pong make ./pong
If everything is working properly, these commands will run an empty Weaver Game. It consists of a black empty fullscreen window that closes when you press any key. It's not a fun game, but it's a good start.
Our main problem now is that we don't want a game that exits if the player presses any key. The player should use some keyboard keys (like Shift and Ctrl) to move the Pong Paddle and the game should exit only if the player presses the Esc button.
To implement this, open with a text editor the file src/game.c. Find a line where is written something like "if(keyboard[ANY]){" and change it this way:
get_input(); if(keyboard[ESC]){ break; }
Now compile and run the game again:
make ./pong
It works, doesn't it?
Part 3: Creating the Background
Our game window will be divided in two areas. The first one, is formed by the 100 first pixels of the screen and will show each player's score. The second one will be the game area, where all the action will happen.
First we'll need a texture to put in the background. You can use any texture encoded as a PNG file. Here's a suggestion:
Save the PNG file as texture.png in the directory pong/images/. Now, inside the pong directory, create a new module called "background" typing:
weaver background
The above command creates the files pong/src/background.h and pong/src/background.c.
Weaver already have a surface designed to store background images in our games. A surface is an image that we can manipulate, drawing on the screen or combining with other surfaces to generate new images. We will just create a function to initialize this surface. Declare the function in background.h:
void initialize_background(void);
Open the background.c file and create the function:
// This function initializes our background. It should be called once in // the beginning of the program void initialize_background(void){ // First we load the file "texture.png" located in the directory "images" surface *texture = new_image("texture.png"); // Now we apply the texture to our bigger surface. apply_texture(texture, background); // Done. Now we don't need the texture anymore. We destroy the surface // that stores the texture. destroy_surface(texture); }
And finally, open the game.c and before the main loop (but after the "awake_the_weaver" call), hide the mouse cursor, call the function "initialize_background", draw the background and draw a yellow rectangular frame around the background to delimitate our game area:
hide_cursor(); initialize_background(); draw_surface(background, window, 0, 100); draw_rectangle(0, 100, window_width-1, window_height - 101, YELLOW);
Now let's compile and execute our masterpiece:
make ./pong
If everything was done correctly, you should see something like this:
Part 4: Creating the Paddles
The first thing that we need to create the paddles is a PNG file with their images. Search in the Internet something. Or use these suggestions:
Notice that in the above images, the background is filled with the color 0x00029a, a kind of dark blue. When you try to draw a loaded image and this color is found, Weaver assumes that this area is transparent. So, if you draw these images in the screen, only the flying saucers will appear, not the blue background.
Save the two images (one for each paddle) in the directory pong/images witrh the names paddle1.png and paddle2.png. Next, let's create a new module to our program to deal with the paddles. From inside the pong directory, type in the terminal:
weaver paddle
Next, open the newly created paddle.h and declare:
struct paddle{ int x, y; // Coordinates surface *image; } *paddle1, *paddle2; void initialize_paddles(void); void move_paddle(struct paddle *, int);
And inside the paddle.c file, define the previously declared functions:
// This initializes the paddles. It should be called once in the // beginning of the game void initialize_paddles(void){ // Allocating space paddle1 = (struct paddle *) malloc(sizeof(struct paddle)); paddle2 = (struct paddle *) malloc(sizeof(struct paddle)); // Loading the images paddle1 -> image = new_image("paddle1.png"); paddle2 -> image = new_image("paddle2.png"); // Setting the first paddle coordinates paddle1 -> x = 1; paddle1 -> y = 100 + ((window_height - 100) / 2) - (paddle1 -> image -> height / 2); // Setting the second paddle coordinates paddle2 -> x = window_width - paddle2 -> image -> width - 1; paddle2 -> y = 100 + ((window_height - 100) / 2) - (paddle2 -> image -> height / 2); } // This moves a paddle up or down. void move_paddle(struct paddle *pad, int direction){ // We don't move if this would put us outside the game area if((direction == UP && pad -> y > 102) || (direction == DOWN && pad -> y + pad -> image -> height < window_height - 1)){ // First we erase the given paddle drawing a rectangle with the background // in the area where the paddle is standing. blit_surface(background, window, pad -> x, pad -> y - 100, pad -> image -> width, pad -> image -> height, pad -> x, pad -> y); // Next we change the paddle coordinate pad -> y += ((direction == UP)?(-1):(1)); // And finally we draw the paddle again draw_surface(pad -> image, window, pad -> x, pad -> y); } }
Now in game.c, rightly before the main loop, initialize the paddles and draw them in the screen:
initialize_paddles(); draw_surface(paddle1 -> image, window, paddle1 -> x, paddle1 -> y); draw_surface(paddle2 -> image, window, paddle2 -> x, paddle2 -> y);
And still in game.c, inside the main loop, after the conditional that exits if the player presses ESC, add the following lines of code:
if(keyboard[LEFT_SHIFT]) move_paddle(paddle1, UP); else if(keyboard[LEFT_CTRL]) move_paddle(paddle1, DOWN); if(keyboard[RIGHT_SHIFT]) move_paddle(paddle2, UP); else if(keyboard[RIGHT_CTRL]) move_paddle(paddle2, DOWN);
Now the payer 1 can move the paddle using the keys Left Ctrl and Left Shift while the player 2 controls the paddle with Right Shift and Right Ctrl.
Compile and run the game:
make ./pong
Part 5: Making the Game Run at Same Speed in All Machines
Currently our game have some problems. We don't have much control over the paddle speed (sometimes move the paddle 1 pixel per frame is too slow, but move 2 pixels per frame is too fast). And in slower computers, our paddle would move slower than in faster computers.
One possible solution for this problem would be store in each object it's speed (in pixels per second) and also their position as floating point numbers that should be rounded down before we drawn them in the screen. But we will use another solution, similar to this.
We are going to store in each paddle a variable called "speed" and "cy" that would store an offset of "centipixels" in the objects. So, if a paddle has y=250 and cy=49, it's position is 250.49 . Of course, we can increase the precision using "milipixels" instead of "centipixels", but in this case, a precision of 2 decimal places would be enough. In paddle.h, replace the previous version of the paddle struct declaration with:
struct paddle{ int x, y, cy; // Coordinates int speed; // Pixels per second surface *image; } *paddle1, *paddle2;
As we added new variables to the paddle struct, we should also upddate the initialization function to initialize the new variables too. Open paddle.c and replace the old "initialize_paddles" function with:
// This initializes the paddles. It should be called once in the // beginning of the game void initialize_paddles(void){ // Allocating space paddle1 = (struct paddle *) malloc(sizeof(struct paddle)); paddle2 = (struct paddle *) malloc(sizeof(struct paddle)); // Loading the images paddle1 -> image = new_image("paddle1.png"); paddle2 -> image = new_image("paddle2.png"); // Setting the first paddle coordinates paddle1 -> x = 1; paddle1 -> y = 100 + ((window_height - 100) / 2) - (paddle1 -> image -> height / 2); paddle1 -> cy = 0; // Setting the second paddle coordinates paddle2 -> x = window_width - paddle2 -> image -> width - 1; paddle2 -> y = 100 + ((window_height - 100) / 2) - (paddle2 -> image -> height / 2); paddle2 -> cy = 0; // Setting speed paddle1 -> speed = paddle2 -> speed = 200; }
Now, to update the "move_paddle" function to use these new information, we should know how fast our program is running in the machine. This is easy. The global variable "fps" stores how many frames per second the game is processing. Knowing how many pixels we move in a second and knowing our current frames per second rate, discover how many pixels we should move is a matter of simple math. Here's our new "move_paddle" function:
// This moves a paddle up or down. void move_paddle(struct paddle *pad, int direction){ // We don't move if this would put us outside the game area if((direction == UP && pad -> y > 102) || (direction == DOWN && pad -> y + pad -> image -> height < window_height - 1)){ // Updating paddle cy value int move = (pad -> speed * 100) / fps; pad -> cy += ((direction == UP)? (-move):(move)); // If we moved more than 100 "centipixels", we should erase and draw the paddle again if(pad -> cy >= 100 || pad -> cy <= -100){ // First we erase the given paddle drawing a rectangle with the background // in the area where the paddle is standing. blit_surface(background, window, pad -> x, pad -> y - 100, pad -> image -> width, pad -> image -> height, pad -> x, pad -> y); // Next we change the paddle coordinate pad -> y += pad -> cy / 100; pad -> cy %= 100; // And finally we draw the paddle again draw_surface(pad -> image, window, pad -> x, pad -> y); } } }
Compile and test to check if everything was done correctly:
make ./pong
Part 6: Creating the Score
Now that the paddles are created, let's create the scores. The scores are two numbers located in the upper left and right side of the screen, representing how many points each player made.
Weaver still doesn't have support for True Type Font and other font formats. Because this, to write numbers in the screen, we will use a PNG file with all the ten digits drawn:
Then, to draw a number like "10" in the screen, we first draw the region of the image that countains the number 1 and then the number 0.
Save that image as numbers.png in the directory pong/images/ and create a new module to handle the score manipulation:
weaver score
Now in score.h, declare the variables that'll store the scores and also a function to draw the score in the screen:
int score1, score2; surface *numbers; void initialize_scores(void); void draw_scores(void);
And now create the function to initialize the scores and other to draw them in the screen. Use the technic that we learned with the paddles:
// This initialies our score and gets the image with the numbers that we'll // draw in the screen void initialize_scores(void){ score1 = score2 = 0; numbers = new_image("numbers.png"); } // This draws the 2 scores in the screen. As our maximum score is "12", we // don't treat cases when the sore have more than 2 digits. void draw_scores(void){ int digit; // All the numbers are 46x46 pixels surface *buffer = new_surface(46, 46); // Drawing the Player 1 score... // The first digit: fill_surface(buffer, BLACK); digit = score1 / 10; blit_surface(numbers, buffer, 46*digit, 0, 46, 46, 0, 0); draw_surface(buffer, window, 27, 27); // The second digit: fill_surface(buffer, BLACK); digit = score1 % 10; blit_surface(numbers, buffer, 46*digit, 0, 46, 46, 0, 0); draw_surface(buffer, window, 27+35, 27); // Drawing the Player 2 score... // The first digit: fill_surface(buffer, BLACK); digit = score2 / 10; blit_surface(numbers, buffer, 46*digit, 0, 46, 46, 0, 0); draw_surface(buffer, window, window_width - 27 - 35 - 46, 27); // The second digit: fill_surface(buffer, BLACK); digit = score2 % 10; blit_surface(numbers, buffer, 46*digit, 0, 46, 46, 0, 0); draw_surface(buffer, window, window_width - 27 - 46, 27); // Done. Destroying the buffer... destroy_surface(buffer); }
And finally, right before the main loop in game.c, call the functions to initialize the scores and draw them for the first time:
initialize_scores(); draw_scores();
Also update the conditional that exits if the player presses Esc. It also should exit now if someone scores 12 points:
if(keyboard[ESC] || score1 >= 12 || score2 >= 12){
Compile and run the game:
make ./pong
If everything was done correctly, your game currently should have this appearance:
Part 7: Creating the Ball
The ball is the most complex element in a Pong game. It can move vertically, but unlike the paddles, it can move also horizontally. It produces noise when hits a paddle. It's speed may change over time. And it can affect the scores.
Before creating the ball code, we must have an image file to represent it. Here's a suggestion:
Save it as pong/images/ball.png. Now we need an Ogg Vorbis file to store the sound made when the ball hits the paddle (in our example, when the asteroid hits the flying saucer) and when the ball goes off the screen. Here's two suggestions:
Save the sounds as pong/sound/hit.ogg and pong/sound/explosion.ogg.
Now create a new module for the ball:
weaver ball
And edit the ball.h file to declare the ball and functions to initialize and move it:
struct ball{ int x, y, cx, cy; // The ball's coordinates int dx, dy; // The ball's slope surface *image; int speed; } *ball; void initialize_ball(void); void move_ball(void);
The "initialize_ball" function shall be in ball.c file. Initialize a ball is easy:
// This function initializes the ball void initialize_ball(void){ ball = (struct ball *) malloc(sizeof(struct ball)); ball -> image = new_image("ball.png"); // Putting the ball in the center of screen: ball -> x = (window_width - ball -> image -> width) / 2; ball -> y = (window_height - ball -> image -> height) / 2 + 50; ball -> cx = ball -> cy = ball -> dy = 0; // How many pixels per second the ball starts moving // It should be able to cross the screen in 10 seconds ball -> speed = window_width / 10; // The ball can go left or right. It's random: ball -> dx = ((rand()%2)?(ball -> speed):(-ball -> speed)); }
Notice that we don't need to initialize a seed for the "rand" function. Weaver does it for us in the game initialization.
Now write the function to move the ball:
// This function moves the ball in the screen void move_ball(void){ // Checking if the ball went off the screen if(ball -> x <= -(ball -> image -> width)){ score2 ++; ball -> x = window_width / 2; ball -> y = window_height / 2 + 100; ball -> dx *= -1; draw_scores(); draw_rectangle(0, 100, window_width-1, window_height - 101, YELLOW); play_sound("explosion.ogg"); } else if(ball -> x >= window_width + ball -> image -> width){ score1 ++; ball -> x = window_width / 2; ball -> y = window_height / 2 + 100; ball -> dx *= -1; draw_scores(); draw_rectangle(0, 100, window_width-1, window_height - 101, YELLOW); play_sound("explosion.ogg"); } // Checking if the ball collides with the first paddle: else if(ball -> x <= paddle1 -> x + paddle1 -> image -> width && (ball -> y >= paddle1 -> y - ball -> image -> height && ball -> y <= paddle1 -> y + paddle1 -> image -> height) && ball -> dx < 0){ int ball_center_y = ball -> y + ball -> image -> height / 2; int paddle_center_y = paddle1 -> y + paddle1 -> image -> height / 2; ball -> speed += 5; ball -> dy = -((float)(paddle_center_y - ball_center_y) / (float)(paddle1 -> image -> height / 2)) * 0.65 * ball -> speed; ball -> dx = (int) sqrtf((float)((ball -> speed)*(ball -> speed) - (ball -> dy)*(ball -> dy))); play_sound("hit.ogg"); return; } // Checking if the ball collides with the second paddle: else if(ball -> x + ball -> image -> width >= paddle2 -> x && (ball -> y >= paddle2 -> y - ball -> image -> height && ball -> y <= paddle2 -> y + paddle2 -> image -> height) && ball -> dx > 0){ int ball_center_y = ball -> y + ball -> image -> height / 2; int paddle_center_y = paddle2 -> y + paddle2 -> image -> height / 2; ball -> speed += 5; ball -> dy = -((float)(paddle_center_y - ball_center_y) / (float)(paddle2 -> image -> height / 2)) * 0.65 * ball -> speed; ball -> dx = -(int) sqrtf((float)((ball -> speed)*(ball -> speed) - (ball -> dy)*(ball -> dy))); play_sound("hit.ogg"); return; } // Checking if the ball is colliding with the top or bottom of screen if(ball -> y <= 100 && ball -> dy < 0){ ball -> dy *= -1; ball -> y = 102; draw_rectangle(0, 100, window_width-1, window_height - 101, YELLOW); return; } else if(ball -> y >= window_height - ball -> image -> height && ball -> dy > 0){ ball -> dy *= -1; ball -> y = window_height - ball -> image -> height - 1; draw_rectangle(0, 100, window_width-1, window_height - 101, YELLOW); return; } // Updating the values cx and cy ball -> cy += (ball -> dy * 100) / fps; ball -> cx += (ball -> dx * 100) / fps; // If we moved more than 100 centipixels, we need to update the movement if(ball -> cy >= 100 || ball -> cy <= -100 || ball -> cx >= 100 || ball -> cx <= -100){ blit_surface(background, window, ball -> x, ball -> y - 100, ball -> image -> width, ball -> image -> height, ball -> x, ball -> y); // Moving the ball... ball -> x += ball -> cx / 100; ball -> y += ball -> cy / 100; // Updating buffer and screen... draw_surface(ball -> image, window, ball -> x, ball -> y); // Updating the centipixels offset... ball -> cx %= 100; ball -> cy %= 100; } }
Wow... This will be our most complex function. First it checks if the ball went off the screen, if so, the score is updated and an explosion sound is played. Then, we check if the ball colides with something. If so, the ball's slope is updated and if the collision happened with a paddle, a collision sound is played. Finally, we check if we need to erase and redraw the ball according with how many centipixels it moved.
The ball starts the game slow. But after some collisions, it's speed increases. The point in which the ball hits the paddle interfere with it's angle. We use some analytical geometry properties to make these changes.
Now update the main function in game.c putting right before the main loop:
initialize_ball(); draw_surface(ball -> image, window, ball -> x, ball -> y);
Then, inside the main loop, after the input handling, call the new function:
move_ball();
Compile and test the changes:
make ./pong
Part 8: Final Details
Every game must have a music. Play games in silence is boring. To learn how to set a music for our game, you can download the sample below and save as pong/music/einklang.ogg.
It's a music created by the canadian band Wannsee and disponible for download under the license Creative Commons Attribution-Noncommercial-No Derivative Works 2.5 Canada.
And inside your main function, before the main loop, call:
play_music("einklang.ogg");
Now compile and play:
make ./pong
This finishes the last piece of code from this tutorial. If everything is OK, you can install your new game. As root, run:
make install
If you wish to download the game shown in this tutorial, use this link:
This is a screenshot from our game:
THE END