Python:Pygame Pong With Framework

From GPWiki
Jump to: navigation, search
40x-binary.png This tutorial was written for an older version of Python and some features may be depreciated.
For more information visit What's New In Python 3.0.

Pong

In the last part, we created a framework for games. But what's the point of having a framework without showing how to use it? In true game-programming-tutorial fashion, I shall now create Pong!

Another Word About Style

Like my framework example, this will use more than one file, and they'll each be pretty big. So as to avoid a complete code dump right in the middle of the article, I'll be partitioning it up as follows:

example.py

if __name__ == '__main__':
  print "Hello, world!"

And later, when we add code to that:

example.py

def hiWorld():
  print "Hello, world!"

Sometimes, a method or class will be too freaking huge for me to put in one place. Python's pretty sensitive to indentation, so I'll try not to do this, but at times it makes for far more clear code. In theory, you should be able to just stitch the code together.

example.py (reallyLongFunction, part 1)

def reallyLongFunction():
  print "This is a really really",

example.py (reallyLongFunction, part 2)

  print "really really REALLY long function"

Finally, the code dump:

example.py (entire)

def reallyLongFunction():
  print "This is a really really",
  print "really really REALLY long function"
 
def hiWorld():
  print "Hello, world!"
 
if __name__=='__main__':
  hiWorld()

The Title Screen

What's a game without a title screen? Plus, it's a simple way to illustrate the state mechanism.

Setup

Before we go about making our own state, we'll have to do a few things first - namely, we'll be setting up pygame and our screens. Note that, in order for this code to work, you'll have to have a pygame with TTF support compiled in.

pong.py

#!/usr/bin/env python
 
import states
import pygame
from pygame.constants import *
 
def main():
    pygame.init()
    pygame.font.init()
 
    screen = pygame.display.set_mode( (640,480), DOUBLEBUF)
 
    driver = states.StateDriver(screen)
    title = TitleScreen(driver,screen)
    driver.start(title)
    driver.run()

Four lines in that code deal with our framework: the driver constructor, the title screen's constructor, the driver.start() call which tells our statedriver what state it'll begin with, and finally the driver.run() call which kicks everything off.

TitleScreen

The title screen isn't anything fancy, it's mainly just drawing some text to the screen:

pong.py (TitleScreen, part 1)

class TitleScreen(states.State):
 
    def __init__(self,driver,screen):
        states.State.__init__(self,driver,screen)
        self.pongFont = pygame.font.Font(None,92)
        self.font = pygame.font.Font(None, 16)
 
    def paint(self,screen):
        white = (255, 255, 255)
 
        w,h = screen.get_size()
        surface = self.pongFont.render("PyPong!",0, white)
 
        centerX = w/2 - surface.get_width()/2
        centerY = h*0.25 - surface.get_height()/2
 
        screen.blit(surface, (centerX,centerY))
 
        surface = self.font.render("A tutorial",0,(128,128,128))
        centerX = w/2 - surface.get_width()/2
        centerY = h/2 - surface.get_height()/2
 
        screen.blit(surface, (centerX, centerY))
 
        surface = self.font.render("Press any key to begin", 0, white)
        centerX = w/2 - surface.get_width()/2
        centerY = h*0.75 - surface.get_height()/2
 
        screen.blit(surface, (centerX, centerY))

"Now hold on!" I hear you saying, "The last thing the previous tutorial had us do was create a GuiState! And here we're not using it!"

That's true. The reason here is that the title screen isn't really very dynamic at all - all that's needed is to write a few things up on it. Making the text separate Paintables and adding them to the scene, as well as registering keyables to listen for the keypress, would be a bit of overkill.

Running

With just a little more code, we can see our title screen in action:

pong.py

if __name__ == '__main__':
    main()

Make sure that's at the bottom of the pong.py file. Now, execute the code! If all's gone well, you should see your beautifully rendered title screen in all its splendor.

Moving On

While it's a nice title screen we have there, 'press a key and exit' is not exactly a working game concept. We need to have the title screen respond to key presses and begin the game.

