Pygame Tutorial 4 – Breakout: Step 5 – Part 1
Written by Collin Green — Version 1.0.0 — 2011-01-06
Goal:
This is our last step. At the end of the last tutorial we had a working game. Now we are going to add two last features to really finish it off: powerups and a persistent high score table. This actually requires quite a bit of code, so I’m breaking this into two different pages. For clarity, I strongly recommend you download the package and use a diff program to compare this to the last tutorial as you read instead of trying to piece together the code I add one step at a time.
Planning the Powerups
For the powerups, we need to make changes in quite a few places. We need a powerup class to render the powerups on the screen for the player to collect. We need timers for dropping them, applying them, and turning them back off. We need to check for collisions with the powerups and collisions with their effects.
First things first, we need to decide what we are adding. I threw together some powerup images and decided I was going to add 4 different powerups: 1up, big paddle, slow ball, and a devastating laser. Let’s get started.
The Powerup Class
This is our sprite class for rendering the powerups on screen as they fall toward the player. Not much different than the other sprites we already have. When we create it we are going to tell it what type of powerup to be and it will pick its image and starting location and drop itself as the frames go by.
335 336 337 338 339 340 341 342 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 | class Powerup(pygame.sprite.Sprite): """A powerup.""" def __init__(self, type='bigpaddle'): pygame.sprite.Sprite.__init__(self) # some variables we need self.type = type # which powerup is it self.collected = False # has it been collected yet? self.countdown = 1 # duration of the effect # set individual countdowns for the powerups with actual durations if type == 'bigpaddle': self.countdown = 60 * 25 elif type == 'slowball': self.countdown = 60 * 10 self.imagepaths = { 'bigpaddle': os.path.join('images', 'powerup_paddle.png'), 'laser': os.path.join('images', 'powerup_laser.png'), '1up': os.path.join('images', 'powerup_lightning.png'), 'slowball': os.path.join('images', 'powerup_ball.png'), } # set image and rect so we can be rendered self.image = pygame.image.load(self.imagepaths[type]) self.rect = self.image.get_rect() # set initial position somewhere near the top but below the blocks self.rect.center = random.randint(20, 500), 125 def update(self): """Called every frame. Move the powerup down a bit so it 'falls' down the screen. Return false if below the screen because the player missed it.""" self.rect.y += 2 if self.rect.y > 600: return False return True |
Game.__init__
Next we add a couple variables to the Game.__init__ for managing our powerup system. We are going to track our current powerup (falling or applied) and have a countdown until the next drop (starting at roughly 10 seconds).
501 502 503 | # variables for powerups self.currentpowerup = None self.powerupdrop = 60 * 10 |
Game.run
Next we are going to tweak our sprite rendering code so the update methods return false when the sprites need to be removed. Change the sprite.update() section to look like below. This will allow us to correctly delete our laser sprite when it is finished. We are also going to call our managePowerups method each frame.
531 532 533 534 535 | # update our sprites for sprite in self.sprites: alive = sprite.update() if alive is False: self.sprites.remove(sprite) |
547 548 | # manage powerups self.managePowerups() |
Game.managePowerups
This is a hefty method for handling all the new powerup functionality. I should have split it up into multiple methods for clarity but I didn’t so you’ll just have to suffer through it. Take it as another ‘what not to do’ lesson.
The first case we are going to handle in this function is when there is no active powerup and there isn’t one currently dropping. We are going to decrement our countdown until dropping and, if it hits zero, we create a new powerup. Our powerup class handles its location and dropping so all we have to do is decide which powerup to make. I made a little array of tuples with the ‘chances in 100′ for that powerup to drop, then created a random number between 1 and 100 and used the corresponding powerup.
805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 | def managePowerups(self): """Called each frame. Drops new powerups as necessary. Checks if the paddle hits a powerup and applies the powerup. Manages the powerups timing out.""" # no powerup, update countdown and drop one if necessary if self.currentpowerup is None: # decrement powerup drop countdown if not self.isReset: # dont continue countddown if waiting to serve self.powerupdrop -= 1 # drop a powerup if time if self.powerupdrop <= 0: # drop chances to use with random droppercentages = [ (10, '1up'), # 10% chance (30, 'laser'), # 20% chance (55, 'slowball'), # 25% chance (100, 'bigpaddle') # 45% chance ] # decide which powerup to drop choice = random.uniform(0,100) for chance, type in droppercentages: if choice <= chance: # create new powerup and add it to render group self.currentpowerup = Powerup(type) self.sprites.add(self.currentpowerup) break return |
Next we are going to handle the case we just created above — the user has no powerup but one has spawned and is falling down the screen. In this case we need to check if the paddle hits the powerup and, if so, apply the effect. We call some methods we haven’t written yet but the names should make them pretty obvious. The effects are all pretty easy to apply – the only one that really does anything to the game is the laser, which we will make a sprite for and check the collisions. Note that we don’t care if the Block thinks it should be deleted, we just delete it anyway – this way the laser destroys the solid, formerly indestructible blocks too.
Here we also check if the powerup is ‘dead’ because the user missed it. If so, we reset the countdown to shorter than normal – in this case between 10 and 20 seconds.
836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 | # if powerup hasn't been collected yet, check for collision if not self.currentpowerup.collected: # collision, ie: the player collected the powerup if self.paddle.rect.colliderect(self.currentpowerup.rect): # apply the powerup if self.currentpowerup.type == 'bigpaddle': # increase paddle size self.paddle.grow() elif self.currentpowerup.type == 'laser': # create laser sprite for rendering laser = Laser(self.paddle.rect.centerx) self.sprites.add(laser) # destroy all blocks it touches touchedblocks = pygame.sprite.spritecollide(laser, self.blocks, False) for b in touchedblocks: # add score for those blocks alive,points = b.hit(1000) self.score.add(points) # remove blocks from render group self.blocks.remove(b) elif self.currentpowerup.type == '1up': # increment player lives self.lives.setLives( self.lives.getLives() + 1) elif self.currentpowerup.type == 'slowball': # decrease ball max speed self.ball.slowDown() # set collected so timer starts self.currentpowerup.collected = True self.sprites.remove(self.currentpowerup) # not colliding - keep moving and check if we missed it else: # update powerup and delete if necessary alive = self.currentpowerup.update() if not alive: self.sprites.remove(self.currentpowerup) self.currentpowerup = None # reset drop counter self.powerupdrop = random.randint(60 * 10, 60 * 20) |
Lastly, we need to countdown until the end of the powerup effect (only for big paddle and slow ball) and when we hit zero, turn them off and restart the countdown until a new powerup is dropped (30-60 seconds this time).
882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 | # if powerup is currently active, continue countdown elif self.currentpowerup.countdown > 0: # decrement countdown self.currentpowerup.countdown -= 1 # powerup is over -- has been collected and countdown <= 0 else: # if we haven't turned off the current powerup yet, do so if self.currentpowerup is not None: if self.currentpowerup.type == 'bigpaddle': self.paddle.shrink() elif self.currentpowerup.type == 'slowball': self.ball.speedUp() # set current to none self.currentpowerup = None # set new powerupdrop countdown self.powerupdrop = random.randint(60 * 30, 60 * 60) |
Applying the Big Paddle Powerup
Now we add two methods to the Paddle class – grow and shrink – to handle when we apply and remove the big paddle powerup. It is fairly straightforward – we save the current location, give it a new, expanded image, and place the center back where it was. I also check here to see if we are over one of the walls after growing and move us back in if necessary. Shrink is copy pasted with the regular sized paddle image. I can’t think of a way the paddle would be over a wall when shrinking, but I left that code there just in case.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | def grow(self): """Increases the size of the paddle.""" # get current position xy = self.rect.center # set image and rect self.image = pygame.image.load(os.path.join('images','paddle.gif')) self.image = pygame.transform.rotate(self.image, 90) # double image size self.image = pygame.transform.scale2x(self.image) # get new rect self.rect = self.image.get_rect() # reset position self.rect.centerx, self.rect.centery = xy # if paddle is now over a wall, fix it if self.rect.right > 510: self.rect.right = 509 elif self.rect.left < 10: self.rect.left = 11 def shrink(self): """Returns the size of the paddle to normal""" # get current position xy = self.rect.center # set image and rect self.image = pygame.image.load(os.path.join('images','paddle.gif')) self.image = pygame.transform.rotate(self.image, 90) # get new rect self.rect = self.image.get_rect() # reset position self.rect.centerx, self.rect.centery = xy # if paddle is now over a wall, fix it if self.rect.right > 510: self.rect.right = 509 elif self.rect.left < 10: self.rect.left = 11 |
Applying the Slow Ball powerup
Instead of doing anything directly here, I just decided to change the Ball’s maxspeed variable. If the ball is already going slow it won’t change to super slow and boring, but if it is going fast it will automatically cap the speed at the new lower number. Undoing it is as simple as resetting the maxspeed back up to 10.
170 171 172 173 174 175 176 | def slowDown(self): """Called for the slowball powerup""" self.maxspeed = 5 def speedUp(self): """Called for the slowball powerup""" self.maxspeed = 10 |
Applying the 1-up Powerup
We already added a life to the counter in the managePowerups method, so we could be finished already, but there is a tiny tweak we need to make. When I wrote the Lives code the first time there was no way to have more than the initial 3 lives so I set the left side of the lives sprite and was finished. Now the player can possibly have unlimited lives, so the balls representing extra lives will quickly go off screen. To fix this, I’m going to change just one word and set the center of the lives sprite instead of the left. Now the lives can accommodate a much bigger number before going offscreen or overlapping the score.
331 332 | # move rect to the proper location self.rect.left, self.rect.centery = self.xy |
becomes
331 332 | # move rect to the proper location self.rect.centerx, self.rect.centery = self.xy |
Applying the Laser Powerup
We are going to create another sprite class just like any of our others. The only real difference here is that instead of using an image I just used the pygame draw methods to make an ugly little multicolor laser design. The Laser moves itself up and offscreen and returns False in its update method when it is safe to delete it, which is the code we added way up at the start of this tutorial.
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 | class Laser(pygame.sprite.Sprite): """A laser sprite for use with the laser powerup.""" def __init__(self, x): pygame.sprite.Sprite.__init__(self) # create an image image = pygame.Surface((50, 550)) image.fill( (255,0,0) ) # fill it with red pygame.draw.rect(image, (255,255,0), pygame.Rect(10,0,30,550)) # yellow rect pygame.draw.rect(image, (0,255,250), pygame.Rect(20,0,10,550)) # cyan rect # set image and rect so we can be rendered self.image = image self.rect = self.image.get_rect() # set initial position somewhere near the top but below the blocks self.rect.centerx = x self.rect.bottom = 550 def update(self): """Called every frame. Moves the laser up and off the screen. Returns false when the laser is completely gone and can be deleted.""" self.rect.y -= 20 if self.rect.bottom < 0: return False return True |
Bug Fix! – Block.hit()
When I wrote the hit method in the previous tutorials I planned for the ability to cause more than one point of damage per hit, but I wrote it totally wrong. I didn’t notice the bug because there wasn’t a way to actually do it yet, but now that the laser destroys everything (by doing 1000 points of damage) we have to fix it. The old code just returned the current level of the block * 100. The new, correct code adds up the points for each level of the block that gets destroyed by the hit. The first couple lines of the hit method now look like this.
203 204 205 206 207 208 209 210 211 212 | points = 0 while damage > 0 and self.level > 0: # points earned for hitting the block points += 100 * self.level # decrement the block level self.level -= damage # decrement the damage remaining to apply damage -= 1 |
Result and Download
Here is a shot of the laser in action!

Tutorial Download:
Pygame Tutorial 4 - Breakout (243)