Pygame 4: Breakout – Step 5 – Part 1

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 (1056)