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)