Pygame Tutorial 4 – Breakout: Step 4
Written by Collin Green — Version 1.0.0 — 2010-12-31
Goal:
This time we will be taking what we made in the previous tutorials and making it into an actual game. Before we could bounce the ball around and break blocks, but to be complete we need multiple levels, score keeping, and a way to win/lose. We made the level editor last time, so all we have to do to get levels is write a way to load them. We are going to add a score at the bottom and give the player a limited number of ‘lives’. If the player beats every level, we will tell them they win. If they don’t, they lose!
Score Object
The first thing we are going to add to our code from tutorial 2 is a sprite to show the score. This is extremely similar to the TextSprite from the level builder tutorial; we create an image of some text using pygame.font. There also a couple methods to make it easier to change the score and re-generate the image.
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | 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.color = (255, 165, 0) # our font color in rgb self.score = 0 # start at zero self.reRender() # generate the image def update(self): pass def add(self, points): """Adds the given number of points to the score.""" self.score += points self.reRender() def reset(self): """Resets the scores to zero.""" self.score = 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"%(self.score), True, self.color) self.rect = self.image.get_rect() self.rect.center = self.xy |
Lives Object
Similar to the score object, we are going to create a Lives object to show how many lives the player has left. We are going to use the pygame Sprite class as usual, but generate our image by copying the ball image for however many lives are left. Every time we change how many lives remain we just regenerate the image.
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | class Lives(pygame.sprite.Sprite): """An object to represent the player's remaining lives.""" def __init__(self, xy, startinglives=3): pygame.sprite.Sprite.__init__(self) self.xy = xy # our rendering position self.ballimage = pygame.image.load(os.path.join('images','ball.gif')) # path to the ball image self.setLives(startinglives) # sets lives and generates the lives image def getLives(self): return self.lives def setLives(self, lives): self.lives = lives self.generateLivesImage() def generateLivesImage(self): """Generates a new image for this sprite by repeating the ball image for each life the player still has.""" # get a new surface that is the width of the ball image * the lives ballrect = self.ballimage.get_rect() padding = 5 newwidth = (ballrect.width + padding) * self.lives # create the surface surface = pygame.Surface( (newwidth, ballrect.height) ) surface.set_colorkey((0,0,0)) # set the color key to black so we # have a transparent background # draw the ball on it repeatedly for l in range(self.lives): surface.blit(self.ballimage, ((ballrect.width + padding) * l, 0)) # set as image and rect so it can be rendered self.image = surface self.rect = surface.get_rect() # move rect to the proper location self.rect.left, self.rect.centery = self.xy |
Game.__init__ Changes
We are going to change the end of our Game.__init__ method. First of all we need a variable to track which level we are on since we have more than one now. We are also going to change the loadLevel method to accept the level to load. Next we need a variable to check if we are still playing – we will use this to stop the gameplay when the player has won or lost. Finally, we add our score and lives sprites and add them to the sprites render group.
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | # load the first level self.currentlevel = 1 self.loadLevel(self.currentlevel) # track the state of the game self.isReset = True self.playing = True # create our score object self.score = Score((75, 575)) self.sprites.add(self.score) # create our lives object self.lives = Lives((450, 575), 3) self.sprites.add(self.lives) |
Game.run
Next we are going to make some changes to the run method. We aren’t replacing all of it, but I’m posting the entire run method here for clarity. We are going to move most of the gameplay (rendering, collisions, movement) into an if statement so it only happens if self.playing is True. Also, each frame we are going to check if the player beat the level (ie, all the blocks are gone) and change to the next level if necessary.
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | 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 4 - Breakout %d fps' % self.clock.get_fps()) # if we haven't lost yet if self.playing: # update our sprites for sprite in self.sprites: sprite.update() # handle ball -- all our ball management here self.manageBall() # manageCollisions self.manageCollisions() # check if we beat the level if len(self.blocks) == 0: self.newLevel() # 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 # render blocks self.blocks.clear(self.window, self.background) dirty += self.blocks.draw(self.window) # blit the dirty areas of the screen pygame.display.update(dirty) # updates just the 'dirty' areas print 'Quitting. Thanks for playing' |
Game.manageCollisions
Remember when we made the Block.hit method return both if the Block was still around and the score? Now we are going to use that by adding that returned score to our score object. We are only changing two lines here, but I’m posting the entire method so there is no confusing on where they go.
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 | def manageCollisions(self): """Called every frame. Manages collisions between the ball and the paddle and the ball and the blocks""" # lets do the paddle and the ball first if pygame.sprite.collide_rect(self.ball, self.paddle): # need to get WHERE on the paddle the ball hit so we can apply # the proper rebound self.collisionHelper(self.ball, self.paddle) # ball and blocks collisions = pygame.sprite.spritecollide(self.ball, self.blocks, dokill=False) # if we hit two blocks at once we need to bounce differently if len(collisions) >= 2: # going to just take the first 2 # if between them horizontally, bounce like a flat horizontal wall if collisions[0].rect.y == collisions[1].rect.y: self.ball.vely *= -1 # bounce in y direction self.ball.rect.top = collisions[0].rect.bottom + 1 # move out of collision # if between them vertically, bounce like a flat vertical wall else: # we were moving right if self.ball.velx > 0: self.ball.rect.right = collisions[0].rect.left - 1 # move out of collision # we were moving left else: self.ball.rect.left = collisions[0].rect.right + 1 # move out of collision # bounce x direction self.ball.velx *= -1 # apply damage to blocks for block in collisions: destroyed, points = block.hit(self.ball.damage) self.score.add(points) # add the points to the score if destroyed: self.blocks.remove(block) # if we hit one block, use the collisionHelper function if len(collisions) == 1: item = collisions[0] self.collisionHelper(self.ball, item) # collided with a block, call the block hit method if hasattr(item, 'hit'): destroyed, points = item.hit(self.ball.damage) self.score.add(points) # add the points to the score # remove from render group if block is destroyed if destroyed: self.blocks.remove(item) |
Game.newLevel
Now we are going to add a function to handle when the player beats the current level. First we check if there is another level available. If so, we load it and reset the paddle and ball. If not, the player has beaten every level we have, so we are going to create some text and put it on screen, then turn off the gameplay with our self.playing variable we put in our run method. Notice how easy this makes adding new levels — if we create new ones or take some away, the game will automatically handle the change without us having to edit anything in the code.
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 | def newLevel(self): """Called when the user completes the level. Loads the next level if possible and resets the paddle and ball. If no more levels are available, shows the win message.""" self.currentlevel += 1 # if there is a file for the next level, load it if os.path.isfile(os.path.join('levels', 'level%d.level'%self.currentlevel)): self.loadLevel(self.currentlevel) self.paddle.reset() self.ball.reset() self.isReset = True # no file, show win message else: # game over! render a game over message and stop the game font = pygame.font.Font(None, 50) # load the default font, size 50 endmessage = font.render("You Win!", True, (255,150,80)) endmessagerect = endmessage.get_rect() endmessagerect.center = (260, 250) # blit it on the background and flip (render) the display one last time self.window.blit(endmessage, endmessagerect) pygame.display.flip() # turn off all the gameplay self.playing = False |
Game.reset
Now we have to rewrite our reset method. Since we are keeping track of lives now we have to take away a life when the ball hits the bottom. If the player is out of lives, we are going to stop the gameplay and display a message just like in the newLevel method.
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 | def reset(self): """Called when the ball hits the bottom wall. The player loses a life and the ball is placed on the paddle, ready to be served.""" # handle the lives lives = self.lives.getLives() # if we have lives to spare if lives > 0: self.lives.setLives(lives-1) self.paddle.reset() self.ball.reset() self.isReset = True else: # game over! render a game over message and stop the game font = pygame.font.Font(None, 50) # load the default font, size 50 endmessage = font.render("Game Over!", True, (255,100,50)) endmessagerect = endmessage.get_rect() endmessagerect.center = (260, 250) font = pygame.font.Font(None, 40) # load the default font, size 40 endmessage2 = font.render("Press Escape to Quit", True, (255,100,50)) endmessagerect2 = endmessage2.get_rect() endmessagerect2.center = (260, endmessagerect.bottom + 20) # blit it on the background and flip (render) the display one last time self.window.blit(endmessage, endmessagerect) self.window.blit(endmessage2, endmessagerect2) pygame.display.flip() # turn off all the gameplay self.playing = False |
Game.parseLevelFile
Since we have multiple levels now we need a way to load them from the files we create with the level editor into the game. This method takes the path to the next level file, reads it into an array, converts the strings to integers, and returns the array as the new level. I also added a bit of code to return the default level we made in the previous tutorials in case we don’t find the level we are looking for. This also stops the game from crashing if we don’t have any levels when the game starts.
658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 | def parseLevelFile(self, filepath): """Parses a level file and returns a 2D array representing it. If it fails to find the level it returns a default level""" defaultlevel = [ [0, 1, 1, 2, 1, 1, 2, 1, 1, 0], [0, 0, 2, 3, 4, 4, 3, 2, 0, 0], [0, 0, 0, 4, 5, 5, 4, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ] # verify the file exists if os.path.isfile(filepath): # open it for reading f = open(filepath, 'r') # read all the lines from it into an array rows = f.readlines() level = [] # for all the rows in the array we read from the file for r in rows: # strip any /n from the row, then split on spaces so we get # an array of our block levels blocks = r.strip().split(' ') newrow = [] # convert our strings to integers and place in the row for b in blocks: newrow.append(int(b)) level.append(newrow) # return the level return level else: return defaultlevel |
Game.loadLevel
The last thing we need to do is change the loadLevel method to accept the levelnumber to load as a parameter and to get the level from the parseLevelFile method instead of just using the default one we created before.
730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 | def loadLevel(self, levelnumber): """Loads a level. Places blocks on the board and adds them to the blocks render group""" # parse the desired level file level = self.parseLevelFile( os.path.join('levels', 'level%d.level' % levelnumber)) # levels are a 2d array with 5 rows and 10 columns # each space represents a block for i in range(5): # for every row for j in range(10): # for every column if level[i][j] != 0: # if the space isnt empty blocklevel = level[i][j] # get the level of the block x = 35 + (50*j) # x = 10 (for the wall) + 25 (to center of first block) # + 50 (width of a block) * j (number of blocks over we are) y = 20 + (20*i) # y = 10 (for the wall ) + 10 (to center of first block) # + 20 (height of a block) * i (number of blocks down) # if greater than 0 and less than 6, ie not a gray block if blocklevel > 0 and blocklevel < 6: # create a block and add it self.blocks.add(self.blockfactory.getBlock((x,y), blocklevel)) # if block level == 6, solid block elif blocklevel == 6: # create solid block and add it self.blocks.add(self.blockfactory.getSolidBlock((x,y))) |
Result and Download
Now we have a game! We added multiple levels, keeping score, and ways to win and lose. Go make some levels of your own in the level editor and you can actually have some fun.

Tutorial Download:
Pygame Tutorial 4 - Breakout (243)