Pygame Tutorial 4 – Breakout: Step 1
Written by Collin Green — Version 1.0.0 — 2010-12-27
Goal:
In this tutorial we will build on what we learned in the pong tutorial. Step 1, as usual, is to just get things set up. We will create our game class and most of our sprite classes, as well as set up a default ‘level’ and place blocks in it. When we are finished we will have a screen with some blocks and a paddle at the bottom that can move.
Planning
Breakout is kind of like pinball: we are trying to keep a ball in play by bouncing it while scoring points and moving to the next level by breaking blocks. So this time around we need a paddle, a ball, some blocks, and a ‘board’ to contain it all. I created some block images that are 50 pixels wide and I decided I wanted the ability to place 10 blocks side by side in each row of a level, requiring 500 pixels of space. I also have 10 pixel wide walls on the sides, making the total screen width 520. I created a simple background image of the correct size (520 x 600) and we will keep the ball and paddle inside the walls with simple code just like in the pong tutorial (hardcoded x and y values, not collision detection). Keep these things in mind as you look at the code.
The Setup
As always, we start with our import code. Just like in the pong tutorial we are going to grab pygame along with math, random, os, and sys. If we need something else down the line, its import will get added here.
11 12 13 14 15 16 17 18 | try: import sys, os, math, random import pygame from pygame.locals import * except ImportError, err: print "%s Failed to Load Module: %s" % (__file__, err) sys.exit(1) |
Paddle
Next is our paddle class. This is copied over from the pong tutorial and modified slightly to fit breakout instead. Notable changes are the rotation of the paddle (we need horizontal in this game, not vertical) and the changes to move and keep it inside the walls horizontally.
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 61 62 63 64 65 66 | class Paddle(pygame.sprite.Sprite): """A paddle sprite. Subclasses the pygame sprite class. Handles its own position so it will not go off the screen.""" def __init__(self, xy): # initialize the pygame sprite part pygame.sprite.Sprite.__init__(self) # set image and rect self.image = pygame.image.load(os.path.join('images','paddle.gif')) self.image = pygame.transform.rotate(self.image, 90) self.rect = self.image.get_rect() # set position self.rect.centerx, self.rect.centery = xy # the movement speed of our paddle self.movementspeed = 5 # the current velocity of the paddle -- can only move in X direction self.velocity = 0 def left(self): """Increases the velocity""" self.velocity -= self.movementspeed def right(self): """Decreases the velocity""" self.velocity += self.movementspeed def move(self, dx): """Move the paddle in the x direction. Don't go past the sides""" if dx != 0: if self.rect.right + dx > 510: self.rect.right = 510 elif self.rect.left + dx < 10: self.rect.left = 10 else: self.rect.x += dx def update(self): """Called to update the sprite. Do this every frame. Handles moving the sprite by its velocity""" self.move(self.velocity) def reset(self): """Moves the paddle to the center of the booard""" self.center = 260, 550 |
The Block Class
This is our block class. I decided to make one block class for all the different blocks instead of using a separate class for each one. This will make it easy and clean to handle blocks losing levels — we can just track the level internally and change the block image as necessary.
You should also notice the images dict — we are going to load the images one time in a different class and pass the dictionary containing their surfaces to each block class, effectively ‘caching’ it to save memory (even though that isn’t an issue with this small of a game).
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | class Block(pygame.sprite.Sprite): """A block sprite. Has a level and a position.""" def __init__(self, xy, images, level=1): pygame.sprite.Sprite.__init__(self) # save images and level self.images = images self.level = level # set image and rect so we can be rendered self.image = self.images[self.level] self.rect = self.image.get_rect() # set initial position self.rect.center = xy |
The Solid Block class
Similar to the Block class except it can only ever have one image and can never be destroyed, which we will handle in the next step of the tutorial.
86 87 88 89 90 91 92 93 94 | class SolidBlock(pygame.sprite.Sprite): """A block that can't be destroyed""" def __init__(self, xy): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', 'block_gray.png')) self.rect = self.image.get_rect() # set position self.rect.center = xy |
The Block Factory Class
This one is pretty interesting. This is the class mentioned in the Block description where we pre-load all the images and pass them to the Blocks. We are going to create one of these in our game class and ask it for a block every time we need one. All it does is create a new block, pass it the dict of images, and return the block so we can use it.
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | class BlockFactory(object): """Using this class to return blocks with a copy of the images already loaded. No sense in re-loaded all the images for every block every time one is created.""" def __init__(self): # load all our block images self.images = { 1: pygame.image.load(os.path.join('images','block_blue.png')), 2: pygame.image.load(os.path.join('images','block_green.png')), 3: pygame.image.load(os.path.join('images','block_red.png')), 4: pygame.image.load(os.path.join('images','block_orange.png')), 5: pygame.image.load(os.path.join('images','block_purple.png')) } def getBlock(self, xy, level=1): return Block(xy, self.images, level) def getSolidBlock(self, xy): return SolidBlock(xy) |
The Game Object
Next is the Game object. This is extremely similar to the one we created in the pong tutorial so I will skip most of it and you can go back and look at pong if you have questions. We set everything up the same way, we add another sprite group to hold and render our blocks (we are keeping them in their own group so we can take advantage of pygame’s collision checking functions later), we use the same buffered input to move the paddle. The only really interesting thing is the loadLevel function which is explaining in detail next.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 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 234 235 236 | class Game(object): """Our game object! This is a fairly simple object that handles the initialization of pygame and sets up our game to run.""" def __init__(self): """Called when the the Game object is initialized. Initializes pygame and sets up our pygame window and other pygame tools that we will need for more complicated tutorials.""" # load and set up pygame pygame.init() # create our window self.window = pygame.display.set_mode((520, 600)) # clock for ticking self.clock = pygame.time.Clock() # set the window title pygame.display.set_caption("Pygame Tutorial 4 - Breakout") # tell pygame to only pay attention to certain events # we want to know if the user hits the X on the window, and we # want keys so we can close the window with the esc key pygame.event.set_allowed([QUIT, KEYDOWN, KEYUP]) # make background self.background = pygame.image.load(os.path.join('images','background.jpg')) # blit the background onto the window self.window.blit(self.background, (0,0)) # flip the display so the background is on there pygame.display.flip() # a sprite rendering group for our ball and paddle self.sprites = pygame.sprite.RenderUpdates() # create our paddle and add to sprite group self.paddle = Paddle((260,550)) self.sprites.add(self.paddle) # create sprite group for blocks self.blocks = pygame.sprite.RenderUpdates() # create our blockfactory object self.blockfactory = BlockFactory() # load the first level self.loadLevel() 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()) # update our sprites for sprite in self.sprites: sprite.update() # 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' def handleEvents(self): """Poll for PyGame events and behave accordingly. Return false to stop the event loop and end the game.""" # poll for pygame events for event in pygame.event.get(): if event.type == QUIT: return False # handle user input elif event.type == KEYDOWN: # if the user presses escape, quit the event loop. if event.key == K_ESCAPE: return False # paddle control if event.key == K_a or event.key == K_LEFT: self.paddle.left() if event.key == K_d or event.key == K_RIGHT: self.paddle.right() # serve with space if the ball isn't moving if event.key == K_SPACE: pass elif event.type == KEYUP: # paddle control if event.key == K_a or event.key == K_LEFT: self.paddle.right() if event.key == K_d or event.key == K_RIGHT: self.paddle.left() return True |
Game.loadLevel
At this stage, this just creates and loads a default level of blocks. I decided each level can have up to 5 rows and 10 columns for a total of 50 blocks per level. We are going to represent levels with simple 2D array where the array values represent the level of the block at that position. This gives us a coordinate system to access our level data (level[rownumber][columnnumber]), making it very easy to process it programatically while still being able to understand it by looking at the code. Later we will create a level editor so we don’t need to edit it by hand, but for now it makes our lives a little easier.
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 | def loadLevel(self): """Loads a level. Places blocks on the board and adds them to the blocks render group""" level = [ [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] ] # 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))) |
Wrapping Up
The last thing we need is the code to create the Game object and run it when a user runs the script.
279 280 281 282 | # create a game and run it if __name__ == '__main__': game = Game() game.run() |
Result and Download
Here is our board, our default level with some blocks of varying levels, and our paddle.
Tutorial Download:
Pygame Tutorial 4 - Breakout (223)