pong.py (TitleScreen, part 2)

    def keyEvent(self,key,unicode,pressed):
        if(pressed):
            playing = PlayingGameState(self._driver,self.screen)
            self._driver.replace(playing)

In order for that to work, however, we need to import the next state. Put the import statement below at the top of pong.py with the rest of the imports:

pong.py

from playing import PlayingGameState

Playing

Before we get to the meat of playing the game, we first need to cover (yet more) basics.

Score

The most important thing about playing any game is winning! Thus, we'll need to make a way to keep track of the score for each side. Whereas before, in the title screen, making all the text a paintable was overkill, here it's exactly what we want. There are going to be two scores on the screen, after all.

playing.py

from states import *
import gui
 
import random
 
import pygame
from pygame.constants import *
from done import GameOver
 
class Score(gui.Paintable):
    def __init__(self,loc):
        gui.Paintable.__init__(self,loc)
        self.scoreFont = pygame.font.Font(None, 36)
        self.setScore(0)
 
    def setScore(self,score):
        self.score = score
        white = (255,255,255)
        self.scoreImage = self.scoreFont.render(str(score),0,white)
 
    def getScore(self):
        return self.score
 
    def paint(self,screen):
        if(self.scoreImage and self.loc):
            screen.blit(self.scoreImage, self.loc)

Ball

For the three of you on earth who have never heard of Pong, the object is to keep the ball from bouncing into your side. Our Ball is not only a paintable, but also updateable, since it'll need to move itself around the screen.

playing.py

class Ball(gui.Paintable, gui.Updateable):
 
    AXIS_X = 1
    AXIS_Y = 2
 
    def __init__(self,loc,bounds,radius=16,speed=110,increase=0.1):
        """The 'bounds' parameter indicates the width and height
        of the playing area"""
        gui.Paintable.__init__(self,loc)
        self.bounds = bounds
        self.radius = radius
        self.speed = speed
        self.increase = increase
        self.originalSpeed = speed
        self.dx = self.dy = 0
        self.center()
 
    def bounce(self, axis):
        if(axis & self.AXIS_X):
            self.dx = -self.dx
        if(axis & self.AXIS_Y):
            self.dy = -self.dy
 
        self.speed = self.speed + self.speed * self.increase;
 
    def center(self):
        self.loc = [self.bounds[0]/2, self.bounds[1]/2]
        self.dx = random.choice((-1,1))
        self.dy = random.choice((-1,1))
        self.outOfBounds = 0
        self.speed = self.originalSpeed
 
    def paint(self,screen):
        x = int(self.loc[0])
        y = int(self.loc[1])
        pygame.draw.circle(screen, (255,255,0), (x,y),self.radius)
 
    def update(self,delay):
        x,y = self.loc
        radius = self.radius
        toMove = delay * self.speed
        moveX = self.dx * toMove
        moveY = self.dy * toMove
 
        newX = x + moveX
        newY = y + moveY
 
        if(newY < radius or newY > self.bounds[1] - radius):
            self.bounce(self.AXIS_Y)
            moveY = self.dy * toMove * 2
            newY = y + moveY
        if(newX < radius):
            self.outOfBounds = -1
        elif(newX > self.bounds[0] - radius):
            self.outOfBounds = 1
 
        self.loc[0] = newX
        self.loc[1] = newY

Paddle

Our game has matured from its humble beginnings as a title screen to a program which will bounce a ball around all day. It'd be nice to have some interaction:

playing.py

