4-13 Building a Complete Game: pyPong v1.0

At this point, you’ve learned the key elements needed for basic game development.  Let’s put everything we’ve learned together to make a complete game!

In 1972, Atari launched one of the most famous, and simple, video games ever created: “Pong”.  Pong was the first commercially successful video game and is credited with starting the whole video game industry.

In today’s lesson, we’ll create a simple 2-player prototype for a pyPong game.  Player 1 will be controlled using a joystick, and Player 2 will use the up and down arrow keys on the keyboard.  The goal for our prototype will be to implement the basic logic for our game.  After we have the basic game complete, we’ll focus on adding jazzy sound effects and graphics to enhance the user experience.

Planning Our Design

It’s tempting to jump into coding right away, but experienced developers know that it’s much smarter to spend a little time planning out our design first.  The time we spend on design will pay for itself many times over when we actually sit down to write code!

Let’s think about what Sprite classes we’ll need.

    • Ball: To start with, we’ll use a simple red circle to represent our ball.  Our Ball sprite will need instance variables to keep track of its current trajectory: dx and dy.  Apart from an __init__() method we’ll need a method to make the ball change its dx direction whenever it hits a player’s paddle or one of the end zones.  We’ll also need a standard update() method to reposition the ball for each frame.
    • Player: We’ll have two players, one on each side of the screen, but they can both be instantiated from the same class.  For our prototype, we’ll simply use black rectangles to represent the players. The __init__() method will need a parameter to indicate which player is to be created so we know which side of the screen to draw it on.  Like the Ball sprite, each Playersprite will need a method to change its dy direction that we can call in response to user events from the joystick or keyboard.  We won’t need to keep track of dx for Player sprites since they’ll only move up and down.  Again, we’ll need an update() method to reposition Player sprites for each frame.
    • EndZone: A player will score a point if they get the ball past the opposite player.  We’ll need two end zones, but again they can be instantiated from the same class.  Isn’t code reuse wonderful?  This sprite is very simple since it won’t move.  We’ll only need an __init__() method that takes a parameter to specify the x_position of the end zone.  To represent the end zone we’ll use a 1-pixel wide black line.  The user doesn’t have to see this sprite, we only need it to detect if a point has been scored.
    • ScoreKeeper: Like the original Pong game we’ll need to track and display the score for both players.  This sprite will include an __init__() method to load the “Arial” system font and initialize the players’ scores to 0.  We’ll need one method to add a point for Player 1 and another method to add a point for Player 2.  We’ll assume that the game is won when the first player gets to 3 points.  To check this we’ll write a method that returns 1 if Player 1 has won, 2 if Player 2 has won, and 0 if there is no winner yet.  Finally, we’ll need an update() method to display the current score at the top of the window.

You’ll notice that I’m not implementing any fancy graphics or audio at this point.  It’s best to start small and create a “proof of concept” to demonstrate the basic game logic.  Once we have the skeleton of our game working the way we want, we’ll add cosmetic enhancements.

Now that we have a pretty clear picture of our Sprite designs, we can start coding them!  As usual for good design, I have stored all of my game sprite classes in a separate module (pySprites.py), so that my main gaming logic (pyPong.py) is as uncluttered as possible.

The Ball Sprite Class

Here is the code for the Ball class (stored in pySprites.py):

The __init__() method creates a red circle image, centres it in the middle of the game window, and sets the initial direction of the Ball such that it’s travelling to the right (dx) 5 pixels, and down (dy) 2 pixels on each frame.  The changeDirection() method reverses the sign of dx, effectively changing the direction of travel.  This method will be called whenever the ball collides with a player or an end zone.

The update() method will be called on each iteration of our main game loop.  Like we’ve seen before, the code here checks to see if we have reached one of the boundaries of the window and if so causes our Ball sprite to change direction.  If not, it continues on its current trajectory.

The Player Sprite Class

The Player class has similar methods to the Ball class and is also stored in our pySprites.py module.

We will represent each player with a 10×100 pixel black rectangle.  If you look at the __init__() method above you’ll see that the playerNum parameter allows us to use the same class to create two unique Player sprites.  If playerNum is 1, then the rectangle for the sprite being instantiated is positioned 10 pixels from the left of the screen.  Otherwise, then the rectangle for the sprite is positioned 10 pixels from the right of the screen, for player 2.  Both Player sprites start off-centred vertically.

The changeDirection() method takes an (x,y) tuple as a parameter – I’ve chosen this approach since the JOYHATMOTION event returns an (x,y) tuple value which I can then simply pass into this method.  Since Player sprites will only move up and down, I ignore the x component of the tuple, and only store the y.  The update() method checks if the Player sprite has reached the top or bottom of the window.  If not, the Player is repositioned 5 pixels up or down (depending on the current dy direction).

