User:Atiaxi/pong
From GPWiki
The wiki is now hosted by GameDev.NET at wiki.gamedev.net. All gpwiki.org content has been moved to the new server. However, the GPWiki forums are still active! Come say hello.
[edit] 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 fasion, I shall now create Pong! [edit] Another Word About StyleLike 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() [edit] The Title ScreenWhat's a game without a title screen? Plus, it's a simple way to illustrate the state mechanism. [edit] SetupBefore 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. [edit] TitleScreenThe 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. [edit] RunningWith 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. [edit] Moving OnWhile 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 [edit] PlayingBefore we get to the meat of playing the game, we first need to cover (yet more) basics. [edit] ScoreThe 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)
[edit] BallFor 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
[edit] PaddleOur 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. [edit] AIPaddleWe 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! [edit] PlayingGameStateMost 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. [edit] End of the Game as we know itThe 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. [edit] GameOverGameOver 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)
[edit] 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! [edit] Room for ImprovementAs nice a framework as it is, it's just a beginning. There are many ways it could be improved:
[edit] Code Dump, Part 2[edit] 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()
[edit] 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)
[edit] 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)
[edit] Related Links |