class Paddle(gui.Paintable, gui.Keyable, gui.Updateable):
 
    def __init__(self,loc,size,maxY,speed=125):
        gui.Paintable.__init__(self,loc)
        gui.Keyable.__init__(self, [ K_UP, K_DOWN ])
        self.size = size
        self.maxY = maxY
        self.dy = 0
        self.speed = speed
        self.center()
 
    def center(self):
        y = self.maxY / 2 - self.size[1] / 2
        self.loc = (self.loc[0],y)
 
    def collidesWithBall(self,ball):
        topLeftX = self.loc[0] - self.size[0] / 2
        topLeftY = self.loc[1] - self.size[1] / 2
        width = self.size[0]
        height = self.size[1]
        ourRect = Rect(topLeftX,topLeftY,width,height)
 
        ballLeftX = ball.loc[0] - ball.radius
        ballLeftY = ball.loc[1] - ball.radius
        ballWidth = ball.radius * 2
        ballHeight = ball.radius * 2
        ballRect = Rect(ballLeftX,ballLeftY,ballWidth,ballHeight)
 
        if(ourRect.colliderect(ballRect)):
            ball.bounce(Ball.AXIS_X)
            return True
        return False
 
    def update(self,delay):
        x,y = self.loc
        halfHeight = self.size[1]/2
        toMove = delay * self.speed
        moveY = self.dy * toMove
 
        newY = y + moveY
        if(newY < halfHeight or newY > self.maxY - halfHeight):
            return
 
        self.loc = (self.loc[0],newY)
 
    def keyEvent(self,key,unicode, pressed):
        if(key == K_UP):
            self.dy = -1
        elif(key == K_DOWN):
            self.dy = 1
        if(not pressed):
            self.dy = 0
 
    def paint(self,screen):
        topLeftX = self.loc[0] - (self.size[0] / 2)
        topLeftY = self.loc[1] - (self.size[1] / 2)
        rect = [topLeftX,topLeftY, self.size[0], self.size[1]]
        pygame.draw.rect(screen, (255,255,255), rect)

The Paddle uses just about every part of our GUI framework (the only exception is that it's not a Mouseable, because clicking on a Pong paddle isn't going to be very productive). It paints itself to the screen, it updates itself by moving whichever way it's set to, and it takes keypresses to change where it's moving to.

AIPaddle

We could stop right now and make the PlayingGameState - our pong would be a two-player game, where each player used different keys to move their paddle. The above code would need a few tweaks, but it's possible. However, it's not likely you're pair-programming during a tutorial, so it'd be somewhat difficult to test. Instead, let's make a computer opponent:

playing.py

class AIPaddle(Paddle):
    def __init__(self,loc,size,maxY,ball,speed=125):
        Paddle.__init__(self,loc,size,maxY,speed)
        self.ball = ball
 
    def keyEvent(self,key,unicode,pressed):
        pass
 
    def update(self,delay):
        Paddle.update(self,delay)
 
        if(self.ball.loc[1] > self.loc[1] + 5):
            self.dy = 1
        elif(self.ball.loc[1] < self.loc[1] - 1):
            self.dy = -1
        else:
            self.dy = 0

It's not the smartest - eventually the ball will be moving too fast for it to follow - but it's a decent enough opponent. Note that we had to override keyEvent here - otherwise the parent Paddle logic would have moved the computer's paddle whenever the player hit a key!

PlayingGameState

Most of the game logic is actually handled by the objects themselves - they know how to move, and the paddle can even check to see if a ball is hitting it and bounce it accordingly.

class PlayingGameState(GuiState):
 
    def __init__(self,driver,screen):
        GuiState.__init__(self,driver,screen)
        self.ball = Ball(None, (640,480))
        self.ball.center()
        self.add(self.ball)
 
        self.player1 = Paddle( (10,0), (15,75), 480)
        self.player1.center()
        self.add(self.player1)
 
        self.score1 = Score((20,5))
        self.add(self.score1)
 
        self.player2 = AIPaddle( (630,0), (15,75), 480,self.ball)
        self.player2.center()
        self.add(self.player2)
 
        self.score2 = Score((610,5))
        self.add(self.score2)
 
    def update(self,delay):
        GuiState.update(self,delay)
 
        self.player1.collidesWithBall(self.ball)
        self.player2.collidesWithBall(self.ball)
 
        score = 0
        if(self.ball.outOfBounds < 0):
            score = self.score2.getScore() + 1
            self.score2.setScore(score)
        elif(self.ball.outOfBounds > 0):
            score = self.score1.getScore() + 1
            self.score1.setScore(score)
 
        if(score):
            self.ball.center()
            if(score >= 3):
                done = GameOver(self._driver,self.screen,
                                self.score1,self.score2)
                self._driver.replace(done)

