Pygame Tutorial 3: Pong Step 2
Written by Collin Green — Version 1.0.1 — 2010-12-26
Goal
In this tutorial we are going to take what we created in step 1 and add the ball, paddle/wall collisions, and scoring. At the end you’ll have a rough but playable version of pong.
The Ball Class
Like the Paddle class, we are going to create a class to represent the ball. The movement code is a lot more complex for the ball, so we are going to handle that in the game class, but this object is still going to keep track of its velocity as well as include some helpful functions like reset and serve that we can call when needed. Reset is pretty obvious – it puts the ball in the middle of the table and resuts its velocity to zero. Serve is interesting – we want the ball to start in a random direction that is still mostly toward one of the paddles so we have a little bit of random code to get a random angle then set the ball’s velocity along it.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | class Ball(pygame.sprite.Sprite): """A ball sprite. Subclasses the pygame sprite class.""" def __init__(self, xy): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images','pong_ball.gif')) self.rect = self.image.get_rect() self.rect.centerx, self.rect.centery = xy self.maxspeed = 10 self.servespeed = 5 self.velx = 0 self.vely = 0 def reset(self): """Put the ball back in the middle and stop it from moving""" self.rect.centerx, self.rect.centery = 400, 200 self.velx = 0 self.vely = 0 def serve(self): angle = random.randint(-45, 45) # if close to zero, adjust again if abs(angle) < 5 or abs(angle-180) < 5: angle = random.randint(10,20) # pick a side with a random call if random.random() > .5: angle += 180 # do the trig to get the x and y components x = math.cos(math.radians(angle)) y = math.sin(math.radians(angle)) self.velx = self.servespeed * x self.vely = self.servespeed * y |
The Score Class
First, let me start by saying there are lots of ways to handle text. I personally don’t like dealing with it, so I usually wrap it up in a sprite and just stick it in my sprite rendering group and call it done, which is exactly what we are going to do here. This is a simple sprite subclass that keeps track of our game score and uses the pygame font class to render some text. I’m adding the left() and right() functions so we can easily update the score from the game class. These will also call re-render, which will recreate the score image and re-center it at the specified coordinates so it always looks right.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | class Score(pygame.sprite.Sprite): """A sprite for the score.""" def __init__(self, xy): pygame.sprite.Sprite.__init__(self) self.xy = xy # save xy -- will center our rect on it when we change the score self.font = pygame.font.Font(None, 50) # load the default font, size 50 self.leftscore = 0 self.rightscore = 0 self.reRender() def update(self): pass def left(self): """Adds a point to the left side score.""" self.leftscore += 1 self.reRender() def right(self): """Adds a point to the right side score.""" self.rightscore += 1 self.reRender() def reset(self): """Resets the scores to zero.""" self.leftscore = 0 self.rightscore = 0 self.reRender() def reRender(self): """Updates the score. Renders a new image and re-centers at the initial coordinates.""" self.image = self.font.render("%d %d"%(self.leftscore, self.rightscore), True, (0,0,0)) self.rect = self.image.get_rect() self.rect.center = self.xy |
Changes to Game.__init__
Now that we have the Ball and Score classes, we need to put them into our game. Inside the Game.__init__ function, add these lines to create the ball, create the Score object, and put them in the sprite group for rendering.
187 188 189 190 191 192 193 | # create ball self.ball = Ball((400,200)) self.sprites.add(self.ball) # score image self.scoreImage = Score((400, 50)) self.sprites.add(self.scoreImage) |
Changes to Game.run
We have a lot to process for the ball movement, but instead of cluttering up the main loop, I broke it off into its own function so all we need to do is call that function in the main loop.
219 220 | # handle ball -- all our ball management here self.manageBall() |
Changes to Game.handleEvents
The only thing we want to add in the user input is to serve the ball with the spacebar when it isn’t moving (ie, has just been reset). We already coded the server method into the ball class so all we have to do is call self.ball.serve(). Add this inside the keydown block.
247 248 249 250 | # serve with space if the ball isn't moving if event.key == K_SPACE: if self.ball.velx == 0 and self.ball.vely == 0: self.ball.serve() |
The meat of the game — manageBall
New we need to write the manageBall function we added to our event loop. In this we need to do four things: move the ball, bounce it off the walls, bounce it off the paddles, and reset it when a point is made. Moving the ball is easy enough, it has a velocity – all we have to do is change its position by this. Unfortunately, this will send it off the top/bottom of the screen, so we want to handle that too.
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 | def manageBall(self): """This basically runs the game. Moves the ball and handles wall and paddle collisions.""" # move the ball according to its velocity self.ball.rect.x += self.ball.velx self.ball.rect.y += self.ball.vely # check if ball is off the top if self.ball.rect.top < 0: self.ball.rect.top = 1 # reverse Y velocity so it 'bounces' self.ball.vely *= -1 # check if ball is off the bottom elif self.ball.rect.bottom > 400: self.ball.rect.bottom = 399 # reverse Y velocity so it 'bounces' self.ball.vely *= -1 |
Next we are going to test if the ball hits either side, thereby scoring a point for the opposite team. If it does, we want to add the point to our score object and reset the ball in the middle of the table.
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | # check if the ball hits the left side -- point for right! if self.ball.rect.left < 0: # keep score self.scoreImage.right() # reset ball self.ball.reset() return # check if the ball hits the right side -- point for left! elif self.ball.rect.right > 800: #keep score self.scoreImage.left() # reset ball self.ball.reset() return |
Lastly, we need to bounce the ball off the paddles. We are going to use pygames collision functions to test if the ball is hitting either of our paddles. If it is, we are going to move it outside of the paddle (so it doesn’t collide again the next frame and give us a wonky ball-inside-paddle bug) and reverse its X velocity, ie bouncing it back the other way.
343 344 345 346 347 348 349 350 351 352 353 354 | # check for collisions with the paddles using pygames collision functions collided = pygame.sprite.spritecollide(self.ball, [self.leftpaddle, self.rightpaddle], dokill=False) # if the ball hit a paddle, it will be in the collided list if len(collided) > 0: hitpaddle = collided[0] # reverse the x velocity on the ball self.ball.velx *= -1 # need to make sure the ball is no longer in the paddle -- going to move it again manually self.ball.rect.x += self.ball.velx |
Result and Download:
Here is our game with ball, paddles, and score.

The ball image: ![]()
Tutorial Download:
Pygame Tutorial 3 - Pong (297)