Pages

Pong - Step 2




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.

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.

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</pre>

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.

# create ball
self.ball = Ball((400,200))
self.sprites.add(self.ball)

# score image
self.scoreImage = Score((400, 50))
self.sprites.add(self.scoreImage)</pre>

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.

# handle ball -- all our ball management here
self.manageBall()</pre>

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.

# 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.

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.

    # 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.

    # 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</pre>

Result and Download:

Here is our game with ball, paddles, and score.