Objectifying Pong
Take a look at the video to the right to see an example of the transformation.
Let’s start by going back and objectify our ‘Pong’ game. Looking at our game, we can see that we only have 2 logical categories for our classes: a ‘Paddle’ class and a ‘Ball’ class. Let’s use the following class structure to objectify our code:
class Paddle(): def __init__(self, x, y): # Initialize the Paddle’s variables. def update(self, down, up): # Update the Paddle’s position class Ball(): def __init__(self, x, y): # Initialize the Paddle’s variables. def update(self, left_paddle, right_paddle): # Update the Ball’s position.When objectifying our code, we need to ask ourselves, which variables are associated with the paddle? Looking at our code, we can see that the paddle needs (x,y) values to specify where on the screen it should be blitted. The paddle also needs a speed, width, height and lastly, the paddle needs an ‘image’ variable to contain the Surface object which contains the image of the paddle. Let’s add those to the constructor of our Paddle class. Remember that the constructor is used to initialize our variables and that we need to set these variables to their initial values.
Your constructor should look like this:
def __init__(self, x, y): self.x = x self.y = y self.speed = 8 self.score = 0 self.height = PADDLE_HEIGHT self.width = PADDLE_WIDTH self.image = pygame.image.load("image/paddle.png").convert_alpha() self.image = pygame.transform.scale(self.image, (self.width, self.height))Now try running your code. You should notice that your paddles disappeared. This is because we now need to create an instance of our Paddle object. Add the following code:
def main(): ... left_paddle = Paddle(WIN_W/15, (WIN_H/2)-(PADDLE_HEIGHT/2)) right_paddle = Paddle(WIN_W/1.1, (WIN_H/2)-(PADDLE_HEIGHT/2)) ... while intro:Since we created objects that contain each paddle’s set of properties, we need to make a change to the bottom of our game loop. Replace the following code:
screen.blit(paddle1, (x1, y1)) screen.blit(paddle2, (x2, y2))with the following code:
screen.blit(left_paddle.image, (lelf_paddle.x, left_paddle.y)) screen.blit(right_paddle.image, (right_paddle.x, right_paddle.y))Now, if you run your code, you will see that our paddles are in place and our program runs well. This is because when we create our object ‘left_paddle’ at the top of our program, it automatically runs the constructor for the ‘Paddle’ class using the (x, y) values that will place it on the left side of the screen. The constructor also initializes all of the variables associated with the paddle, including ‘image’, which contains the Surface object that contains the image of the paddle. Looking at the change we made to the bottom of our game loop, we can see that we blit each paddle’s image to the screen using each paddle object’s properties: ‘image’, ‘x’ and ‘y’.
Objectifying Update
Now that we have created a constructor for our Paddle class, let’s objectify our ‘update’ method. Since our update method will be simply updating the paddle’s position, we can copy and paste the code that adjusts the speed of the paddle and the limits the paddle’s movement to within the top and bottom of the screen. Once you do that, you will see that the conditional statements that adjust the Paddle’s speed requires the user’s up and down keyboard inputs. This is why we defined the parameters of the ‘update’ method as ‘up’ and ‘down’ since the main function will need to pass this necessary information to the method. Once you have completed the steps above, your update method should look like this:
Running your program, you will see that your paddles now don’t move based on the user’s input. As you can probably guess, this is because we need to invoke the newly created ‘update’ method in the main function at the same place where this code was once located, towards the bottom of the game loop. Add the following invocation of ‘update’:
def update(self, down, up):Running your program, you will see that your paddles now don’t move based on the user’s input. As you can probably guess, this is because we need to invoke the newly created ‘update’ method in the main function at the same place where this code was once located, towards the bottom of the game loop. Add the following invocation of ‘update’:
# Adjust speed
if up or down:
if up:
self.y -= self.speed
if down:
self.y += self.speed
# paddle movement
if self.y < 0:
self.y = 0
if self.y > WIN_H - self.height:
self.y = WIN_H - self.height
… # Reading in the keyboard inputs from the user left_paddle.update(lp_moveDOWN, lp_moveUP) right_paddle.update(rp_moveDOWN, rp_moveUP) # Writing to the main surface using ‘flip’ method ...Running your program, you will see that now your paddle moves around the screen. You have just objectified your ‘Pong’ program!
Now Try
Now, objectify the ball by writing the constructor and the ‘update’ method.
Adding an Intro Screen
Every video game that I’ve played has started with an introduction screen, so let’s add one to our version of Pong. Before we do so, we need to understand that every video game ever created has a game loop that is used to read inputs from the user and change the backgrounds, sprites and text accordingly on the screen. It should follow that the introduction also requires its own loop to be run at the beginning of the algorithm just before the game loop. When we add an ending to our game, this element will also require another loop to be written after our game loop so when someone loses and our game is over and the game loop stops and the program will initiate the outro loop which will wait for a player action.
Let’s get started by writing a while loop before our game loop. Add the following code before your game loop:
… intro = play = True while intro: … while play: # Print background screen.fill(WHITE) # Checks if window exit button pressed for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() # Limits frames per iteration of while loop clock.tick(fps) # Writes to main surface pygame.display.flip()Notice that our intro loop has the same basic elements that our game loop has: a for loop that checks for user inputs, an invocation to the ‘tick’ method and an invocation to ‘display.flip()’. If you run your code, you will see a blank white screen that remains unchanging until we press the red button to exit the window. Our game is in this state because that is all we have programmed in our loop. We have filled the main screen with white, checked for the user to press the red button, limited the loop to 60 iterations per second and burned the white screen to the monitor.
Blitting the Title
This is a good start. Now, let’s add the text “Pong” as a title. In order to add text to our game we need to add the following code to our program:
… intro = play = True # Creating our text objects font = pygame.font.Font(None, 100) title = font.render("Pong", 1, BLACK) titlepos = title.get_rect() titlepos.centerx = screen.get_rect().centerx while intro: # Print background screen.fill(WHITE) screen.blit(title, titlepos) # Checks if window exit button pressed for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() # Limits frames per iteration of while loop clock.tick(fps) # Writes to main surface pygame.display.flip() while play: …If we look at the added code above, we can see that we needed an invocation to the method ‘Font’. The ‘Font’ method has two arguments: a font type and the font size. The ‘Font’ method returns an object that will be written in the type of font and size that was given the method as arguments. Now that we have our font object, we can invoke ‘render’ to get a surface that has the text we want to print. You can see that the ‘render’ method takes in three arguments: text, anti-aliasing, and a RGB color code.
Render Returns a Surface
We must understand that the ‘render’ method returns a surface that is the width and height of the text found in the first argument. In other words, our variable ‘title’ will contain a surface that will contain the text ‘Pong’, displayed in the standard font type and set to size 100pt. Remember that this surface is not the size of the entire screen, it is simply the size of the text. This is similar to the surface that contains the ball and paddles, which is the size of the image of the ball and paddles respectively.
Looking at the next line, we have an invocation of ‘get_rect()’ that returns the rect for the surface in the variable ‘title’. Now that we have a rect for the surface containing the text ‘Pong’, we have access to the ‘centerx’ property. ‘centerx’ is a property of a rect that contains the value of the rect’s center point. By setting the ‘centerx’ of the title’s rect to the ‘centerx’ of the screen’s rect, we are positioning the title’s midpoint equal to the midpoint of the screen. Effectively, this centers the position of the text. Finally, we blit the surface and the rect to the main screen. Remember that when we blit, we are sending the method the surface with the image that we want to print and the location to place the image, in this case, a rect.
If you run your program, you will see that your intro screen that was previously only white, now has the text ‘Pong’ in 100pt font placed in the center of the screen. However, there is a problem because the text doesn’t go away. This is because our intro loop runs forever. We can fix this by adding blinking text that tells the user to click on the screen to get started.
You Try
Add another line of text that says ‘- Click here to start -‘ just below ‘Pong’ in smaller text. Be sure to center the text and make sure your spacing is even.
Blinking Text
We have an intro loop that shows the title and the message telling the player that they need to click on the screen to get the game started. Now we need to get our text to blink on and off every half second. In order to get this done, we need to keep track of two pieces of data: the time the game started and the current time. The first piece of data is fixed and needs to be calculated outside of the intro loop. The second piece of data is variable and needs to be calculated inside the intro loop. Add the following code to your algorithm:
… beg_time = pygame.time.get_ticks() intro = play = True lp_moveUP = lp_moveDOWN = rp_moveDOWN = rp_moveUP = False while intro: # Print background screen.fill(WHITE) screen.blit(title, titleRect) # Blinking Text: Click here to start cur_time = pygame.time.get_ticks() if ((cur_time - beg_time) % 1000) < 500: screen.blit(click, clickRect) … # Checks if window exit button pressedTaking a look at the code above, we can see that we created a variable called ‘beg_time’ and set it equal to the time when the program was first run. This time data is in milliseconds, where 1000 milliseconds equals 1 second. We need this time as an anchor that will allow us to calculate when to show the text and when to not show the text.
Next, let’s look at the place where we create the variable ‘cur_time’. Here we set this variable equal to the current time as the intro loop is running. Every time we enter the loop, ‘cur_time’ is set to the current time. Now, in our conditional statement, we are subtracting the beginning time from the current time and modding by 1000. Since the time data is in milliseconds, if mod the difference of the times by a 1000 and check if the result is less than 500, the branch statement will be evaluated to true every other half second. This is perfect because all we need to do is blit the text that states “- Click here to start –“ in the branch and for half a second the text will be displayed and for the next half second it won’t be.
Mouse Button Down
Try out your code and you’ll see that now you have a title and blinking text that tells the user to click on the screen. However, we aren’t done yet. You should notice that if you click on the screen, nothing happens. This is because we didn’t program the computer to break out of the loop if the user clicks on the screen. Let’s do this now:
# Checks if window exit button pressed for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() elif event.type == pygame.MOUSEBUTTONDOWN or pygame.key.get_pressed()[pygame.K_RETURN] != 0: screen.blit(click, clickRect) pygame.display.flip() pygame.time.wait(1500) intro = FalseIf you add the above code to your intro loop code, you will see that your program now ends the intro loop if the user clicks on the screen. To see why this works, take a close look at the second branch of the if/else statement. You can see that the first expression in the condition is true if the user presses the mouse button down in the game window. The second expression checks if the user presses the return button on the keyboard. Either of these actions should start the game. Now looking into the code block that executes when either of these conditions are true, we can see that the click text is blitted, the display is flipped, the algorithm waits for 2 seconds and the intro variable is set to false.
Let’s take each of these commands one at a time. The blit needs to happen because it feels weird if the user clicks on the screen when the click text is not shown. If the user clicks on the screen when the click text isn’t showing, this line ensures that the text surface will be printed to the main screen. The invocation of ‘display.flip()’ burns the main surface to the screen. The next command, ‘pygame.time.wait(1500)’ forces the game to wait for 1.5 seconds. This allows time for there to be a sound effect played. (We are going to lay down the dynamics of the entire game first, then add sound effects at the end.) Lastly, we set the intro variable equal to false so that the intro loop will exit and the game loop will start. Play your game and you will see that we now have an intro!
Keeping Score
In our game’s current state, a game cannot end because we never stop the game when the ball goes off either end of the court. Furthermore, we never keep score. Lastly, we don’t end the game loop when the score reaches some arbitrary number and designate a winner. Let’s do that now.
To start, we need to have a variable for each paddle that is incremented every time the other side loses the ball. Since we have a paddle class, we can simply add a ‘score’ property and initialize it to zero in the constructor. After we do this, when our game starts, each paddle has a score that is set to zero and can be incremented with the following command:
left_paddle.score += 1 or right_paddle.score += 1Now that we have our ‘score’ properties for each paddle, we need to display them onto the screen. Since we have gone over how to create a font object, create a text surface and blit the surface to the main screen, we now need to show each player what their score is at the top of the screen.
You Try
Display each player’s score at the top center of each player’s side. Remember that we haven’t programmed the incrementation of each paddle’s score property just yet, so our display functionality should always display “Player 1 score: 0 Player 2 score: 0”.
Incrementing ‘score’
Now that we have our scores displayed, our programs need to increment the score variable when the ball goes off the screen. To start this, add the following logic to your game loop:
class Ball(): ... def update(lpaddle, rpaddle): if ball.rect.right < 0: right_paddle.score += 1 ...Taking a look at the condition in the branch, we can see that the algorithm is incrementing player 2’s score if the x-coordinate of the right side of the ball’s rect is less than 0. Since the top left of the screen equates to (0, 0) and the bottom right of the screen equates to (WIN_W, WIN_H), this condition will be true when the ball is no longer on the screen. If you run your code, you’ll see that if the ball runs off the left side of the screen, player 2’s score will go up. However, ‘right_paddle.score’ will continue to increment as fast as the while loop runs. Before we correct this issue, complete the logic that will check if the ball moves off the right side of the screen.
You Try
Write a second condition to the unary conditional statement that will check if the ball goes off the right side of the screen and increments player 1’s score.
Reposition the Ball
We have our scores incrementing correctly but they won’t stop incrementing because the ball isn’t being moved back to the center of the screen. To do this, we need to pause the game for a second and reposition the ball and paddles to their starting positions. Then after the pause, we should start the game loop again. We could do this in the game loop, but since we have a paddle class, instead let’s put the code in a paddle method and run the method in the game loop instead.
def restart(self, left_paddle, right_paddle): time.sleep(1) self.rect.y = WIN_H/2-(BALL_HEIGHT/2) self.rect.x = WIN_W/2 left_paddle.y = (WIN_H/2) - (PADDLE_HEIGHT/2) right_paddle.y = (WIN_H/2) - (PADDLE_HEIGHT/2)Taking a look at the ball method above, we can see that it pauses the game for a second, sets the x and y properties of the ball’s rect to their starting values, and the paddles y properties are repositioned to their starting values. We can run this code by adding the following code in our game loop:
# If ball moves off the screen if ball.rect.left < 0 - ball.rect.width or ball.rect.left > WIN_W + ball.rect.width: if ball.rect.left < 0: right_paddle.score += 1 elif ball.rect.left > WIN_H + ball.rect.width: left_paddle.score += 1 ball.restart(left_paddle, right_paddle)Looking at the nested conditional statement above, we can see that the inner conditional statement checks if the ball runs off either side of the screen and increments accordingly. Looking at the outer conditional statement, we can see the conditional expression checks if the ball runs off either side of the screen and runs the ball’s restart method whenever that occurs. Just as a note, we could have omitted the outer conditional statement and simply placed the invocation of the ‘restart’ method in both of the inner conditional statements branches, however as programmers, we shouldn’t repeated code and try to avoid it at all costs.
After running your code it should be obvious that our problem of an ever increasing score value is fixed and our game is restarting each time the ball leaves the screen. Great job! We are very close to a complete game.
You Try
You will notice that your game pauses when the ball leaves the screen, then blit’s the ball and paddle to the screen and immediately begin the game. This feels rushed. Fix this problem by pausing the game for a second after the ‘restart’ method has been invoked, the ball and paddle have been repositioned, the ball and paddle have been blitted to the screen and the screen has been ‘display.flip()’ed.
You Try
You will notice that your game never ends. Add a conditional to your game loop that breaks when someone earns 3 points.
Outro Loop
The dynamics of our game is just one step away from being complete. Currently, when the first player reaches 3 points, the game window simply disappears. Instead of simply exiting the window, our game should have an ending loop that displays an appropriate message and tells the user to click the window to continue. Continuing the game should restart back to the intro screen.
You Try
Create an ending loop that runs after the game loop breaks. Be sure to print an appropriate message that announces who won, shows blinking text that tells the user to click on the screen to continue, starts the game over again after the screen is clicked.
Sound
What’s a game without sound? Let’s add to our game by introducing sound. You can run a search for "8-Bit Sounds", or you can download the sound files I used here. If you are going to search for your own sounds, understand that you only need 7 sounds: choosing to play, ball hitting a wall, ball hitting a paddle, ball going off the screen, game ending and restarting. Pick out sounds that fit each of these situations. These sound files are most likely mp3’s or wav files, however these types of files may not run on some systems. According to the Pygame documentation, we should be using ogg files.
You can easily convert our files using http://audio.online-convert.com/convert-to-ogg. After converting your files, create a folder called ‘sound’ in your project and place your ogg files there. Be sure to rename each file according to what the file will be used for. My files are name ‘choose’, ‘beep’, ‘bop’, ‘boom’, ‘end’ and ‘select’. It was my hope that the names of each file would make the function for which each will be used obvious.
Loading Sound Objects
To load our sound objects, we are going to be using a data structure called a dictionary and the ‘pygame.mixer.Sound()’ method. A dictionary is exactly like an array, except that its indexes are words rather than numbers. An array might look like this:
array = [] for i in range(10): array[i] = i * iWhile a dictionary that contains the ages of a set of humans might look like this:
name = {} name[“tom”] = 36 name[“dick”] = 25 name[“harry”] = 72We are going to use a dictionary to hold all of our sound objects by doing the following at the top of our main function:
sound = {} sound[“beep"] = pygame.mixer.Sound("sound/beep.oog")These lines of code first create an empty dictionary and then create the first element in the dictionary that is delineated by the index ‘beep’.
You Try
Load the rest of the sound objects into the dictionary. To avoid confusion, be sure to delineate each index using the same name as the name of each sound file.
Playing Sounds
Now that we have our sound files loaded into our ‘sound’ dictionary, we can play them by running the following command:
sound[“beep”].play()Furthermore, we can set the volume of each sound by running the following code:
sound[“beep”].set_volume(1)The above code must be run just before running the ‘play’ method. The argument for the ‘set_volume’ method designates the level of volume. Zero equals no sound and 1 equals maximum sound. If the ‘play’ method is run without being preceded by the ‘set_volume’ method, then the default volume is 1.
Take a look at the following code to see an example of these methods being used:
def update(self, lPaddle, rPaddle, sound): # If ball hits the top or bottom if self.rect.top < 0 or self.rect.top > WIN_H - BALL_HEIGHT: self.speed[1] = -self.speed[1] sound["bop"].set_volume(.5) sound["bop"].play()The code above would play the ‘bop’ sound at half volume when the ball hits the top or bottom walls.
You Try
Play sounds where you need them. Be sure to have at least 7 sounds in your program.
Now your game has sound!
Graphics
The last missing element is the use of graphics. Let’s start by downloading the graphics files here. You should create a folder called ‘image’ and place all the graphic files in there. Note that you can search for your own images, however realize that the images for the paddle and ball need to be transparent so that the ball is circular rather than a square.
We can now load all the graphic files into Pygame using the following code at the beginning of our program:
# Creating game objects … # Create our graphic objects intro_back = pygame.image.load("image/introBackground.jpg").convert() intro_back = pygame.transform.scale(intro_back, (WIN_W, WIN_H)) clock = pygame.time.Clock() … # Creating and initializing loop variablesThe first line of code creates an image object by invoking the ‘load’ method. Take note that the ‘load’ method takes a path to the file (‘/image/introBackground.jpg’). The second command is an invocation to ‘scale’ which scales the image to the desired height and width. In our case, we want the height and width to be the height and width of the screen. On the same line of code, we also invoke the ‘convert’ method, which converts the image into one that is easily processed by Pygame. Be sure to understand that the value is a tuple of the form (x, y). Let’s start by creating all of our image objects.
You Try
Create an image object for each graphic file in the ‘image’ folder.
Blitting Graphics
If you now try running your program, you will see that there is no change in the appearance of your program. This is because we have simply created the objects, but we haven’t used them by blitting them on the screen, then using ‘display.flip()’ to burn that screen onto the monitor. Let’s do that now.
We need to start with the ‘introBackground.jpg’ file by showing it to the user during the intro portion of our game. We can do this by going into the intro loop and deleting the call to ‘screen.fill(white)’ and replacing it with a command to blit. Add the following code to your program.
# Creating and initializing variables. … while intro: # screen.fill(WHITE) screen.blit(intro_back, (0,0)) # Title Text: Pong … # Code to print title and blinking textYou can see that all I asked you to do is replace the first line of code in the intro loop with blit command. Instead of filling the background with white, we are blitting the ‘introBackground.jpg’ to the main screen. Remember that we don’t have to worry about the background color because when we created the ‘introBackground.jpg’ file, we specified its size to be the size of the screen.
Now that we have the background blitted to the main screen, that background will be burned onto the monitor because there is an invocation to ‘display.flip’ at the end of the intro loop. Try running your program and you will see that you now have a background for your intro. However, there is now a problem because your text is black and that doesn’t look very good against a dark green background. Let’s change the color of all our font objects to be a light shade of green using the following RGB values: (212, 243, 200). If you haven’t yet, you should create a global variable called ‘GREEN’ and set it equal to that RGB value. Now you can use the variable ‘GREEN’ when defining the color of your font instead of typing all of those strange numbers.
You Try
Redefine all of your font objects to use the color ‘GREEN’ instead of ‘BLACK’. Check out the difference in your intro loop. Take note that you may want to adjust the sizes of your font objects for aesthetic purposes.
You Try
Replace the white background in your game loop and your outro loop.
Ball and Paddles
After doing the above tasks, you will notice that your game plays a bit strange because once you start, your ball and paddle are black. This is unusual because your background is primarily black as well. We need to change our ball and paddle so that they use a graphic image rather than simply fill the rect with black.
Let’s start with our ball. Change this ball method:
def __init__(self, x, y): Entity.__init__(self) self.speed = [-5, -5] self.ball = pygame.Surface((BALL_WIDTH, BALL_HEIGHT)) self.rect = pygame.Rect(WIN_W/2, WIN_H/2-(BALL_HEIGHT/2), BALL_WIDTH, BALL_HEIGHT)To this:
def __init__(self, x, y): Entity.__init__(self) self.speed = [-10, -10] self.ball = pygame.image.load("image/ball.png").convert_alpha() self.ball = pygame.transform.scale(self.ball, (BALL_WIDTH, BALL_HEIGHT)) self.rect = self.ball.get_rect() self.rect = self.rect.move(x, y)What we were doing in the first version of the ball method was creating a surface that was 20 pixels by 20 pixels and placing the surface in ball. The last line of code specified what location to place the rect on the screen. Understand that this last line was setting the x and y value of the ball’s rect, which was effectively moving the rect to that specific location. Also understand that when you create a surface without specifying a color, the surface is defaulted to black, which explains why our ball and paddles are black.
Now let’s look at the second ball method. You can see we are loading the ball image and this call to ‘load’ returns a surface that contains an image of the ball. We also invoke the ‘convert_alpha’ method to make sure that the ball keeps its transparency characteristics. Take a look at the ball file. It’s a square file that contains a ball which is on a transparent background. If we didn’t invoke ‘convert_alpha’, then this image would be a ball on a square background whose color is black.
Next, we invoked ‘transform.scale’, which takes the surface that has the picture of our transparent ball on it and stretches or compresses it to the (x, y) dimensions provided. Then we invoked ‘get_rect’ to get the rect that is associated with the surface that has the picture of our transparent ball on it. Finally, we changed the x and y properties of the ball’s rect to place it in the middle of the screen using the ‘move’ function.
Run your program and you should see a picture of a ball instead of black square. You may need to adjust the dimensions of the ball for aesthetic purposes.
You Try
Replace the black paddles with the graphics for the paddles.
Congratulations, you have created your first complete game using the Pygame module!
PROJECT
Add at least 4 of the following functionalities to your game:
- Add a countdown from 3, 2, 1 when each round starts.
- Fade in the score.
- Add two more paddles instead of walls for a four player game.
(Add any of the following PowerUps that follow the path of a sine wave towards each respective player.)
- Bigger paddle, smaller paddle for opponent
- Slower ball, faster ball for opponent
- Slower paddle for opponent.
- Allows next Power up to last 6 hits.
- Remove the effect of a Power up.
- Have hotkeys for Power ups.
rubric
can be found here.