The EndZone Sprite Class

The EndZone class is the simplest one:

Like the Player class, the __init__() method takes a parameter to specify the xPosition of the EndZone sprite being instantiated.  This way one class can be used to create two end zones at either side of the game window.  There are no other methods in the EndZone class since they will not be changing or moving during the game.  The reason that we need EndZone sprites is to check for a collision between the Ball sprite and the left or right edge of the screen.  When this happens, one of the players has scored a point.

The ScoreKeeper Sprite Class

The purpose of this sprite is to display the current score for both players at the top of the window:

The __init__() method prepares the “Arial” system font and sets the starting score for each player to 0.  There are two methods, player1Scored() and player2Scored(), that can be called from our main game loop when a player scores a point.  The main game loop needs a way to tell when the game has ended (i.e., one of the players has scored 3 points).  The winner() method can be called to provide this information.  It returns 0 if there is no winner yet, 1 if player 1 has won, or 2 if player 2 has won.   Finally, the update() method prepares and displays the current score centred at the top of the window during each frame of the game.

The Main Game Loop

Now let’s turn our attention to the main game loop code (stored in pyPong.py).

We’ll start with the IDEA sections:

There is nothing new in the Import and Initialize or Display (IDEA) sections.  In the Entities (IDEA) section, after colouring the background, I have cut-and-pasted code from our joystick (U4-12) lesson.  I also instantiate a ScoreKeeper sprite, a Ball sprite, 2 Player sprites, and 2 EndZone sprites.  All of these sprites are then put into an OrderedUpdates sprite group called allSprites so that I can update and refresh the screen with minimal code.

Notice when creating the Player sprites I pass a 1 or 2 to specify whether I want to instantiate the player on the left (player 1) or right (player 2).  I use a similar trick when creating left and right EndZone sprites. The left EndZone is instantiated with x position 0, and the right EndZone is instantiated with x position 639.

Here is the ALTER code:

The most interesting part of this code happens in the Event Handling (ALTER) section.  Player 1 is controlled by the D-pad on the joystick, so when a JOYHATMOTION event occurs we simply pass the (x,y) tuple in event.value to the changeDirection() method for the player1 sprite.  Player 2 is controlled by the up and down arrow keys on the keyboard, so we catch these two events separately.  If player 2 presses the up arrow (K_UP), we pass the (x,y) tuple (0,1) to the changeDirection() method for the player2 sprite. Or, if the user presses the down arrow (K_DOWN), we pass the (x,y) tuple (0,-1) instead.  By similarly handling joystick and keyboard events, we can use the same class for both Player sprites.

After input event handling, we check for possible collisions between the ball sprite and the end zones and players.  Since there are only a few sprites in this game, I am not using sprite groups for collision detection, but you could if you wanted to.  If the ball has collided with one of the end zones, we call the player1_scored() or player2_scored() methods respectively and then call the Ballsprite method changeDirection().

Next, we check if there is a winner (i.e., if one of the players has reached 3 points).  If so, the game loop is set to terminate.  Finally, we check if the Ball sprite has collided with either of the Player sprites.  If so, we call the Ball sprite methodchangeDirection().

In the Refresh (ALTER) section, we use the allSprites sprite group to update and redraw all of the sprites in the game window.  Even though we have 6 sprites, this only takes 4 lines of code.

With all of this code, our pyPong v1.0 game looks something like this:

Not too shabby eh?  Our version of Pong is pretty close to Atari’s version, it’s just too bad that we’ve created this 40 years too late or we’d all be millionaires!

At this point, we have a pretty functional game, but I think you’ll agree that it’s not the most engaging one you’ve ever played.  Next time we’ll look at enhancing our pyPong game with groovy sound, slick graphics, and increasing the difficulty level.

You Try!

There are many ways to enhance our basic pyPong game.  Try and implement all of the following features using the code from today as a starting point:

      1. Change the Ball sprite from a red circle to an image file of a ball.
      2. Change the Player sprite from a black rectangle to an appropriate image.
      3. Use a custom font for the ScoreKeeper sprite.
      4. Add a background image.
      5. Every time a Ball bounces off a Player, increase the Ball speed by 1.
      6. Play background music while the game is running.
      7. Play a sound effect when the Ball bounces off a Player.
      8. Play a sound effect when a Player scores a point.
      9. Change control of Player 2 to a second joystick or the mouse.
      10. When the game ends, display a “Game Over” message in the window.