Ryans Tutorials
More great tutorials at RyansTutorials

PyGame Pong

A simple version built using functions!

Introduction

Pygame Pong in gameOn the previous page we created a very simple implementation of Pong. Here the intention is to extend upon that and improve both the game and the code by utilising functions. As you will see this will make the code easier to manage and simplify the process of adding functionality. In a future page we will then further enhance the game by making use of object oriented programming.

Getting Set Up

Pygame template windowBefore we begin, let's create a new file (call it pong-functions.py) and copy in the template code from the previous section.

Once you've saved your file with the template code in it, run the file to make sure you've copied it in ok.

Working through the rest of this section you will progressively add code to this file to build up the game.

Before we begin though, let's change the name of the game on line 15 from My Game! to Pong!.

We also want to make the window a little bigger so we have more room to play the game in. Change the WINDOW_WIDTH to 800 and the WINDOW_HEIGHT to 600 on lines 11 and 12 (or any other size that you prefer).

Finally, we need to add two more libraries which will be utilised. Add the two libraries in bold below to line 1 :

  • import pygame, sys, random, time, math

Development Strategy

When building up a game like this it helps if you have the right strategy. What we are after is a methodical approach that allows us to progressively build the game up, testing constantly as we go. That way, if you make a mistake and break things, you will find out earlier and it will be quicker and easier to fix.

We have already started by getting a basic template in place. Next up, we want to think about the general structure of the game and create some blank function placeholders (called stubs). Then we progressively fill these in and build them up. As we do, we will most likely discover that more functions will be useful. We will add them in as we go.

As our code is starting to get a bit more complex now, some organisation will help in keeping track of things. Make sure you label groups of functions as you go which will help in finding code and jumping around you code more efficientyly as the number of lines of code starts to build.

I'm also not going to go into as much detail here on individual lines of code / small chunks of code. By this point you should understand what they are doing and are just learning how to structure it together into more complex programs. If you need more guidance on how the code actually works I suggest you go back over some of the previous content.

The Overall Game Structure

In the simple version of Pong we had the game and that was it. Now we will aim to make it a bit more of a polished product by utilising differenct screens to make it more user friendly. The screens will be :

  • A splash screen - to introduce the game
  • A menu screen - so the user may choose between instructions and actually playing the game
  • An instructions screen
  • The actual game
  • A game over screen
  • A goodbye screen - when the user quits the game

There are a few ways in which we could code this in PyGame but the easiest way is to create a function for each of the screens and each function has its own game loop within it. Our main function manages moving between the screens through a while loop and a series of if statements.

Start by adding a variable within the main function called screen (and a winner which we will use later) :

  •   looping = True
  •  
  •   screen = 'splash'
  •   winner = ''

And then a series of if, elif statements to move between the screens (paste this over "# This section will be built outlater"):

  •     # Processing
  •  
  •     if screen == 'splash' :
  •       screen = splash_screen()
  •     elif screen == 'menu' :
  •       screen = menu_screen()
  •     elif screen == 'game' :
  •       screen = game_screen()
  •     elif screen == 'game_over' :
  •       screen = game_over_screen()
  •     elif screen == 'instructions' :
  •       screen = instructions_screen()
  •     else :
  •       screen = goodbye_screen()

You will notice that each function returns a value which is the next screen to display. Other values may be returned as well (which we will add later on) to pass state data between the screens as well.

We could have called the functions for the screens from the end of the various screen functions and that would have worked too however this method allows us to manage things in a more orderly fashion which will make it easier as more complexity is added. It will also make testing of various screens as we develop them easier to do.

Now we will add in a series of stub functions to match these screens :

  • pygame.display.set_caption('Pong!')
  •  
  • # Screens
  •  
  • # Shows when the game first starts
  • def splash_screen () :
  •  
  •   return 'menu'
  •  
  •  
  • # Shows navigation options to the user
  • def menu_screen () :
  •  
  •   return 'menu'
  •  
  •  
  • # The actual game
  • def game_screen () :
  •  
  •   return 'menu'
  •  
  •  
  • # Shows when a player has won the game
  • def game_over_screen () :
  •  
  •   return 'menu'
  •  
  •  
  • # Display instructions for the game
  • def instructions_screen () :
  •  
  •   return 'menu'
  •  
  •  
  • # Shows just before the user exits the game
  • def goodbye_screen () :
  •  
  •   return 'menu'
  •  
  •  