As you can see, the parent GuiState and the Updateables themselves take care of a lot of the code - all our PlayingGameState had to do was add the objects, check for collisions, award points, and see if somebody won.

It's the End of the Game as we know it

The code just above here checks to see if anyone's achieved a score of 3 yet. If they have, it creates a new state and transitions to it. This is the last part of the game, where we'll display the final score and a victory or consolation message, as appropriate.

GameOver

GameOver is in many ways similar to TitleScreen, though it uses a bit more of the framework by borrowing PlayingGameState's score objects to display:

done.py

import pygame
from pygame.constants import *
 
from states import *
 
class GameOver(State):
 
    def __init__(self,driver,screen,score1,score2):
        State.__init__(self,driver,screen)
 
        if(score1.getScore() > score2.getScore()):
            win = 1
        else:
            win = 0
 
        self.messageFont = pygame.font.Font(None,36)
        self.font = pygame.font.Font(None, 20)
 
        if(win):
            self.setMessage("You are victorious!")
        else:
            self.setMessage("You have lost!")
 
        self.score1 = score1
        self.score2 = score2
 
    def setMessage(self, message):
        self.message = message
        self.msgImage = self.messageFont.render(message, 0, (255,255,255))
 
    def paint(self,screen):
        w = screen.get_width()
        h = screen.get_height()
 
        surface = self.msgImage
        centerX = w/2 - surface.get_width()/2
        centerY = h*0.25 - surface.get_height()/2
        screen.blit(surface, (centerX,centerY))
 
        surface = self.messageFont.render("to",0,(255,255,255))
        centerX = w/2 - surface.get_width()/2
        centerY = h/2 - surface.get_height()/2
        screen.blit(surface, (centerX,centerY))
 
        self.score1.loc = [30,centerY]
        self.score2.loc = [600,centerY]
 
        self.score1.paint(screen)
        self.score2.paint(screen)

That's all, folks!

If everything's been entered correctly, you should have a fully operational game of pong at this point - run pong.py and find out!

Room for Improvement

As nice a framework as it is, it's just a beginning. There are many ways it could be improved:

  • Our framework uses a double-buffered surface and repaints it every time. Some games may find it more efficient to just re-paint the dirty rectangles.
  • Pygame has built-in sprite objects, as well as routines for creating groups and checking collisions between them - it might be useful to co-opt them into the framework
  • The screen is simply pygame's screen object - we could wrap this into a class of its own in case we wanted to use something else to render everything (for instance, if we wanted to give the user an option between pygame and opengl)

Code Dump, Part 2

pong.py

#!/usr/bin/env python
 
import states
import pygame
from pygame.constants import *
from playing import PlayingGameState
 
def main():
    pygame.init()
    pygame.font.init()
 
    screen = pygame.display.set_mode( (640,480), DOUBLEBUF)
 
    driver = states.StateDriver(screen)
    title = TitleScreen(driver,screen)
    driver.start(title)
    driver.run()
 
class TitleScreen(states.State):
 
    def __init__(self,driver,screen):
        states.State.__init__(self,driver,screen)
        self.pongFont = pygame.font.Font(None,92)
        self.font = pygame.font.Font(None, 16)
 
    def paint(self,screen):
        white = (255, 255, 255)
 
        w,h = screen.get_size()
        surface = self.pongFont.render("PyPong!",0, white)
 
        centerX = w/2 - surface.get_width()/2
        centerY = h*0.25 - surface.get_height()/2
 
        screen.blit(surface, (centerX,centerY))
 
        surface = self.font.render("A tutorial",0,(128,128,128))
        centerX = w/2 - surface.get_width()/2
        centerY = h/2 - surface.get_height()/2
 
        screen.blit(surface, (centerX, centerY))
 
        surface = self.font.render("Press any key to begin", 0, white)
        centerX = w/2 - surface.get_width()/2
        centerY = h*0.75 - surface.get_height()/2
 
        screen.blit(surface, (centerX, centerY))
 
    def keyEvent(self,key,unicode,pressed):
        if(pressed):
            playing = PlayingGameState(self._driver,self.screen)
            self._driver.replace(playing)
 
