Beginnings
Anyhow, we are going to code our own version of the game that includes images, motion, sounds, beginning/ending screens, and variations of your choice. As the old adage goes, every journey of a thousand miles starts with a single step. We will start by coding the original version, then give it a facelift.
Take a look at the video
above to get an idea of what your game will look like by the end of this tutorial.
When creating a video game using Pygame, we need to start by importing the Pygame module. We can do this by placing the following code at the top of our program:
import pygame
Gaming Window
Next, we need to start by creating the window in which the game will be shown to the user. We can do this by using the function ‘display.set_mode(WIN_W, WIN_H)’ with WIN_W and WIN_H being the width and height of the window respectively. A good idea is to create global variables for these values:
import pygame WIN_W = 920 WIN_H = 570 # Runs imported module pygame.init() def main(): pygame.display.set_caption('Pong') screen = pygame.display.set_mode((WIN_W, WIN_H), pygame.SRCALPHA) if __name__ == "__main__": main()
The short version of what’s happening is that the ‘main’ function is being invoked.
What is actually happening is something we don’t need to understand at the moment, however if you would like a further explanation, I would forward you to this thread found on stackoverflow.
Try running the program above and you will see that pygame creates a window for the briefest of moments and closes it. This is because the ‘display.set_mode’ command is run and then the interpreter moves to the next line of code which is the end of main and the program ends.
Game Loop
Something you have to understand is that Pygame is a video game engine, and
video game engines, from lowly Pygame to Unreal Engine 4, requires that a loop to be run throughout the duration of the game. In this loop, all commands will be executed repeatedly changing the state of the game. If a ball is bouncing across the screen, the ball is drawn on the screen in each iteration of the loop, moving its x and y location by just a bit. As the loop runs, it appears that the ball is bouncing across the screen. Once the loop ends, the program ends and thus the game.
Let’s add a game loop to our code:
def main(): pygame.display.set_caption('Pong') screen = pygame.display.set_mode((WIN_W, WIN_H), pygame.SRCALPHA) clock = pygame.time.Clock() play = True # Gameplay while play: print "hello world" # Limits frames per iteration of while loop clock.tick(60)One thing that may confuse you in the code above is the call to ‘clock.tick()’. What this is doing is limiting the number of times the loop can run to 60 times per second. You can try changing that value to 3 if you’d like to see what happens.
It’s important to realize that you must press the stop button on the bottom left-hand side of the Pycharm window to stop the program.
Setting a Background Color
Now, let’s get our screen to have a white background color by adding the following code:
def main(): pygame.display.set_caption('Pong') screen = pygame.display.set_mode((WIN_W, WIN_H), pygame.SRCALPHA) clock = pygame.time.Clock() play = True screen.fill((255, 255, 255)) # Gameplay while play: pygame.event.get() # Limits frames per iteration of while loop clock.tick(60)Notice that I have erased the unnecessary ‘print’ command. Also, notice that I added a call to ‘fill’, which fills our screen with the color white. Try running your program and remember you must press the stop button on the bottom left-hand side to stop the program. Our program is easier to understand and manipulate if we create a global variable for the color white.
# Constants WHITE = (255, 255, 255) def main(): ... screen.fill(WHITE) # Gameplay loop ...
Burning a Surface to the Monitor
Run your program and you will notice that nothing has changed and the screen does not have a white background. This is because of how Pygame works. It is very important for you to understand that in Pygame there are what we call ‘surfaces’. You can think of surfaces as transparent sheets of paper that have sprites or text on them. We must have a different surface for each sprite in our game. We can then print each of those surfaces onto our screen at specific (x, y) coordinates for the user to see. We can print a bird in the corner, a sun in the sky and a tree on the ground. Ultimately, we need to print our surfaces onto a special surface so that the user can see them. The function call to ‘display.set_mode’ creates that special surface that represents the user’s screen. It’s important that you remember that what you print to this surface will be seen by the user.
def main(): pygame.display.set_caption('Pong') screen = pygame.display.set_mode((WIN_W, WIN_H), pygame.SRCALPHA) clock = pygame.time.Clock() play = True screen.fill(WHITE) # Gameplay while play: pygame.event.get() # Limits frames per iteration of while loop clock.tick(60) # Writes to main surface pygame.display.flip()
Adding a Paddle Sprite
Run your program now and you will see that now your window is white! Wonderful. Now, let’s get a sprite on the screen. To get a paddle onto the screen, we need to make a call to ‘Surface’.
def main(): pygame.display.set_caption('Pong') screen = pygame.display.set_mode((WIN_W, WIN_H), pygame.SRCALPHA) # Paddle information paddle_width =20 paddle_height = 70 paddle = pygame.Surface((paddle_width, paddle_height)) clock = pygame.time.Clock() play = True screen.fill(WHITE) # Gameplay while play: pygame.event.get() # Limits frames per iteration of while loop clock.tick(60) # Writes to main surface pygame.display.flip()The invocation of the ‘Surface’ method by default returns a surface that is colored black which is of the dimensions (x,y), or in our case (paddle_width, paddle_height). The surface that is returned is saved in the variable ‘paddle’. Remember that we cannot see this new sprite until we print it to the surface found in the ‘screen’ variable. If you don’t believe me, try running your program.
'Blit'-ing
The way Pygame allows you to print surfaces to other surfaces, including the main surface, is by invoking the ‘blit’ method. It’s important to understand that ‘blit’ simply prints to a surface before burning it onto the monitor using ‘display.flip’.
If we have a ‘screen’ that is filled white and we want to show a sprite, we need to ‘blit’ the sprite onto ‘screen’ and then when we are finished with the ‘screen’ variable, we need to burn it to the monitor using ‘display.flip’. Let’s try using ‘blit’ to print our paddle onto the screen by adding the following code:
def main(): pygame.display.set_caption('Pong') screen = pygame.display.set_mode((WIN_W, WIN_H), pygame.SRCALPHA) # Paddle information paddle_width =20 paddle_height = 70 paddle = pygame.Surface((paddle_width, paddle_height)) clock = pygame.time.Clock() play = True screen.fill(WHITE) # Gameplay while play: pygame.event.get() # printing to the main screen screen.blit(paddle, (WIN_W/2, WIN_H/2)) # Limits frames per iteration of while loop clock.tick(60) # Writes to main surface pygame.display.flip()We can see that the invocation to ‘blit’ required the main surface found in ‘screen’ and takes the paddle surface and (x, y) as arguments.
It’s important to realize that ‘blit’ needs the ‘screen’ so that it knows where this surface should be printed on to, ‘blit’ needs the paddle variable to know what image to paste onto the screen and ‘blit’ needs (WIN_W/2, WIN_H/2) to know where on the gaming window to paste the paddle.
Try running your program and you’ll see a paddle!
Now Try:
Now try and re-position the paddle in the correct starting location on the left side of the screen. Use the global variables WIN_W and WIN_H in your positional calulation so that the paddle is placed in the correct location no matter what window dimensions are used.
Now Try:
Now that your paddle is in the right location, try adding second paddle to your screen. Be sure to change your existing code and call the first paddle ‘paddle_1’ and the second paddle ‘paddle_2’. Also be sure to use variables for the (x, y) locations of each paddle. Let’s call them (x1, y1) for the left paddle and (x2, y2) for the right paddle. Remember that variables should be created and initialized at the top of your main function.
Now Try:
Now can you make a ball of dimensions 20x20 pixels appear in the middle of your screen?
You should have a window that looks very similar to this:
Exiting with the Red Button
Before we start with the motion characteristics of the paddles, have you noticed that when you try and click the red button to close the Pygame window your cursor turns into the rainbow ball of death? Let’s fix that annoying behavior. Add this ‘for loop’ to your while loop:
# Gameplay while play: # Checks if window exit button pressed for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() # printing to the main screen screen.blit(paddle1, (x1, y1)) screen.blit(paddle2, (x2, y2)) screen.blit(ball, (WIN_W/2, WIN_H/2)) # Limits frames per iteration of while loop clock.tick(60) # Writes to main surface pygame.display.flip()If you add this to your code, you will get an error because we are using a method called ‘exit’ that comes from the ‘sys’ module. However, we haven’t imported the module to our program. You can import the module by adding the following code to the top line of your program:
import sys import pygame
Paddle Movement Variables
We are going to use this system of reading the events on the array to check if the user pressed the up key or down key so we can move the paddle accordingly. First add the following code to your ‘for loop’:
# Checks if window exit button pressed for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() # Keypresses elif event.type == pygame.KEYDOWN: if event.key == pygame.K_UP: moveUP = True moveDOWN = False elif event.key == pygame.K_DOWN: moveUP = False moveDOWN = True elif event.type == pygame.KEYUP: if event.key == pygame.K_UP: moveUP = False elif event.key == pygame.K_DOWN: moveDOWN = FalseLooking at the first ‘elif’, we can see that the conditional is checking if the user pressed down a key on the keyboard. Next, the algorithm is checking if the up key or down key were pressed and sets the ‘moveUP’ and ‘moveDOWN’ variables accordingly. The next ‘elif’ is used to check if the user released a key on the keyboard and set ‘moveUP’ and ‘moveDOWN’ accordingly. Remember that we need to create and initialize ‘moveUP’ and ‘moveDOWN’ earlier in our program. Be sure to do this outside of the loop!
Actually Moving the Paddle
Now try running your program. You will see that even if you press the up or down arrows that your paddle doesn’t move. Why is this? It’s because we still need to adjust the y-value of our paddle based on the values of ‘moveUP’ and moveDOWN’. We need to increment the y–value if the down arrow is pressed and decrement the y-value if the up arrow was pressed.
Add the following code to your while loop after the ‘for loop’. Be sure to place it outside of the ‘for loop’ that checks for the user’s keystrokes!
for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() ... # Adjust speed if moveUP or moveDOWN: if moveUP: y2 -= speed if moveDOWN: y2 += speedAt this point, double check that you are:
1. Defining and initializing x1 and y1 for the positional values of the left paddle and x2 and y2 for the right paddle.
2. Defining a ‘speed’ variable that will contain the speed at which the paddles will move when the user presses up or down. Be sure to declare it where you are declaring the other variables for paddle.
3. Defining and initializing ‘moveUP’ and ‘moveDOWN’ at the beginning of your program. These variables should be initialized to ‘False’ so the game starts with no paddle movement.
*** As a personal tip, what I do when faced with complex code that I don’t understand is I print it out and review it from time to time until I become comfortable enough with it that I just begin to understand what the algorithm is doing. Then, I review it some more until I understand most of it. Then, I look at it some more until understand everything about it and can use the same algorithm in a different context. Sometimes this process takes a few minutes, other times a few days, and sad but true, very often this process takes weeks. I would suggest you do the same if you are looking at an algorithm you need to understand but have no idea how it functions.
Now Try:
Now modify your program so that you are able to move your left paddle in the same way using the ‘w’ and ‘s’ keys.
Trailing Sprites
Back to our program, you will notice that your paddle is now able to move up and down however it leaves a trail, which results in a window that looks like the following:
The reason this is the case is that every surface that we print on top of the main surface is transparent.
When we begin to print the paddle a bit higher on the y-axis, it is placed a bit higher and printed, but the last printed paddle is still there printed on the main surface from the last iteration of the game loop.
Repeat this process over time and you are left with a moving sprite that leaves a trail. How do we stop this from happening? We can fill the entire background with white just before we ‘blit’ the paddles and ball. By filling the entire screen with white every time we enter the while loop, we are effectively wiping the drawing slate clean erasing what was printed earlier and printing the left paddle, right paddle and ball on a freshly painted while surface before burning it to the monitor when invoking the ‘display.flip’ method.
Paddles Moving Off the Screen
At this moment, your program should allow your paddles to move without leaving tracers, but there is still a problem: you are allowed to move the paddles off the screen. That isn’t right. To fix that, we need to specify some conditions that will stop the paddle’s y value from changing if the current y value of the paddle is less than zero or greater than the height of the game window. To stop the paddle from moving off the top of the gaming window, we need to stop the paddle’s y value from decreasing below the value 0. This is done by using a condition just before we blit our paddle at the bottom of our while loop:
# paddle movement if y2 < 0: y2 = 0
Now Try:
Now modify this branch statement so that the paddle doesn’t go off the bottom of the screen. Be sure to use the global variable WIN_H in your condition so that your algorithm works for any sized gaming window.
Ball Movement and Rects
We moved the paddles by changing the (x, y) values before blitting them to the screen. Essentially, we simply erased the last printed paddle and printed the next paddle at a y location which is incremented or decremented based on the user hitting the up or down arrows. We could use this same method for the ball, however, this time we are going to move the ball using rects.
What are rects you may ask? Rects are a Pygame object that are used to store the rectangular coordinates of a surface. Every surface has a rect and a rect has different properties that are useful such as the x coordinate of the left and right side, the y coordinate of the top and bottom and the width and height in pixels. Rects are a useful way to handle positional information regarding surfaces. Changing the x or y property of a rect moves the rect to that location when you blit it to the main screen. This aspect of rects make them very convenient for moving surfaces that have graphics printed on them such as a ball or paddle.
Since our paddles and ball are surface objects, they each have a corresponding rect that contains their location. Add this code just after you create your ball surface:
ball = pygame.Surface((ball_width, ball_height)) ballRect = pygame.Rect(WIN_W/2, WIN_H/2-(ball_height/2), ball_width, ball_height)The first line stores a surface that is 20 pixels by 20 pixels and by default is colored black. We are using this surface to represent our ball. The second line is retrieving the associated rect of the surface containing the ‘ball’. When looking at the arguments of the ‘Rect’ method, we can see that we must give it an (x, y) coordinate, which will be used to move the location of the ball surface to that point.
Using rects, we can reposition surfaces without having to specify an (x, y) coordinate since the rect contains this information. This presents a different way to print a surface to the main screen.
We will use rects to print the ball on the main screen in our game loop, which will effectively allow us to animate the ball.
Blit Takes Rect
One other thing to mention, is that when we blit the ball, Pygame allows us to pass in a rect as the second argument. This makes sense if we remember that the blit method takes in the surface to be blitted as the first argument and an (x, y) coordinate as the second argument. Since the ball’s rect contains the associated surface’s coordinates, it should be able to be passed to the blit method to specify where to print the ball.
To use the rect to move the ball, we need to blit using the ball’s rect. Let’s do that by replacing:
# printing to the main screen screen.blit(paddle1, (x1, y1)) screen.blit(paddle2, (x2, y2)) screen.blit(ball, (WIN_W/2, WIN_H/2))with:
# printing to the main screen screen.blit(paddle1, (x1, y1)) screen.blit(paddle2, (x2, y2)) screen.blit(ball, ballRect)
Moving the Ball
We have already seen how we can move a sprite by changing the (x, y) coordinates, then blitting the surface to the main surface. We could absolutely use that method to move the ball as well as the paddles, but instead we are going to use the rect to move the ball. Let’s start by defining a variable ‘ball_speed’ that contains an array that contains the x-axis speed and the y-axis speed:
ball_width = ball_height = 20 # Add this array ball_speed = [5, 5] ball = pygame.Surface((ball_width , ball_height)) ballRect = pygame.Rect(WIN_W/2, WIN_H/2-(ball_height/2), ball_width, ball_height)Next, we need to move the ball along at that speed along the x & y-axis. We can do that with the following code:
# Game Loop: code that moves the paddle ballRect = ballRect.move(ballSpeed) # Code that blits images to the main screen
Bouncing off the Walls
We have added motion to the ball, however we want the ball to bounce off the walls instead of run off the bottom right of the screen. In order to do this, we need to program the behavior of the ball in our game loop. Before we do, we must understand that every Rect has a ‘left’, ‘top’, ‘width’ and ‘height’ property that can be accessed at any time. We will use these attributes to figure out if the ball has hit the top of the game window. Add the following conditional statement to your code:
#code that moves the paddle # If ball hits the top if ballRect.top > WIN_H – ballRect.height: ball_speed[1] = -ball_speed[1] ballRect = ballRect.move(ball_speed) # code that blit images to the main screen
Now Try:
Now add to our conditional statement so that the ball will bounce off the walls to the left and right side.
Now Try:
Now add to our unary conditional statement and create a conditional statement that will tell the ball to bounce when the ball hits the top of the game window.
Bouncing off the Paddles
Now, our Pong game has two paddles that can move based on keystrokes and a ball that bounces around the walls. We now need to make the ball bounce of the paddles. Let’s start by first considering the right paddle. We need to understand that the ball’s speed in the x direction must change signs when we hit the paddle. However, how can we check if the ball hits the right paddle? We can see that the ball will hit the right paddle if its y-position is between the y-position of the top of the paddle and the bottom of the paddle. We also need to understand that the ball hits the paddle if its x-position is at the x position of the left side of the right paddle. Let’s start by coding the x position portion of the algorithm. Copy this code into your program:
# If ball hits the top or bottom ... ballRect = ballRect.move(ball_speed) # If ball hits the paddles if ballRect.right == x2: ball_speed[0] = -ball_speed[0] # code that blit images to the main screenLooking at the unary conditional statement above, we can see that the condition checks if the right side of the ball is equal to the x position of the right paddle. Try running your program and you’ll see that the ball most likely runs right through the paddle. Why is this so?
The x position of the ball is increasing in increments of 5, so it is unlikely that the ball’s x position will be equal to the x position of the right paddle.
To make this point clear, add a print statement that outputs the x position of the ball and the x position of the paddle. Compare the two list of values as the print statement outputs 60 times a second. Pay special attention to the point where the ball goes through the right paddle. Can you see where the x position of the ball should be exactly the x position of the paddle? You can see that they are not equal because this is an unlikely occurrence. These two values will get close, but very rarely will they equal each other because we are adding 5 to the x position of the ball every iteration of the game loop. If we were adding 1 to the x position of the ball every time through the game loop, then our condition above would be valid.
What can we do to remedy this situation? We can check for a range of numbers using a set of inequalities. Try changing your condition to the following:
# If ball hits the top or bottom ... ballRect = ballRect.move(ball_speed) # If ball hits the paddles if ballRect.right > x2 - 5 and ballRect.right < x2 + 5: ball_speed[0] = -ball_speed[0] # code that blit images to the main screen
However, we can see that our algorithm still isn’t complete because we need to add to our condition so that the ball doesn’t bounce if it is above or below the paddle.
Now Try:
Now add to our conditional statement so that the ball also bounces off the right paddle if it lands on the paddle rather than only bouncing off the x position of the paddle.
Now Try:
Now add to our unary conditional statement and write the code to have the ball bounce off the left paddle.
Now Try:
Finally, take away the code that has the ball bounce off the left and right walls.
rubric
can be found here.