Pygame 4: Breakout – Step 2

Pygame Tutorial 4 – Breakout: Step 2
Written by Collin Green — Version 1.0.0 — 2010-12-28

Goal:

In this tutorial we are going to take our basic setup from step 1 and add the crux of the gameplay: collisions. At the end we should be able to serve the ball and have it bounce around, taking out blocks and rebounding as expected from the paddle, the walls, and the blocks.

The Ball

First we are going to create the ball. I copied the ball from the pong tutorial then changed it a bit. I moved the movement code from last tutorial into the ball via the update and move functions. In the update function, we use some trig to cap the ball speed so it doesn’t get too out of hand. I also changed the serve function to send the ball vertically instead of horizontal.

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Ball(pygame.sprite.Sprite):
    """A ball sprite. Subclasses the pygame sprite class."""
 
    def __init__(self, xy):
        pygame.sprite.Sprite.__init__(self)
        # set the image and rect for rendering
        self.image = pygame.image.load(os.path.join('images','ball.gif'))
        self.rect = self.image.get_rect()
        self.rect.centerx, self.rect.centery = xy
 
        # keep track of some info about the ball
        self.maxspeed = 10
        self.servespeed = 5
        self.damage = 1
 
        # the ball velocity
        self.velx = 0
        self.vely = 0
 
    def move(self, dx, dy):
        """Move the ball"""
        self.rect.x += dx
        self.rect.y += dy
 
    def update(self):
        """Called to update the sprite. Do this every frame. Handles
        moving the sprite by its velocity. Caps the speed as necessary."""
        speed = math.hypot(self.velx, self.vely)
        # if going faster than max speed
        if speed > self.maxspeed:
            speed = self.maxspeed                       # cap speed
            angle = math.atan2(self.vely, self.velx)    # get angle
            self.velx = math.cos(angle) * speed         # x component at new speed
            self.vely = math.sin(angle) * speed         # y component at new speed
 
        # move the ball
        self.move(self.velx, self.vely)
 
    def reset(self):
        """Put the ball back in the middle and stop it from moving"""
        self.rect.centerx = 260
        self.rect.bottom = 549   # place just above the paddle so we dont collide
        self.velx = 0
        self.vely = 0
 
    def serve(self):
        angle = -90 + random.randint(-30, 30)
 
        # do the trig to get the x and y components
        x = math.cos(math.radians(angle))
        y = math.sin(math.radians(angle))
 
        self.velx = self.servespeed * x
        self.vely = self.servespeed * y

Block.hit

Next we are going to add the hit function to our block class. We are going to call this function when the ball collides with a block so we need to both decrement the block level (and update the image accordingly) and return if the block is still around so we can delete it if necessary. We also return any points we earned for hitting the block, but we won’t use that until later.

142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
    def hit(self, damage=1):
        """Called when the block gets hit. Damage is the amount
        of levels to drop the block down from one hit -- perhaps for
        powerups or something later on.
        Returns a tuple (destroyed, points)
        destroyed is true if the block is destroyed, False otherwise
        points is the number of points gained by hitting the block"""
        # points earned for hitting the block
        points = 100 * self.level
 
        # decrement the block level
        self.level -= damage
 
        # check if destroyed
        if self.level <= 0:
            return True, points
 
        # not destroyed, set new image
        else:
            self.image = self.images[self.level]
            xy = self.rect.center               # save previous position
            self.rect = self.image.get_rect()   # reset rect in case shape changes
            self.rect.center = xy               # reset block to old position
            return False, points

SolidBlock.hit

Mirrors the Block.hit function, but since SolidBlocks can’t be destroyed it just returns False (not destroyed), 0 (no points earned)

178
179
180
    def hit(self, damage):
        """Returns false, 0 since it cannot be destroyed"""
        return False, 0

Game.__init__ Changes

Now that we have a Ball class we need to add it to the game. Make the following changes to the Game.__init__ method to add the ball (and render it via the sprites group) and a variable to track when the ball is waiting on the paddle, ready to be served.

245
246
247
248
        # create ball
        self.ball = Ball((0,0))
        self.ball.reset()
        self.sprites.add(self.ball)
259
260
        # track the state of the game
        self.isReset = True

Game.run

New we need to add two functions to our Game.run method so they get called every frame.

286
287
288
289
290
            # handle ball -- all our ball management here
            self.manageBall()
 
            # manageCollisions
            self.manageCollisions()

Game.handleEvents

This is the last of our trivial changes. In step 1 we made a spot for handling the pressing of the space bar, but we left it blank. Now we are going to fill that in so that if we are waiting to serve the ball (self.isReset is True) and the spacebar is pressed we serve it and set self.isReset to False so subsequent spacebar presses do nothing until the ball is reset again.