if __name__ == '__main__':
    main()

playing.py

from states import *
import gui
 
import random
 
import pygame
from pygame.constants import *
from done import GameOver
 
class Score(gui.Paintable):
    def __init__(self,loc):
        gui.Paintable.__init__(self,loc)
        self.scoreFont = pygame.font.Font(None, 36)
        self.setScore(0)
 
    def setScore(self,score):
        self.score = score
        white = (255,255,255)
        self.scoreImage = self.scoreFont.render(str(score),0,white)
 
    def getScore(self):
        return self.score
 
    def paint(self,screen):
        if(self.scoreImage and self.loc):
            screen.blit(self.scoreImage, self.loc)
 
class Ball(gui.Paintable, gui.Updateable):
 
    AXIS_X = 1
    AXIS_Y = 2
 
    def __init__(self,loc,bounds,radius=16,speed=110,increase=0.1):
        """The 'bounds' parameter indicates the width and height
        of the playing area"""
        gui.Paintable.__init__(self,loc)
        self.bounds = bounds
        self.radius = radius
        self.speed = speed
        self.increase = increase
        self.originalSpeed = speed
        self.dx = self.dy = 0
        self.center()
 
    def bounce(self, axis):
        if(axis & self.AXIS_X):
            self.dx = -self.dx
        if(axis & self.AXIS_Y):
            self.dy = -self.dy
 
        self.speed = self.speed + self.speed * self.increase;
 
    def center(self):
        self.loc = [self.bounds[0]/2, self.bounds[1]/2]
        self.dx = random.choice((-1,1))
        self.dy = random.choice((-1,1))
        self.outOfBounds = 0
        self.speed = self.originalSpeed
 
    def paint(self,screen):
        x = int(self.loc[0])
        y = int(self.loc[1])
        pygame.draw.circle(screen, (255,255,0), (x,y),self.radius)
 
    def update(self,delay):
        x,y = self.loc
        radius = self.radius
        toMove = delay * self.speed
        moveX = self.dx * toMove
        moveY = self.dy * toMove
 
        newX = x + moveX
        newY = y + moveY
 
        if(newY < radius or newY > self.bounds[1] - radius):
            self.bounce(self.AXIS_Y)
            moveY = self.dy * toMove * 2
            newY = y + moveY
        if(newX < radius):
            self.outOfBounds = -1
        elif(newX > self.bounds[0] - radius):
            self.outOfBounds = 1
 
        self.loc[0] = newX
        self.loc[1] = newY
 
class Paddle(gui.Paintable, gui.Keyable, gui.Updateable):
 
    def __init__(self,loc,size,maxY,speed=125):
        gui.Paintable.__init__(self,loc)
        gui.Keyable.__init__(self, [ K_UP, K_DOWN ])
        self.size = size
        self.maxY = maxY
        self.dy = 0
        self.speed = speed
        self.center()
 
    def center(self):
        y = self.maxY / 2 - self.size[1] / 2
        self.loc = (self.loc[0],y)
 
    def collidesWithBall(self,ball):
        topLeftX = self.loc[0] - self.size[0] / 2
        topLeftY = self.loc[1] - self.size[1] / 2
        width = self.size[0]
        height = self.size[1]
        ourRect = Rect(topLeftX,topLeftY,width,height)
 
        ballLeftX = ball.loc[0] - ball.radius
        ballLeftY = ball.loc[1] - ball.radius
        ballWidth = ball.radius * 2
        ballHeight = ball.radius * 2
        ballRect = Rect(ballLeftX,ballLeftY,ballWidth,ballHeight)
 
        if(ourRect.colliderect(ballRect)):
            ball.bounce(Ball.AXIS_X)
            return True
        return False
 
    def update(self,delay):
        x,y = self.loc
        halfHeight = self.size[1]/2
        toMove = delay * self.speed
        moveY = self.dy * toMove
 
        newY = y + moveY
        if(newY < halfHeight or newY > self.maxY - halfHeight):
            return
 
        self.loc = (self.loc[0],newY)
 
    def keyEvent(self,key,unicode, pressed):
        if(key == K_UP):
            self.dy = -1
        elif(key == K_DOWN):
            self.dy = 1
        if(not pressed):
            self.dy = 0
 
    def paint(self,screen):
        topLeftX = self.loc[0] - (self.size[0] / 2)
        topLeftY = self.loc[1] - (self.size[1] / 2)
        rect = [topLeftX,topLeftY, self.size[0], self.size[1]]
        pygame.draw.rect(screen, (255,255,255), rect)
 
