Pygame – 3. Pong Step 2

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.
Pong: Step 2

The ball image: pong_ball

Tutorial Download:
Pygame Tutorial 3 - Pong (297)