Pages

Pong - Step 1




Goal:

In this tutorial we will subclass the pygame sprite class to make our paddles and ball, then control them using the keyboard. At the end we will have two paddles that move and a ball that does absolutely nothing.

The Setup:

First things first. We are going to start with the same code we used in Tutorial 2, then modify it to work for us. You may want to copy it over, but be aware of the changes, particularly the additional imports, the background, the new sprite code, and the new rendering code. As usual we begin with the import code, but this time we want a couple extra libraries. We won’t need all these for step 1, but we are going to import them now so we don’t have to come back to this code block later. We will use the os module for our path manipulation when we load our images, then math and random for picking a random serving angle for the ball.

try:
    import sys, os, math, random
    import pygame
    from pygame.locals import *

except ImportError, err:
    print "%s Failed to Load Module: %s" % (__file__, err)
    sys.exit(1)

The Sprite Classes - Paddle

This is our first interesting piece of code in these tutorials. In pygame, pretty much everything you see moving around the screen and interacting is a sprite or a subclass of a sprite. This base class provides all the useful behind-the-scenes functionality for rendering, joining and leaving render groups, etc. You can look at the pygame.sprite module docs for more info. From our point of view, a sprite has two important attributes: image and rect. Image is a pygame.Surface object, which is the base class for everything that can get rendered in pygame. Rect is a pygame.Rect, which represents a rectangle (who knew?) and has a handful of useful functions associated with it as well. Basically when pygame goes to render your sprite, it will take the surface in yoursprite.image and render it in the position and size defined by yoursprite.rect. The first sprite class we are going to make will be our paddle. The paddles are pretty simple – little rectangles that can only move along the Y axis. There are lots of ways we could do this, but we are going to use velocity for the paddles and move them based on that. This means, instead of a keypress moving the paddles directly, it will change the velocity of the paddle. Every frame we will move the paddle based on its velocity, even if it is zero. This gives us two benefits - we can test the velocity of the paddle at any time, which will be very useful later when adding spin to the ball, and we can have the ‘expected’ behavior of the paddle not moving when both the up and down keys are pressed, instead of just giving preference to one of them. Be aware though, the way we are doing this depends on the frame rate, so we will be limiting that later on in our run method. (In a more complicated program you can amend your movement code to scale with the time between frames, so users will varying frame rates will always have a consistent experience.)The other thing we will do is add a move function to the paddles that will automatically keep them inside the screen. This will make moving the paddles extremely easy, and easy is good. You’ll need to download the tutorial zip to get the images or just find/create your own images for the paddles and later on the ball. Place them in a folder below the script named images.

class Paddle(pygame.sprite.Sprite):
    """A paddle sprite. Subclasses the pygame sprite class.
    Handles its own position so it will not go off the screen."""

    def __init__(self, xy):
        # initialize the pygame sprite part
        pygame.sprite.Sprite.__init__(self)
        # set image and rect
        self.image = pygame.image.load(os.path.join('images','pong_paddle.gif'))
        self.rect = self.image.get_rect()

        # set position
        self.rect.centerx, self.rect.centery = xy

        # the movement speed of our paddle
        self.movementspeed = 5

        # the current velocity of the paddle -- can only move in Y direction
        self.velocity = 0

    def up(self):
        """Increases the vertical velocity"""
        self.velocity -= self.movementspeed

    def down(self):
        """Decreases the vertical velocity"""
        self.velocity += self.movementspeed

    def move(self, dy):
        """Move the paddle in the y direction. Don't go out the top or bottom"""
        if self.rect.bottom + dy > 400:
            self.rect.bottom = 400
        elif self.rect.top + dy < 0:
            self.rect.top = 0
        else:
            self.rect.y += dy

    def update(self):
        """Called to update the sprite. Do this every frame. Handles
        moving the sprite by its velocity"""
        self.move(self.velocity)

The Game Class

This is our Game class from tutorial 2 with some extras added in for this game. Notably, I changed the window size to 800 x 400, added the KEYUP event, and all the sprite stuff starting at line 92. The first piece of new code is for the background. I’m creating a pygame surface filling it with white, drawing a black line down the middle, and blitting (drawing) it onto the main window. You may want to have a look at the surface or draw documentation if you don’t understand what is happening. Next, I created a RenderUpdates sprite group. This is a pygame list you add your sprites to that is needed for the kind of ‘dirty update’ rendering we will use in the main loop. Lastly, we create two Paddles, self.leftpaddle and self.rightpaddle, give them their initial coordinates on either side of the screen, and add them to the render group.