class AIPaddle(Paddle):
    def __init__(self,loc,size,maxY,ball,speed=125):
        Paddle.__init__(self,loc,size,maxY,speed)
        self.ball = ball
 
    def keyEvent(self,key,unicode,pressed):
        pass
 
    def update(self,delay):
        Paddle.update(self,delay)
 
        if(self.ball.loc[1] > self.loc[1] + 5):
            self.dy = 1
        elif(self.ball.loc[1] < self.loc[1] - 1):
            self.dy = -1
        else:
            self.dy = 0
 
class PlayingGameState(GuiState):
 
    def __init__(self,driver,screen):
        GuiState.__init__(self,driver,screen)
        self.ball = Ball(None, (640,480))
        self.ball.center()
        self.add(self.ball)
 
        self.player1 = Paddle( (10,0), (15,75), 480)
        self.player1.center()
        self.add(self.player1)
 
        self.score1 = Score((20,5))
        self.add(self.score1)
 
        self.player2 = AIPaddle( (630,0), (15,75), 480,self.ball)
        self.player2.center()
        self.add(self.player2)
 
        self.score2 = Score((610,5))
        self.add(self.score2)
 
    def update(self,delay):
        GuiState.update(self,delay)
 
        self.player1.collidesWithBall(self.ball)
        self.player2.collidesWithBall(self.ball)
 
        score = 0
        if(self.ball.outOfBounds < 0):
            score = self.score2.getScore() + 1
            self.score2.setScore(score)
        elif(self.ball.outOfBounds > 0):
            score = self.score1.getScore() + 1
            self.score1.setScore(score)
 
        if(score):
            self.ball.center()
            if(score >= 3):
                done = GameOver(self._driver,self.screen,
                                self.score1,self.score2)
                self._driver.replace(done)

done.py

import pygame
from pygame.constants import *
 
from states import *
 
class GameOver(State):
 
    def __init__(self,driver,screen,score1,score2):
        State.__init__(self,driver,screen)
 
        if(score1.getScore() > score2.getScore()):
            win = 1
        else:
            win = 0
 
        self.messageFont = pygame.font.Font(None,36)
        self.font = pygame.font.Font(None, 20)
 
        if(win):
            self.setMessage("You are victorious!")
        else:
            self.setMessage("You have lost!")
 
        self.score1 = score1
        self.score2 = score2
 
    def setMessage(self, message):
        self.message = message
        self.msgImage = self.messageFont.render(message, 0, (255,255,255))
 
    def paint(self,screen):
        w = screen.get_width()
        h = screen.get_height()
 
        surface = self.msgImage
        centerX = w/2 - surface.get_width()/2
        centerY = h*0.25 - surface.get_height()/2
        screen.blit(surface, (centerX,centerY))
 
        surface = self.messageFont.render("to",0,(255,255,255))
        centerX = w/2 - surface.get_width()/2
        centerY = h/2 - surface.get_height()/2
        screen.blit(surface, (centerX,centerY))
 
        self.score1.loc = [30,centerY]
        self.score2.loc = [600,centerY]
 
        self.score1.paint(screen)
        self.score2.paint(screen)

Related Links

Pygame

Pychecker