328
329
330
                    if self.isReset:
                        self.ball.serve()
                        self.isReset = False

Game.manageBall

We are adding the manageBall function to the Game object that we called in the run function above. This is going to handle keeping the ball inside the play area (by faking collisions with the ‘walls’) and keep the ball on the paddle if we are waiting to serve. This is very similar to the pong tutorial code, so check that out if this confuses you. Notice that the walls in our background image only go down 550 pixels, so if the ball is below that we don’t bounce it horizontally.

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
373
374
375
376
    def manageBall(self):
        """This basically runs the game. Moves the ball and handles
        wall and paddle collisions."""
 
        # if isReset is true, we are waiting to serve the ball so keep it on the paddle
        if self.isReset:
            self.ball.rect.centerx = self.paddle.rect.centerx
            return
 
        # if ball isn't below the walls
        if self.ball.rect.top < 550:
            # bounce ball off the ceiling
            if self.ball.rect.top <= 10:
                self.ball.rect.top = 11
 
                # reverse y velocity so it 'bounces'
                self.ball.vely *= -1
 
            # bounce ball off the left wall
            if self.ball.rect.left <= 10:
                self.ball.rect.left = 11
 
                # reverse X velocity so it 'bounces'
                self.ball.velx *= -1
 
            # bounce ball off the right wall
            elif self.ball.rect.right > 510:
                self.ball.rect.right = 509
 
                # reverse X velocity so it 'bounces'
                self.ball.velx *= -1
 
        # ball IS below the walls
        else:
            # if ball hits the bottom, reset the board
            if self.ball.rect.bottom > 600:
                self.reset()

Game.manageCollisions

This is the first of the actually interesting code for this tutorial. This is the first of two functions we are going to use for handling the collisions between the ball and the blocks and the ball and the paddle. We check if the ball hits two blocks at once (by hitting directly between them) and handle that here by calling each Block’s hit method and bouncing the ball as if it hit a flat wall instead of 2 Block corners. If the ball hits a single block or the paddle, we use the next function we will define, collisionHelper, to decide in which direction we want to reflect the ball.

378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
    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)
                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, score = item.hit(self.ball.damage)
                # remove from render group if block is destroyed
                if destroyed:
                    self.blocks.remove(item)

Game.collisionHelper

This is the meat of the step 2 tutorial. In Breakout, we want the ball to act semi-realistically when it collides with the blocks and the paddle. When it hits a flat face like the top/bottom or the sides, we want it to reflect normally, but when it hits a corner we want it to shoot off the corner back in the direction from which it came. We aren’t going to do any fancy physics library worthy code here — instead when we find a collision between the Ball and a Block we are going to call this function which then tests first if the ball is in the corners then, if not, tests if the ball hit one of the sides. It is fairly accurate and good enough for our simple game at this framerate. However, this type of system won’t work if the ball starts moving fast enough — consider if the ball is traveling to the right and is directly to the left of a block in one frame but is going so fast that in the next frame it is all the way on the right side of the block. With our simple algorithm here, we would find it on the right and therefore think it hit on the right side and handle it totally incorrectly (move it outside the block [i]to the right[/i] and reverse its x velocity, causing it to immediately hit the block again from the right and almost certainly causing exactly the same problem in a loop). If that doesn’t make sense, ignore it :) but if it does, beware of shortcuts like this and the benefits versus the potential costs and inaccuracies.

Look through the function and refer to it as we go into more detail. First of all, we are going to impart 1/3 of the paddle velocity on the ball, just like in pong, to simulate a bit of ‘spin’ and give the game more variety. You may want to try taking this out to see the effect just this one line of code has on how fun it is to play. Next we test the corners. A lot happens in the first line of each block — first we build a pygame.Rect object to represent the corner we are in: in the top-left case we build it from the top left corner (duh) with a height and width of cornerwidth, a variable set a couple lines before to easily adjust how big the corners are. If we hit this box, we treat it like the ball hit the corner, so we want to reflect it with exactly its current speed, but at a 45 degree angle out from the block. Note that it is possible that we just barely clip the top left corner as we come down and left – in this case reflecting up at 45 degrees would feel totally wrong so we add a couple if statements to only adjust the velocity if it is coming at the corner directly — if it hits the top left from the right or from the bottom, we just skip over it like we didn’t hit it at all. The other corners are all the same, with the Rects and angles adjusted to match their corner.

If we don’t hit any corners, we test the sides. We treat the top and the bottom the same way as the corners – since they are wide we create a rect for them and see if the Ball is in them. The left and right are very small though, so to save a tiny amount of processor power we just test if the ball contains the center of that side. You may want to check out the pygame docs if these functions look confusing.