And also set up some global variables which will be utilised shortly :

  • # Colours
  • BACKGROUND = (255, 255, 255)
  • TEXTCOLOUR = (0, 0, 0)
  • BALLCOLOUR = (0, 0, 0)
  • PADDLELEFTCOLOUR = (0, 0, 0)
  • PADDLERIGHTCOLOUR = (0, 0, 0)
  • BOARDCOLOUR = (100, 100, 100)
  •  
  • # Game Variables
  • PADDLEINSET = 20
  • PADDLEWIDTH = 10
  • PADDLEHEIGHT = 60
  • BALLSIZE = 10
  • GAMEROUNDS = 5
  • BOARDTOPSPACING = 40
  • BOARDBOTTOMSPACING = 20

If you save and run your script you should notice no difference (it will still just display a blank, white window). You should do this anyway and make sure no errors occur.

Setting up Static screens

Some of these pages display content but don't actually have any functionality :

  • Splash screen
  • Instructions screen
  • Goodbye screen

As such, we can develop and test these screens very easily to get the ball rolling and get them out of the way.

First off, we can replace the splash_screen function with the following :

  • # Shows when the game first starts
  • def splash_screen () :
  •   font = pygame.font.Font('freesansbold.ttf', 32)
  •   text = font.render('PONG!', True, TEXTCOLOUR)
  •  
  •   textRect = text.get_rect()
  •  
  •   textRect.center = (WINDOW_WIDTH // 2, WINDOW_HEIGHT // 2)
  •  
  •   WINDOW.fill(BACKGROUND)
  •   WINDOW.blit(text, textRect)
  •   pygame.display.update()
  •   time.sleep(1)
  •  
  •   return 'menu'
  •  

Pygame Pong Splash ScreenAnd then to test it, the screen variable in the main function is already set to 'splash' so we can just run our script to see what it looks like.

The next easiest screen is the goodbye_screen so let's set up that one :

  • # Shows just before the user exits the game
  • def goodbye_screen () :
  •   font = pygame.font.Font('freesansbold.ttf', 32)
  •   text = font.render('PONG!', True, TEXTCOLOUR)
  •   goodbyeText = font.render('See you soon...', True, TEXTCOLOUR)
  •  
  •   textRect = text.get_rect()
  •   goodbyeTextRect = goodbyeText.get_rect()
  •  
  •   textRect.center = (WINDOW_WIDTH // 2, (WINDOW_HEIGHT // 2) - 50)
  •   goodbyeTextRect.center = (WINDOW_WIDTH // 2, (WINDOW_HEIGHT // 2) + 50)
  •  
  •   WINDOW.fill(BACKGROUND)
  •   WINDOW.blit(text, textRect)
  •   WINDOW.blit(goodbyeText, goodbyeTextRect)
  •   pygame.display.update()
  •   time.sleep(2)
  •  
  •   pygame.quit()
  •   sys.exit()
  •  
  •   return 'menu'
  •  

Pygame Pong Goodbye ScreenNow if we change the screen variable at the top of the main function to 'goodbye' and run our Python script we may see what it looks like.

Don't forget to change the variable back to 'splash' when you are done testing.

And finally, let's set up the instructions screen.

  • # Display instructions for the game
  • def instructions_screen () :
  •   looping = True
  •   action = 'game'
  •   font = pygame.font.Font('freesansbold.ttf', 32)
  •   fontTitle = pygame.font.Font('freesansbold.ttf', 64)
  •  
  •   title = fontTitle.render('PONG!', True, TEXTCOLOUR)
  •   menuItem1 = font.render('Instructions', True, TEXTCOLOUR)
  •   menuItem2 = font.render('Content coming soon', True, TEXTCOLOUR)
  •   menuItem3 = font.render('Back - b, Play game - g or SPACE', True, TEXTCOLOUR)
  •  
  •   titleRect = title.get_rect()
  •   menuItem1Rect = menuItem1.get_rect()
  •   menuItem2Rect = menuItem2.get_rect()
  •   menuItem3Rect = menuItem3.get_rect()
  •  
  •   titleRect.center = (WINDOW_WIDTH // 2, 100)
  •   menuItem1Rect.topleft = (200, 200)
  •   menuItem2Rect.topleft = (200, 300)
  •   menuItem3Rect.topleft = (200, 400)
  •  
  •  
  •   while looping :
  •     # Get inputs
  •     for event in pygame.event.get() :
  •       if event.type == QUIT :
  •         pygame.quit()
  •         sys.exit()
  •  
  •     pressed = pygame.key.get_pressed()
  •     if pressed[K_b] :
  •       action = 'menu'
  •       looping = False
  •     elif pressed[K_SPACE] or pressed[K_p] :
  •       action = 'game'
  •       looping = False
  •  
  •     # render the screen
  •     WINDOW.fill(BACKGROUND)
  •     WINDOW.blit(title, titleRect)
  •     WINDOW.blit(menuItem1, menuItem1Rect)
  •     WINDOW.blit(menuItem2, menuItem2Rect)
  •     WINDOW.blit(menuItem3, menuItem3Rect)
  •     pygame.display.update()
  •     fpsClock.tick(FPS)
  •  
  •   return action
  •  

Pygame Pong instructionsThis screen is more of a placeholder. It will be up to you to build it out with more content later.

This screen is a little more involved than the previous two. The loop is required so that the page will stay consistently until the user selects an action which is to either return to the menu screen or start the game.

The menu works in a similar fashion to the instructions screen.

  • def menu_screen () :
  •   looping = True
  •   action = 'game'
  •   font = pygame.font.Font('freesansbold.ttf', 32)
  •   fontTitle = pygame.font.Font('freesansbold.ttf', 64)
  •  
  •   title = fontTitle.render('PONG!', True, TEXTCOLOUR)
  •   menuItem1 = font.render('Instructions - i', True, TEXTCOLOUR)
  •   menuItem2 = font.render('Play Game - p or SPACE', True, TEXTCOLOUR)
  •   menuItem3 = font.render('Quit - q', True, TEXTCOLOUR)
  •  
  •   titleRect = title.get_rect()
  •   menuItem1Rect = menuItem1.get_rect()
  •   menuItem2Rect = menuItem2.get_rect()
  •   menuItem3Rect = menuItem3.get_rect()
  •  
  •   titleRect.center = (WINDOW_WIDTH // 2, 100)
  •   menuItem1Rect.topleft = (200, 200)
  •   menuItem2Rect.topleft = (200, 300)
  •   menuItem3Rect.topleft = (200, 400)
  •  
  •  
  •   while looping :
  •     # Get inputs
  •     for event in pygame.event.get() :
  •       if event.type == QUIT :
  •         pygame.quit()
  •         sys.exit()
  •  
  •     pressed = pygame.key.get_pressed()
  •     if pressed[K_i] :
  •       action = 'instructions'
  •       looping = False
  •     elif pressed[K_SPACE] or pressed[K_p] :
  •       action = 'game'
  •       looping = False
  •     elif pressed[K_q] :
  •       action = 'bye'
  •       looping = False
  •  
  •     # render the screen
  •     WINDOW.fill(BACKGROUND)
  •     WINDOW.blit(title, titleRect)
  •     WINDOW.blit(menuItem1, menuItem1Rect)
  •     WINDOW.blit(menuItem2, menuItem2Rect)
  •     WINDOW.blit(menuItem3, menuItem3Rect)
  •     pygame.display.update()
  •     fpsClock.tick(FPS)
  •   return action
  •  
  •  
  • # The actual game
  • def game_screen () :
  •  

This is a very basic menu. There are many ways in which you could improve this menu by eg. including arrow keys or mouse interaction, to make it more intuitive.

Pygame Pong menu screenDon't forget to change the screen variable back to 'splash' if you haven't done so already.

The Actual Game

For this section the code will be provided with minimal explanation. If you want further explanation on processing for this section consider looking over the Pong Simple tutorial.

Paste the following code below all the functions for screens but before the main function.

  • # Game Processing
  •  
  • def process_paddle (paddleY, paddleMomentum) :
  •   newPaddleY = paddleY
  •   newPaddleMomentum = paddleMomentum
  •   newPaddleY = newPaddleY + (newPaddleMomentum // 5)
  •   if newPaddleMomentum > 0 :
  •     newPaddleMomentum = newPaddleMomentum - 1
  •   if newPaddleMomentum < 0 :
  •     newPaddleMomentum = newPaddleMomentum + 1
  •   if newPaddleY < BOARDTOPSPACING :
  •     newPaddleY = BOARDTOPSPACING + 5
  •   if newPaddleY > WINDOW_HEIGHT - BOARDBOTTOMSPACING - PADDLEHEIGHT :
  •     newPaddleY = WINDOW_HEIGHT - BOARDBOTTOMSPACING - PADDLEHEIGHT - 5
  •  
  •   return newPaddleY, newPaddleMomentum
  •  
  •  
  • def process_ball (ballCoordinates, ballAngle, ballMomentum, leftPaddleY, rightPaddleY) :
  •   goal = False
  •   newBallAngle = ballAngle
  •  
  •   ballCoordinates[0] = ballCoordinates[0] + (ballMomentum * math.sin(math.radians(ballAngle)))
  •   ballCoordinates[1] = ballCoordinates[1] + (ballMomentum * math.cos(math.radians(ballAngle)))
  •  
  •   # has ball hit top or bottom of the court?
  •   if ballCoordinates[1] < BOARDTOPSPACING + (BALLSIZE) :
  •     ballCoordinates[1] = BOARDTOPSPACING + (BALLSIZE) + 1
  •     newBallAngle = (180 - ballAngle) % 360
  •   if ballCoordinates[1] > WINDOW_HEIGHT - BOARDBOTTOMSPACING - (BALLSIZE) :
  •     ballCoordinates[1] = WINDOW_HEIGHT - BOARDBOTTOMSPACING - (BALLSIZE) - 1
  •     newBallAngle = (180 - ballAngle) % 360
  •  
  •   # has the ball hit a paddle?
  •   if ballCoordinates[0] > WINDOW_WIDTH - BALLSIZE - PADDLEINSET - PADDLEWIDTH :
  •     # see if the paddle is in range
  •     if ballCoordinates[1] > rightPaddleY and ballCoordinates[1] < rightPaddleY + PADDLEHEIGHT :
  •       ballCoordinates[0] = WINDOW_WIDTH - BALLSIZE - PADDLEINSET - PADDLEWIDTH - 1
  •       # get distanct from middle of paddle
  •       angleMultiplier = (rightPaddleY - ballCoordinates[1])
  •       newBallAngle = ((360 - newBallAngle) % 360) - angleMultiplier
  •   if ballCoordinates[0] < BALLSIZE + PADDLEINSET + PADDLEWIDTH :
  •     # see if the paddle is in range
  •     if ballCoordinates[1] > leftPaddleY and ballCoordinates[1] < leftPaddleY + PADDLEHEIGHT :
  •       ballCoordinates[0] = BALLSIZE + PADDLEINSET + PADDLEWIDTH + 1
  •       # get distanct from middle of paddle
  •       angleMultiplier = (leftPaddleY - ballCoordinates[1])
  •       newBallAngle = ((360 - newBallAngle) % 360) + angleMultiplier
  •  
  •   # has ball hit the edge of the court?
  •   if ballCoordinates[0] < BALLSIZE :
  •     ballCoordinates[0] = WINDOW_WIDTH // 2
  •     goal = 'right'
  •   if ballCoordinates[0] > WINDOW_WIDTH - BALLSIZE :
  •     ballCoordinates[0] = WINDOW_WIDTH // 2
  •     goal = 'left'
  •  
  •   return ballCoordinates, ballMomentum, newBallAngle, goal
  •  
  •  
  • # Game Rendering
  •  
  • def render_board () :
  •   pygame.draw.line(WINDOW, BOARDCOLOUR, (0, BOARDTOPSPACING), (WINDOW_WIDTH, BOARDTOPSPACING), 3)
  •   pygame.draw.line(WINDOW, BOARDCOLOUR, (0, WINDOW_HEIGHT - BOARDBOTTOMSPACING), (WINDOW_WIDTH, WINDOW_HEIGHT - BOARDBOTTOMSPACING), 3)
  •   pygame.draw.line(WINDOW, BOARDCOLOUR, ((WINDOW_WIDTH //2) - 1, BOARDTOPSPACING), ((WINDOW_WIDTH // 2) - 1, WINDOW_HEIGHT - BOARDBOTTOMSPACING), 2)
  •   return True
  •  
  •  
  • def render_scores (playerLeftScore, playerRightScore) :
  •   render_scores_side (playerLeftScore, "left")
  •   render_scores_side (playerRightScore, "right")
  •  
  •   return True
  •  
  •  
  • def render_scores_side (playerScore, side ) :
  •   yCoord = 20
  •   xCoord = 30
  •   xCoordAdd = 20
  •   if side == "right" :
  •     xCoord = WINDOW_WIDTH - 30
  •     xCoordAdd = -20
  •   radius = 7
  •   counter = 1
  •   while counter <= 3 :
  •     if playerScore < counter :
  •       pygame.draw.circle(WINDOW, BOARDCOLOUR, (xCoord, yCoord), radius, 2)
  •     else :
  •       pygame.draw.circle(WINDOW, BOARDCOLOUR, (xCoord, yCoord), radius)
  •     counter = counter + 1
  •     xCoord = xCoord + xCoordAdd
  •   return True
  •  
  •  
  • def render_paddle (side, y) :
  •   if side == 'left' :
  •     x = PADDLEINSET
  •   else :
  •     x = WINDOW_WIDTH - PADDLEINSET - PADDLEWIDTH
  •  
  •   paddleRect = pygame.Rect(x, y, PADDLEWIDTH, PADDLEHEIGHT)
  •  
  •   pygame.draw.rect(WINDOW, PADDLELEFTCOLOUR, paddleRect)
  •  
  •   return True
  •  
  •  
  • def render_ball (ballCoordinates) :
  •   pygame.draw.circle(WINDOW, BALLCOLOUR, (int(ballCoordinates[0]), int(ballCoordinates[1])), BALLSIZE)
  •   return True
  •  
  •  
  •  
  • # Screens

And then the code for the game_screen and game_over_screen functions :

  • # Screens
  •  
  • def game_screen () :
  •   winner = ''
  •   looping = True
  •   action = 'game over'
  •   leftPaddleMomentum = 0
  •   rightPaddleMomentum = 0
  •   leftPaddleY = 200
  •   rightPaddleY = 400
  •   ballCoordinates = [WINDOW_WIDTH // 2, WINDOW_HEIGHT // 2]
  •   ballMomentum = 1.05
  •   ballAngle = 150
  •   playerLeftScore = 0
  •   playerRightScore = 0
  •   goal = False
  •  
  •   while looping :
  •     # Get inputs
  •     for event in pygame.event.get() :
  •       if event.type == QUIT :
  •         pygame.quit()
  •         sys.exit()
  •  
  •     pressed = pygame.key.get_pressed()
  •     if pressed[K_w] :
  •       leftPaddleMomentum -= 2
  •     elif pressed[K_s] :
  •       leftPaddleMomentum += 2
  •     if pressed[K_UP] :
  •       rightPaddleMomentum -= 2
  •     elif pressed[K_DOWN] :
  •       rightPaddleMomentum += 2
  •     if pressed[K_SPACE] :
  •       start = 'go'
  •     if pressed[K_q] :
  •       action = 'menu'
  •       looping = False
  •  
  •     # Processing
  •  
  •     leftPaddleY, leftPaddleMomentum = process_paddle(leftPaddleY, leftPaddleMomentum)
  •     rightPaddleY, rightPaddleMomentum = process_paddle(rightPaddleY, rightPaddleMomentum)
  •  
  •     ballCoordinates, ballMomentum, ballAngle, goal = process_ball(ballCoordinates, ballAngle, ballMomentum, leftPaddleY, rightPaddleY)
  •     if goal == 'left' :
  •       playerLeftScore = playerLeftScore + 1
  •       ballAngle = 150
  •       if playerLeftScore >= 3 :
  •         winner = 'left'
  •         looping = False
  •     elif goal == 'right' :
  •       playerRightScore = playerRightScore + 1
  •       ballAngle = 330
  •       if playerRightScore >= 3 :
  •         winner = 'right'
  •         looping = False
  •  
  •     # render the screen
  •     WINDOW.fill(BACKGROUND)
  •     render_board()
  •     render_scores(playerLeftScore, playerRightScore)
  •     render_ball(ballCoordinates)
  •     render_paddle('left', leftPaddleY)
  •     render_paddle('right', rightPaddleY)
  •     pygame.display.update()
  •     fpsClock.tick(FPS)
  •  
  •   time.sleep(0.5)
  •   return action, winner
  •  
  •  
  • def game_over_screen (winner) :
  •   looping = True
  •   action = 'game'
  •   font = pygame.font.Font('freesansbold.ttf', 32)
  •   fontTitle = pygame.font.Font('freesansbold.ttf', 64)
  •  
  •   title = fontTitle.render('PONG!', True, TEXTCOLOUR)
  •   menuItem1 = font.render('Game over', True, TEXTCOLOUR)
  •   menuItem2 = font.render(f'Player {winner} wins!!', True, TEXTCOLOUR)
  •   menuItem3 = font.render('Play again - p or SPACE, Menu - b', True, TEXTCOLOUR)
  •  
  •   titleRect = title.get_rect()
  •   menuItem1Rect = menuItem1.get_rect()
  •   menuItem2Rect = menuItem2.get_rect()
  •   menuItem3Rect = menuItem3.get_rect()
  •  
  •   titleRect.center = (WINDOW_WIDTH // 2, 100)
  •   menuItem1Rect.topleft = (200, 200)
  •   menuItem2Rect.topleft = (200, 300)
  •   menuItem3Rect.topleft = (200, 400)
  •  
  •   while looping :
  •     # Get inputs
  •     for event in pygame.event.get() :
  •       if event.type == QUIT :
  •         pygame.quit()
  •         sys.exit()
  •  
  •     pressed = pygame.key.get_pressed()
  •     if pressed[K_b] :
  •       action = 'menu'
  •       looping = False
  •     elif pressed[K_SPACE] or pressed[K_p] :
  •       action = 'game'
  •       looping = False
  •     elif pressed[K_q] :
  •       action = 'bye'
  •       looping = False
  •  
  •     # render the screen
  •     WINDOW.fill(BACKGROUND)
  •     WINDOW.blit(title, titleRect)
  •     WINDOW.blit(menuItem1, menuItem1Rect)
  •     WINDOW.blit(menuItem2, menuItem2Rect)
  •     WINDOW.blit(menuItem3, menuItem3Rect)
  •     pygame.display.update()
  •     fpsClock.tick(FPS)
  •   return action
  •  
  •  

And now just one final change to the main function so that the winner gets passed to the game over screen :

  •     # Processing
  •  
  •     if screen == 'splash' :
  •       screen = splash_screen()
  •     elif screen == 'menu' :
  •       screen = menu_screen()
  •     elif screen == 'game' :
  •       screen, winner = game_screen()
  •     elif screen == 'game_over' :
  •       screen = game_over_screen(winner)
  •     elif screen == 'instructions' :
  •       screen = instructions_screen()
  •     else :
  •       screen = goodbye_screen()

Some of these lines are quite long and go over two lines on the screen.

This presents a simple breakdown into functions that should be easy to follow. There are more effective / efficient means to organise the code and processing but the intent here is to teach the basics.

Pygame Pong in gameYou should now have a working game of Pong utilising functions and multiple screens. This is by no means a great implementation but the intent is to demosntrate a pattern for creating a multi screen game in PyGame utilising functions. The game itself could be greatly improved. There are also other, more complex patterns for managing multiple screens in PyGame but it's best to learn to crawl before you learn to walk before you learn to run. This could also be further enhanced with the use of OOP (Object Oriented Programming) but that will come in a future section.

Here is a copy of the completed code (in case you get lost trying to piece all the code together). You should have a go at following along and building it up first however as you will then have a better understanding of what is going on.

Activities

You now have a basic version of Pong, ripe for tinkering with. Let's see how you go improving the game.

Have a go at the following :

1. Can you adjust the colours and add more detail to the court?

2. Can you increase the speed and angle of the ball to make it more challenging?

3. Can you add a scoring mechanism?

4. Can you make is to that if you press 'p' on the keyboard it will pause the game?

5. Can you add sound effects when the paddles hit the ball or the ball bounces off the edge of the window?