class Game(object):
    """Our game object! This is a fairly simple object that handles the
    initialization of pygame and sets up our game to run."""

    def __init__(self):
        """Called when the the Game object is initialized. Initializes
        pygame and sets up our pygame window and other pygame tools
        that we will need for more complicated tutorials."""

        # load and set up pygame
        pygame.init()

        # create our window
        self.window = pygame.display.set_mode((800, 400))

        # clock for ticking
        self.clock = pygame.time.Clock()

        # set the window title
        pygame.display.set_caption("Pygame Tutorial 3 - Pong")

        # tell pygame to only pay attention to certain events
        # we want to know if the user hits the X on the window, and we
        # want keys so we can close the window with the esc key
        pygame.event.set_allowed([QUIT, KEYDOWN, KEYUP])

        # make background -- all white, with black line down the middle
        self.background = pygame.Surface((800,400))
        self.background.fill((255,255,255))
        # draw the line vertically down the center
        pygame.draw.line(self.background, (0,0,0), (400,0), (400,400), 2)
        self.window.blit(self.background, (0,0))
        # flip the display so the background is on there
        pygame.display.flip()

        # a sprite rendering group for our ball and paddles
        self.sprites = pygame.sprite.RenderUpdates()

        # create our paddles and add to sprite group
        self.leftpaddle = Paddle((50,200))
        self.sprites.add(self.leftpaddle)
        self.rightpaddle = Paddle((750,200))
        self.sprites.add(self.rightpaddle)

The Event Loop - Run()

This is the same as the run method in tutorial 2, but with our new rendering code put in. First we will run each sprite’s update method, then use the rendergroup to draw them. Notice in lines 134 the sprite group’s draw method returns a list of areas that have changed, which we then pass on to the update method in line 137. This means only those areas will be redrawn, which is far more efficient than re-rendering the entire window if you have a lot going on. It will have almost no effect for us, but it is good practice to start with because when you do a real project you’ll be much better off using it. As mentioned above in the movement discussion, we need a consistent frame rate for the paddles to move correctly, so we are going to cap our frame rate using the pygame clock. The ‘right’ way to do it is to pass the time since the last frame to the update methods and scale the movement from there, but since our game is so simple almost any computer can run this over 60 fps, so capping it there should work for us and keep the tutorial more simple.

def run(self):
    """Runs the game. Contains the game loop that computes and renders
    each frame."""

    print 'Starting Event Loop'

    running = True
    # run until something tells us to stop
    while running:

        # tick pygame clock
        # you can limit the fps by passing the desired frames per seccond to tick()
        self.clock.tick(60)

        # handle pygame events -- if user closes game, stop running
        running = self.handleEvents()

        # update the title bar with our frames per second
        pygame.display.set_caption('Pygame Tutorial 3 - Pong   %d fps' % self.clock.get_fps())

        # update our sprites
        for sprite in self.sprites:
            sprite.update()

        # render our sprites
        self.sprites.clear(self.window, self.background)    # clears the window where the sprites currently are, using the background
        dirty = self.sprites.draw(self.window)              # calculates the 'dirty' rectangles that need to be redrawn

        # blit the dirty areas of the screen
        pygame.display.update(dirty)                        # updates just the 'dirty' areas

    print 'Quitting. Thanks for playing'

HandleEvents - Processing User Input

Just like the others, this starts with the tutorial 2 handleEvents method, then we add some additional code. At the top we have the quit code if the user hits the X or presses the escape key. After that, however, is the interesting stuff. We are listening for both keydown and keyup events, and we need to handle the accordingly. We want the paddle to move as long as the user is holding the button, and if they press both the up and down keys at the same time, the paddle to not move at all. In the keydown block, we call the paddles’ up methods for W and the up arrow, and the down methods for S and the down arrow. On keyup, we simple do the opposite to undo the change we made to the velocity. Have a look at the code below, then glance back up at the Paddle class to see how it fits together. If you download the source you can also see a commented out piece of code that does the same thing in a different way (using buffered input instead).

def handleEvents(self):
    """Poll for PyGame events and behave accordingly. Return false to stop
    the event loop and end the game."""

    # poll for pygame events
    for event in pygame.event.get():
        if event.type == QUIT:
            return False

        # handle user input
        elif event.type == KEYDOWN:
            # if the user presses escape, quit the event loop.
            if event.key == K_ESCAPE:
                return False

            # paddle control
            if event.key == K_w:
                self.leftpaddle.up()
            if event.key == K_s:
                self.leftpaddle.down()

            if event.key == K_UP:
                self.rightpaddle.up()
            if event.key == K_DOWN:
                self.rightpaddle.down()

        elif event.type == KEYUP:
            # paddle control
            if event.key == K_w:
                self.leftpaddle.down()
            if event.key == K_s:
                self.leftpaddle.up()

            if event.key == K_UP:
                self.rightpaddle.down()
            if event.key == K_DOWN:
                self.rightpaddle.up()

    return True

Running the Script

The last thing is the standard tutorial ending that creates the game object and calls run when you actually run the script.

# create a game and run it
if __name__ == '__main__':
    game = Game()
    game.run()