425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
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
    def collisionHelper(self, ball, item):
        """Function that takes the ball and an item it collides with
        and sets the new ball velocity and position based on how the
        two collided. Does a fairly cheap but also fairly inaccurate
        guess on how the ball collided with the object:)"""
 
        # going to simulate actual collision code my checking if the ball is in
        # certain areas of the block/paddle
        cornerwidth = 5
 
        # if the item is the paddle, apply some of its velocity to the ball
        if hasattr(item, 'velocity'):
            self.ball.velx += item.velocity/3.0
 
        # test corners first
        # if the ball hit the top left
        if self.ball.rect.colliderect( pygame.Rect(item.rect.left, item.rect.top, cornerwidth, cornerwidth) ):
            speed = math.hypot(self.ball.velx, self.ball.vely)
            component = speed * .7071 # sqrt2 estimate -- x and y component of 45 degrees
            if self.ball.velx >= 0: # only change x velocity if going right
                self.ball.velx = -component
            if self.ball.vely >= 0: # only change y velocity if going down
                self.ball.vely = -component
            self.ball.rect.bottom = item.rect.top -1    # move out of collision
            return
 
        # if the ball hit the top right
        if self.ball.rect.colliderect( pygame.Rect(item.rect.right, item.rect.top, cornerwidth, cornerwidth) ):
            speed = math.hypot(self.ball.velx, self.ball.vely)
            component = speed * .7071 # sqrt2 estimate -- x and y component of 45 degrees
            if self.ball.velx <= 0: # only change x velocity if going left
                self.ball.velx = component
            if self.ball.vely >= 0: # only change y velocity if going down
                self.ball.vely = -component
            self.ball.rect.bottom = item.rect.top -1    # move out of collision
            return
 
        # if the ball hit the bottom left
        if self.ball.rect.colliderect( pygame.Rect(item.rect.left, item.rect.bottom-cornerwidth, cornerwidth, cornerwidth) ):
            speed = math.hypot(self.ball.velx, self.ball.vely)
            component = speed * .7071 # sqrt2 estimate -- x and y component of 45 degrees
            if self.ball.velx >= 0: # only change x velocity if going right
                self.ball.velx = -component
            if self.ball.vely <= 0: # only change y velocity if going up
                self.ball.vely = component
            self.ball.rect.top  = item.rect.bottom + 1  # move out of collision
            return
 
        # if the ball hit the bottom right
        if self.ball.rect.colliderect( pygame.Rect(item.rect.left, item.rect.bottom-cornerwidth, cornerwidth, cornerwidth) ):
            speed = math.hypot(self.ball.velx, self.ball.vely)
            component = speed * .7071 # sqrt2 estimate -- x and y component of 45 degrees
            if self.ball.velx <= 0: # only change x velocity if going left
                self.ball.velx = component
            if self.ball.vely <= 0: # only change y velocity if going up
                self.ball.vely = component
            self.ball.rect.top = item.rect.bottom + 1   # move out of collision
            return
 
        # didnt hit the corners, let's try the sides
        # if the ball hit the top edge
        if self.ball.rect.colliderect( pygame.Rect(item.rect.left, item.rect.top, item.rect.width, 2) ):
            self.ball.vely *= -1                        # flip y velocity
            self.ball.rect.bottom = item.rect.top - 1   # move out of collision
            return
 
        # if the ball hit the bottom edge
        elif self.ball.rect.colliderect( pygame.Rect(item.rect.left, item.rect.bottom-2, item.rect.width, 2) ):
            self.ball.vely *= -1                        # flip y velocity
            self.ball.rect.top = item.rect.bottom + 1   # move out of collision
            return
 
        # if the ball hit the left side
        if self.ball.rect.collidepoint((item.rect.left, item.rect.centery)):
            self.ball.velx *= -1                        # flip x velocity
            self.ball.rect.right = item.rect.left - 1   # move out of collision
            return
 
        # if the ball hit the right side
        elif self.ball.rect.collidepoint((item.rect.right, item.rect.centery)):
            self.ball.velx *= -1                        # flip x velocity
            self.ball.rect.left = item.rect.right + 1   # move out of collision
            return

Game.reset

The last thing we need is the reset function we called above when the ball goes off the bottom of the board (in other words, the player missed it). All this does is call our Ball and Paddle reset functions (placing them back in the center) and set self.isReset which makes our Ball stay on the Paddle and let’s the user serve it with the spacebar. In a later step of this tutorial this will also be the place where we decrement the user’s lives and, if they have none left, show some sort of ‘Game Over’ feedback.

509
510
511
512
513
514
515
516
    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."""
        self.paddle.reset()
        self.ball.reset()
        self.isReset = True
 
        # todo: decrement player lives here

Result and Download

Now we have a ball that bounces around destroying blocks!

Tutorial Download:
Pygame Tutorial 4 - Breakout (243)