The previous four chapters went over the pygame module and demonstrated how to use its many features. In this chapter, we’ll use that knowledge to create a graphical game called Dodger.
In the Dodger game, the player controls a sprite (the player’s character) who must dodge a whole bunch of baddies that fall from the top of the screen. The longer the player can keep dodging the baddies, the higher their score will get.
Just for fun, we’ll also add some cheat modes to this game. If the player holds down the X key, every baddie’s speed is reduced to a super slow rate. If the player holds down the Z key, the baddies will reverse their direction and travel up the screen instead of down.
Before we start making Dodger, let’s review some of the basic data types used in pygame:
pygame.Rect
Rect objects represent a rectangular space’s location and size. The location is determined by the Rect object’s topleft attribute (or the topright, bottomleft, and bottomright attributes). These corner attributes are a tuple of integers for the x- and y-coordinates. The size is determined by the width and height attributes, which are integers indicating how many pixels long or high the rectangle is. Rect objects have a colliderect() method that checks whether they are colliding with another Rect object.
pygame.Surface
Surface objects are areas of colored pixels. A Surface object represents a rectangular image, while a Rect object represents only a rectangular space and location. Surface objects have a blit() method that is used to draw the image on one Surface object onto another Surface object. The Surface object returned by the pygame.display.set_mode() function is special because anything drawn on that Surface object is displayed on the user’s screen when pygame.display.update() is called.
pygame.event.Event
The pygame.event module generates Event objects whenever the user provides keyboard, mouse, or other input. The pygame.event.get() function returns a list of these Event objects. You can determine the type of the Event object by checking its type attribute. QUIT, KEYDOWN, and MOUSEBUTTONUP are examples of some event types. (See “Handling Events” on page 292 for a complete list of all the event types.)
pygame.font.Font
The pygame.font module uses the Font data type, which represents the typeface used for text in pygame. The arguments to pass to pygame.font. SysFont() are a string of the font name (it’s common to pass None for the font name to get the default system font) and an integer of the font size.
pygame.time.Clock
The Clock object in the pygame.time module is helpful for keeping our games from running faster than the player can see. The Clock object has a tick() method, which can be passed the number of frames per second (FPS) we want the game to run. The higher the FPS, the faster the game runs.
When you run this program, the game will look like Figure 21-1.
Figure 21-1: A screenshot of the Dodger game
Enter the following code in a new file and save it as dodger.py. You can download the code, image, and sound files from https://www.nostarch.com/inventwithpython/. Place the image and sound files in the same folder as dodger.py.
If you get errors after entering this code, compare the code you typed to the book’s code with the online diff tool at https://www.nostarch.com/inventwithpython#diff.
dodger.py
1. import pygame, random, sys
2. from pygame.locals import *
3.
4. WINDOWWIDTH = 600
5. WINDOWHEIGHT = 600
6. TEXTCOLOR = (0, 0, 0)
7. BACKGROUNDCOLOR = (255, 255, 255)
8. FPS = 60
9. BADDIEMINSIZE = 10
10. BADDIEMAXSIZE = 40
11. BADDIEMINSPEED = 1
12. BADDIEMAXSPEED = 8
13. ADDNEWBADDIERATE = 6
14. PLAYERMOVERATE = 5
15.
16. def terminate():
17. pygame.quit()
18. sys.exit()
19.
20. def waitForPlayerToPressKey():
21. while True:
22. for event in pygame.event.get():
23. if event.type == QUIT:
24. terminate()
25. if event.type == KEYDOWN:
26. if event.key == K_ESCAPE: # Pressing ESC quits.
27. terminate()
28. return
29.
30. def playerHasHitBaddie(playerRect, baddies):
31. for b in baddies:
32. if playerRect.colliderect(b['rect']):
33. return True
34. return False
35.
36. def drawText(text, font, surface, x, y):
37. textobj = font.render(text, 1, TEXTCOLOR)
38. textrect = textobj.get_rect()
39. textrect.topleft = (x, y)
40. surface.blit(textobj, textrect)
41.
42. # Set up pygame, the window, and the mouse cursor.
43. pygame.init()
44. mainClock = pygame.time.Clock()
45. windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
46. pygame.display.set_caption('Dodger')
47. pygame.mouse.set_visible(False)
48.
49. # Set up the fonts.
50. font = pygame.font.SysFont(None, 48)
51.
52. # Set up sounds.
53. gameOverSound = pygame.mixer.Sound('gameover.wav')
54. pygame.mixer.music.load('background.mid')
55.
56. # Set up images.
57. playerImage = pygame.image.load('player.png')
58. playerRect = playerImage.get_rect()
59. baddieImage = pygame.image.load('baddie.png')
60.
61. # Show the "Start" screen.
62. windowSurface.fill(BACKGROUNDCOLOR)
63. drawText('Dodger', font, windowSurface, (WINDOWWIDTH / 3),
(WINDOWHEIGHT / 3))
64. drawText('Press a key to start.', font, windowSurface,
(WINDOWWIDTH / 3) - 30, (WINDOWHEIGHT / 3) + 50)
65. pygame.display.update()
66. waitForPlayerToPressKey()
67.
68. topScore = 0
69. while True:
70. # Set up the start of the game.
71. baddies = []
72. score = 0
73. playerRect.topleft = (WINDOWWIDTH / 2, WINDOWHEIGHT - 50)
74. moveLeft = moveRight = moveUp = moveDown = False
75. reverseCheat = slowCheat = False
76. baddieAddCounter = 0
77. pygame.mixer.music.play(-1, 0.0)
78.
79. while True: # The game loop runs while the game part is playing.
80. score += 1 # Increase score.
81.
82. for event in pygame.event.get():
83. if event.type == QUIT:
84. terminate()
85.
86. if event.type == KEYDOWN:
87. if event.key == K_z:
88. reverseCheat = True
89. if event.key == K_x:
90. slowCheat = True
91. if event.key == K_LEFT or event.key == K_a:
92. moveRight = False
93. moveLeft = True
94. if event.key == K_RIGHT or event.key == K_d:
95. moveLeft = False
96. moveRight = True
97. if event.key == K_UP or event.key == K_w:
98. moveDown = False
99. moveUp = True
100. if event.key == K_DOWN or event.key == K_s:
101. moveUp = False
102. moveDown = True
103.
104. if event.type == KEYUP:
105. if event.key == K_z:
106. reverseCheat = False
107. score = 0
108. if event.key == K_x:
109. slowCheat = False
110. score = 0
111. if event.key == K_ESCAPE:
112. terminate()
113.
114. if event.key == K_LEFT or event.key == K_a:
115. moveLeft = False
116. if event.key == K_RIGHT or event.key == K_d:
117. moveRight = False
118. if event.key == K_UP or event.key == K_w:
119. moveUp = False
120. if event.key == K_DOWN or event.key == K_s:
121. moveDown = False
122.
123. if event.type == MOUSEMOTION:
124. # If the mouse moves, move the player to the cursor.
125. playerRect.centerx = event.pos[0]
126. playerRect.centery = event.pos[1]
127. # Add new baddies at the top of the screen, if needed.
128. if not reverseCheat and not slowCheat:
129. baddieAddCounter += 1
130. if baddieAddCounter == ADDNEWBADDIERATE:
131. baddieAddCounter = 0
132. baddieSize = random.randint(BADDIEMINSIZE, BADDIEMAXSIZE)
133. newBaddie = {'rect': pygame.Rect(random.randint(0,
WINDOWWIDTH - baddieSize), 0 - baddieSize,
baddieSize, baddieSize),
134. 'speed': random.randint(BADDIEMINSPEED,
BADDIEMAXSPEED),
135. 'surface':pygame.transform.scale(baddieImage,
(baddieSize, baddieSize)),
136. }
137.
138. baddies.append(newBaddie)
139.
140. # Move the player around.
141. if moveLeft and playerRect.left > 0:
142. playerRect.move_ip(-1 * PLAYERMOVERATE, 0)
143. if moveRight and playerRect.right < WINDOWWIDTH:
144. playerRect.move_ip(PLAYERMOVERATE, 0)
145. if moveUp and playerRect.top > 0:
146. playerRect.move_ip(0, -1 * PLAYERMOVERATE)
147. if moveDown and playerRect.bottom < WINDOWHEIGHT:
148. playerRect.move_ip(0, PLAYERMOVERATE)
149.
150. # Move the baddies down.
151. for b in baddies:
152. if not reverseCheat and not slowCheat:
153. b['rect'].move_ip(0, b['speed'])
154. elif reverseCheat:
155. b['rect'].move_ip(0, -5)
156. elif slowCheat:
157. b['rect'].move_ip(0, 1)
158.
159. # Delete baddies that have fallen past the bottom.
160. for b in baddies[:]:
161. if b['rect'].top > WINDOWHEIGHT:
162. baddies.remove(b)
163.
164. # Draw the game world on the window.
165. windowSurface.fill(BACKGROUNDCOLOR)
166.
167. # Draw the score and top score.
168. drawText('Score: %s' % (score), font, windowSurface, 10, 0)
169. drawText('Top Score: %s' % (topScore), font, windowSurface,
10, 40)
170.
171. # Draw the player's rectangle.
172. windowSurface.blit(playerImage, playerRect)
173.
174. # Draw each baddie.
175. for b in baddies:
176. windowSurface.blit(b['surface'], b['rect'])
177.
178. pygame.display.update()
179.
180. # Check if any of the baddies have hit the player.
181. if playerHasHitBaddie(playerRect, baddies):
182. if score > topScore:
183. topScore = score # Set new top score.
184. break
185.
186. mainClock.tick(FPS)
187.
188. # Stop the game and show the "Game Over" screen.
189. pygame.mixer.music.stop()
190. gameOverSound.play()
191.
192. drawText('GAME OVER', font, windowSurface, (WINDOWWIDTH / 3),
(WINDOWHEIGHT / 3))
193. drawText('Press a key to play again.', font, windowSurface,
(WINDOWWIDTH / 3) - 80, (WINDOWHEIGHT / 3) + 50)
194. pygame.display.update()
195. waitForPlayerToPressKey()
196.
197. gameOverSound.stop()
The Dodger game imports the same modules as did the previous pygame programs: pygame, random, sys, and pygame.locals.
1. import pygame, random, sys
2. from pygame.locals import *
The pygame.locals module contains several constant variables that pygame uses, such as the event types (QUIT, KEYDOWN, and so on) and keyboard keys (K_ESCAPE, K_LEFT, and so on). By using the from pygame.locals import * syntax, you can just use QUIT in the source code instead of pygame.locals.QUIT.
Lines 4 to 7 set up constants for the window dimensions, the text color, and the background color:
4. WINDOWWIDTH = 600
5. WINDOWHEIGHT = 600
6. TEXTCOLOR = (0, 0, 0)
7. BACKGROUNDCOLOR = (255, 255, 255)
We use constant variables because they are much more descriptive than if we had typed out the values. For example, the line windowSurface.fill(BACKGROUNDCOLOR) is more understandable than windowSurface.fill((255, 255, 255)).
You can easily change the game by changing the constant variables. By changing WINDOWWIDTH on line 4, you automatically change the code everywhere WINDOWWIDTH is used. If you had used the value 600 instead, you would have to change each occurrence of 600 in the code. It’s easier to change the value in the constant once.
On line 8, you set the constant for the FPS, the number of frames per second you want the game to run:
8. FPS = 60
A frame is a screen that’s drawn for a single iteration through the game loop. You pass FPS to the mainClock.tick() method on line 186 so that the function knows how long to pause the program. Here FPS is set to 60, but you can change FPS to a higher value to have the game run faster or to a lower value to slow it down.
Lines 9 to 13 set some more constant variables for the falling baddies:
9. BADDIEMINSIZE = 10
10. BADDIEMAXSIZE = 40
11. BADDIEMINSPEED = 1
12. BADDIEMAXSPEED = 8
13. ADDNEWBADDIERATE = 6
The width and height of the baddies will be between BADDIEMINSIZE and BADDIEMAXSIZE. The rate at which the baddies fall down the screen will be between BADDIEMINSPEED and BADDIEMAXSPEED pixels per iteration through the game loop. And a new baddie will be added to the top of the window every ADDNEWBADDIERATE iterations through the game loop.
Finally, the PLAYERMOVERATE stores the number of pixels the player’s character moves in the window on each iteration through the game loop (if the character is moving):
14. PLAYERMOVERATE = 5
By increasing this number, you can increase the speed at which the character moves.
There are several functions you’ll create for this game. The terminate() and waitForPlayerToPressKey() functions will end and pause the game, respectively, the playerHasHitBaddie() function will track the player’s collisions with baddies, and the drawText() function will draw the score and other text to the screen.
The pygame module requires that you call both pygame.quit() and sys.exit() to end the game. Lines 16 to 18 put them both into a function called terminate().
16. def terminate():
17. pygame.quit()
18. sys.exit()
Now you only need to call terminate() instead of both pygame.quit() and sys.exit().
Sometimes you’ll want to pause the program until the player presses a key, such as at the very start of the game when the Dodger title text appears or at the end when Game Over shows. Lines 20 to 24 create a new function called waitForPlayerToPressKey():
20. def waitForPlayerToPressKey():
21. while True:
22. for event in pygame.event.get():
23. if event.type == QUIT:
24. terminate()
Inside this function, there’s an infinite loop that breaks only when a KEYDOWN or QUIT event is received. At the start of the loop, pygame.event.get() returns a list of Event objects to check out.
If the player has closed the window while the program is waiting for the player to press a key, pygame will generate a QUIT event, which you check for in line 23 with event.type. If the player has quit, Python calls the terminate() function on line 24.
If the game receives a KEYDOWN event, it should first check whether ESC was pressed:
25. if event.type == KEYDOWN:
26. if event.key == K_ESCAPE: # Pressing ESC quits.
27. terminate()
28. return
If the player pressed ESC, the program should terminate. If that wasn’t the case, then execution will skip the if block on line 27 and go straight to the return statement, which exits the waitForPlayerToPressKey() function.
If a QUIT or KEYDOWN event isn’t generated, the code keeps looping. Since the loop does nothing, this will make it look like the game has frozen until the player presses a key.
The playerHasHitBaddie() function will return True if the player’s character has collided with one of the baddies:
30. def playerHasHitBaddie(playerRect, baddies):
31. for b in baddies:
32. if playerRect.colliderect(b['rect']):
33. return True
34. return False
The baddies parameter is a list of baddie dictionary data structures. Each of these dictionaries has a 'rect' key, and the value for that key is a Rect object that represents the baddie’s size and location.
playerRect is also a Rect object. Rect objects have a method named colliderect() that returns True if the Rect object has collided with the Rect object that is passed to it. Otherwise, colliderect() returns False.
The for loop on line 31 iterates through each baddie dictionary in the baddies list. If any of these baddies collides with the player’s character, then playerHasHitBaddie() returns True. If the code manages to iterate through all the baddies in the baddies list without detecting a collision, playerHasHitBaddie() returns False.
Drawing text on the window involves a few steps, which we accomplish with drawText(). This way, there’s only one function to call when we want to display the player’s score or the Game Over text on the screen.
36. def drawText(text, font, surface, x, y):
37. textobj = font.render(text, 1, TEXTCOLOR)
38. textrect = textobj.get_rect()
39. textrect.topleft = (x, y)
40. surface.blit(textobj, textrect)
First, the render() method call on line 37 creates a Surface object that renders the text in a specific font.
Next, you need to know the size and location of the Surface object. You can get a Rect object with this information using the get_rect() Surface method.
The Rect object returned from get_rect() on line 38 has a copy of the width and height information from the Surface object. Line 39 changes the location of the Rect object by setting a new tuple value for its topleft attribute.
Finally, line 40 draws the Surface object of the rendered text onto the Surface object that was passed to the drawText() function. Displaying text in pygame takes a few more steps than simply calling the print() function. But if you put this code into a single function named drawText(), then you only need to call this function to display text on the screen.
Now that the constant variables and functions are finished, we’ll start calling the pygame functions that set up the window and clock:
42. # Set up pygame, the window, and the mouse cursor.
43. pygame.init()
44. mainClock = pygame.time.Clock()
Line 43 sets up pygame by calling the pygame.init() function. Line 44 creates a pygame.time.Clock() object and stores it in the mainClock variable. This object will help us keep the program from running too fast.
Line 45 creates a new Surface object that is used for the window display:
45. windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
Notice that there’s only one argument passed to pygame.display.set_mode(): a tuple. The arguments for pygame.display.set_mode() are not two integers but one tuple of two integers. You can specify the width and height of this Surface object (and the window) by passing a tuple with the WINDOWWIDTH and WINDOWHEIGHT constant variables.
The pygame.display.set_mode() function has a second, optional parameter. You can pass the pygame.FULLSCREEN constant to make the window fill the entire screen. Look at this modification to line 45:
45. windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT),
pygame.FULLSCREEN)
The parameters WINDOWWIDTH and WINDOWHEIGHT are still passed for the window’s width and height, but the image will be stretched larger to fit the screen. Try running the program with and without fullscreen mode.
Line 46 sets the caption of the window to the string 'Dodger':
46. pygame.display.set_caption('Dodger')
This caption will appear in the title bar at the top of the window.
In Dodger, the mouse cursor shouldn’t be visible. You want the mouse to be able to move the player’s character around the screen, but the mouse cursor would get in the way of the character’s image. We can make the mouse invisible with just one line of code:
47. pygame.mouse.set_visible(False)
Calling pygame.mouse.set_visible(False) tells pygame to make the cursor invisible.
Since we are displaying text on the screen in this program, we need to give the pygame module a Font object to use for the text. Line 50 creates a Font object by calling pygame.font.SysFont():
49. # Set up the fonts.
50. font = pygame.font.SysFont(None, 48)
Passing None uses the default font. Passing 48 gives the font a size of 48 points.
Next, we’ll create the Sound objects and set up the background music:
52. # Set up sounds.
53. gameOverSound = pygame.mixer.Sound('gameover.wav')
54. pygame.mixer.music.load('background.mid')
The pygame.mixer.Sound() constructor function creates a new Sound object and stores a reference to this object in the gameOverSound variable. In your own games, you can create as many Sound objects as you like, each with a different sound file.
The pygame.mixer.music.load() function loads a sound file to play for the background music. This function doesn’t return any objects, and only one background sound file can be loaded at a time. The background music will play constantly during the game, but Sound objects will play only when the player loses the game by running into a baddie.
You can use any WAV or MIDI file for this game. Some sound files are available from this book’s website at https://www.nostarch.com/inventwithpython/. You can also use your own sound files for this game, as long as you name the files gameover.wav and background.mid or change the strings used on lines 53 and 54 to match the filename you want.
Next you’ll load the image files to be used for the player’s character and the baddies:
56. # Set up images.
57. playerImage = pygame.image.load('player.png')
58. playerRect = playerImage.get_rect()
59. baddieImage = pygame.image.load('baddie.png')
The image for the character is stored in player.png, and the image for the baddies is stored in baddie.png. All the baddies look the same, so you need only one image file for them. You can download these images from this book’s website at https://www.nostarch.com/inventwithpython/.
When the game first starts, Python should display the Dodger title on the screen. You also want to tell the player that they can start the game by pushing any key. This screen appears so that the player has time to get ready to start playing after running the program.
On lines 63 and 64, we write code to call the drawText() function:
61. # Show the "Start" screen.
62. windowSurface.fill(BACKGROUNDCOLOR)
63. drawText('Dodger', font, windowSurface, (WINDOWWIDTH / 3),
(WINDOWHEIGHT / 3))
64. drawText('Press a key to start.', font, windowSurface,
(WINDOWWIDTH / 3) - 30, (WINDOWHEIGHT / 3) + 50)
65. pygame.display.update()
66. waitForPlayerToPressKey()
We pass this function five arguments:
The string of the text you want to appear
The font in which you want the string to appear
The Surface object onto which the text will be rendered
The x-coordinate on the Surface object at which to draw the text
The y-coordinate on the Surface object at which to draw the text
This may seem like a lot of arguments to pass for a function call, but keep in mind that this function call replaces five lines of code each time you call it. This shortens the program and makes it easier to find bugs since there’s less code to check.
The waitForPlayerToPressKey() function pauses the game by looping until a KEYDOWN event is generated. Then the execution breaks out of the loop and the program continues to run.
With all the functions now defined, we can start writing the main game code. Lines 68 and on will call the functions that we defined earlier. The value in the topScore variable starts at 0 when the program first runs. Whenever the player loses and has a score larger than the current top score, the top score is replaced with this larger score.
68. topScore = 0
69. while True:
The infinite loop started on line 69 is technically not the game loop. The game loop handles events and drawing the window while the game is running. Instead, this while loop iterates each time the player starts a new game. When the player loses and the game resets, the program’s execution loops back to line 69.
At the beginning, you also want to set baddies to an empty list:
70. # Set up the start of the game.
71. baddies = []
72. score = 0
The baddies variable is a list of dictionary objects with the following keys:
'rect' The Rect object that describes where and what size the baddie is.
'speed' How fast the baddie falls down the screen. This integer represents pixels per iteration through the game loop.
'surface' The Surface object that has the scaled baddie image drawn on it. This is the Surface that is drawn to the Surface object returned by pygame.display.set_mode().
Line 72 resets the player’s score to 0.
The starting location of the player is in the center of the screen and 50 pixels up from the bottom, which is set by line 73:
73. playerRect.topleft = (WINDOWWIDTH / 2, WINDOWHEIGHT - 50)
The first item in line 73’s tuple is the x-coordinate of the left edge, and the second item is the y-coordinate of the top edge.
Next we set up variables for the player movements and the cheats:
74. moveLeft = moveRight = moveUp = moveDown = False
75. reverseCheat = slowCheat = False
76. baddieAddCounter = 0
The movement variables moveLeft, moveRight, moveUp, and moveDown are set to False. The reverseCheat and slowCheat variables are also set to False. They will be set to True only when the player enables these cheats by holding down the Z and X keys, respectively.
The baddieAddCounter variable is a counter to tell the program when to add a new baddie at the top of the screen. The value in baddieAddCounter increments by 1 each time the game loop iterates. (This is similar to the code in “Adding New Food Squares” on page 295.)
When baddieAddCounter is equal to ADDNEWBADDIERATE, then baddieAddCounter resets to 0 and a new baddie is added to the top of the screen. (This check is done later on line 130.)
The background music starts playing on line 77 with a call to the pygame.mixer.music.play() function:
77. pygame.mixer.music.play(-1, 0.0)
Because the first argument is -1, pygame repeats the music endlessly. The second argument is a float that says how many seconds into the music you want it to start playing. Passing 0.0 means the music starts playing from the beginning.
The game loop’s code constantly updates the state of the game world by changing the position of the player and baddies, handling events generated by pygame, and drawing the game world on the screen. All of this happens several dozen times a second, which makes the game run in real time.
Line 79 is the start of the main game loop:
79. while True: # The game loop runs while the game part is playing.
80. score += 1 # Increase score.
Line 80 increases the player’s score on each iteration of the game loop. The longer the player can go without losing, the higher their score. The loop will exit only when the player either loses the game or quits the program.
There are four types of events the program will handle: QUIT, KEYDOWN, KEYUP, and MOUSEMOTION.
Line 82 is the start of the event-handling code:
82. for event in pygame.event.get():
83. if event.type == QUIT:
84. terminate()
It calls pygame.event.get(), which returns a list of Event objects. Each Event object represents an event that has happened since the last call to pygame.event.get(). The code checks the type attribute of the Event object to see what type of event it is, and then handles it accordingly.
If the type attribute of the Event object is equal to QUIT, then the user has closed the program. The QUIT constant variable was imported from the pygame.locals module.
If the event’s type is KEYDOWN, the player has pressed a key:
86. if event.type == KEYDOWN:
87. if event.key == K_z:
88. reverseCheat = True
89. if event.key == K_x:
90. slowCheat = True
Line 87 checks whether the event describes the Z key being pressed with event.key == K_z. If this condition is True, Python sets the reverseCheat variable to True to activate the reverse cheat. Similarly, line 89 checks whether the X key has been pressed to activate the slow cheat.
Lines 91 to 102 check whether the event was generated by the player pressing one of the arrow or WASD keys. This code is similar to the keyboard-related code in the previous chapters.
If the event’s type is KEYUP, the player has released a key:
104. if event.type == KEYUP:
105. if event.key == K_z:
106. reverseCheat = False
107. score = 0
108. if event.key == K_x:
109. slowCheat = False
110. score = 0
Line 105 checks whether the player has released the Z key, which will deactivate the reverse cheat. In that case, line 106 sets reverseCheat to False, and line 107 resets the score to 0. The score reset is to discourage the player from using the cheats.
Lines 108 to 110 do the same thing for the X key and the slow cheat. When the X key is released, slowCheat is set to False, and the player’s score is reset to 0.
At any time during the game, the player can press ESC to quit:
111. if event.key == K_ESCAPE:
112. terminate()
Line 111 determines whether the key that was released was ESC by checking event.key == K_ESCAPE. If so, line 112 calls the terminate() function to exit the program.
Lines 114 to 121 check whether the player has stopped holding down one of the arrow or WASD keys. In that case, the code sets the corresponding movement variable to False. This is similar to the movement code in Chapter 19’s and Chapter 20’s programs.
Now that you’ve handled the keyboard events, let’s handle any mouse events that may have been generated. The Dodger game doesn’t do anything if the player has clicked a mouse button, but it does respond when the player moves the mouse. This gives the player two ways of controlling the character in the game: the keyboard or the mouse.
The MOUSEMOTION event is generated whenever the mouse is moved:
123. if event.type == MOUSEMOTION:
124. # If the mouse moves, move the player to the cursor.
125. playerRect.centerx = event.pos[0]
126. playerRect.centery = event.pos[1]
Event objects with a type set to MOUSEMOTION also have an attribute named pos for the position of the mouse event. The pos attribute stores a tuple of the x- and y-coordinates of where the mouse cursor moved in the window. If the event’s type is MOUSEMOTION, the player’s character moves to the position of the mouse cursor.
Lines 125 and 126 set the center x- and y-coordinate of the player’s character to the x- and y-coordinates of the mouse cursor.
On each iteration of the game loop, the code increments the baddieAddCounter variable by one:
127. # Add new baddies at the top of the screen, if needed.
128. if not reverseCheat and not slowCheat:
129. baddieAddCounter += 1
This happens only if the cheats are not enabled. Remember that reverseCheat and slowCheat are set to True as long as the Z and X keys are being held down, respectively. While the Z and X keys are being held down, baddieAddCounter isn’t incremented. Therefore, no new baddies will appear at the top of the screen.
When the baddieAddCounter reaches the value in ADDNEWBADDIERATE, it’s time to add a new baddie to the top of the screen. First, baddieAddCounter is reset to 0:
130. if baddieAddCounter == ADDNEWBADDIERATE:
131. baddieAddCounter = 0
132. baddieSize = random.randint(BADDIEMINSIZE, BADDIEMAXSIZE)
133. newBaddie = {'rect': pygame.Rect(random.randint(0,
WINDOWWIDTH - baddieSize), 0 - baddieSize,
baddieSize, baddieSize),
134. 'speed': random.randint(BADDIEMINSPEED,
BADDIEMAXSPEED),
135. 'surface':pygame.transform.scale(baddieImage,
(baddieSize, baddieSize)),
136. }
Line 132 generates a size for the baddie in pixels. The size will be a random integer between BADDIEMINSIZE and BADDIEMAXSIZE, which are constants set to 10 and 40 on lines 9 and 10, respectively.
Line 133 is where a new baddie data structure is created. Remember, the data structure for baddies is simply a dictionary with keys 'rect', 'speed', and 'surface'. The 'rect' key holds a reference to a Rect object that stores the location and size of the baddie. The call to the pygame.Rect() constructor function has four parameters: the x-coordinate of the top edge of the area, the y-coordinate of the left edge of the area, the width in pixels, and the height in pixels.
The baddie needs to appear at a random point along the top of the window, so pass random.randint(0, WINDOWWIDTH - baddieSize) for the x-coordinate of the left edge of the baddie. The reason you pass WINDOWWIDTH - baddieSize instead of WINDOWWIDTH is that if the left edge of the baddie is too far to the right, then part of the baddie will be off the edge of the window and not visible onscreen.
The bottom edge of the baddie should be just above the top edge of the window. The y-coordinate of the top edge of the window is 0. To put the baddie’s bottom edge there, set the top edge to 0 - baddieSize.
The baddie’s width and height should be the same (the image is a square), so pass baddieSize for the third and fourth arguments.
The speed at which the baddie moves down the screen is set in the 'speed' key. Set it to a random integer between BADDIEMINSPEED and BADDIEMAXSPEED.
Line 138 will then add the newly created baddie data structure to the list of baddie data structures:
138. baddies.append(newBaddie)
The program uses this list to check whether the player has collided with any of the baddies and to determine where to draw baddies on the window.
The four movement variables moveLeft, moveRight, moveUp, and moveDown are set to True and False when pygame generates the KEYDOWN and KEYUP events, respectively.
If the player’s character is moving left and the left edge of the player’s character is greater than 0 (which is the left edge of the window), then playerRect should move to the left:
140. # Move the player around.
141. if moveLeft and playerRect.left > 0:
142. playerRect.move_ip(-1 * PLAYERMOVERATE, 0)
The move_ip() method will move the location of the Rect object horizontally or vertically by a number of pixels. The first argument to move_ip() is how many pixels to move the Rect object to the right (to move it to the left, pass a negative integer). The second argument is how many pixels to move the Rect object down (to move it up, pass a negative integer). For example, playerRect.move_ip(10, 20) would move the Rect object 10 pixels to the right and 20 pixels down and playerRect.move_ip(-5, -15) would move the Rect object 5 pixels to the left and 15 pixels up.
The ip at the end of move_ip() stands for “in place.” This is because the method changes the Rect object itself, rather than returning a new Rect object with the changes. There is also a move() method, which doesn’t change the Rect object but instead creates and returns a new Rect object in the new location.
You’ll always move the playerRect object by the number of pixels in PLAYERMOVERATE. To get the negative form of an integer, multiply it by -1. On line 142, since 5 is stored in PLAYERMOVERATE, the expression -1 * PLAYERMOVERATE evaluates to -5. Therefore, calling playerRect.move_ip(-1 * PLAYERMOVERATE, 0) will change the location of playerRect by 5 pixels to the left of its current location.
Lines 143 to 148 do the same thing for the other three directions: right, up, and down.
143. if moveRight and playerRect.right < WINDOWWIDTH:
144. playerRect.move_ip(PLAYERMOVERATE, 0)
145. if moveUp and playerRect.top > 0:
146. playerRect.move_ip(0, -1 * PLAYERMOVERATE)
147. if moveDown and playerRect.bottom < WINDOWHEIGHT:
148. playerRect.move_ip(0, PLAYERMOVERATE)
Each of the three if statements in lines 143 to 148 checks that its movement variable is set to True and that the edge of the Rect object of the player is inside the window. Then it calls move_ip() to move the Rect object.
Now the code loops through each baddie data structure in the baddies list to move them down a little:
150. # Move the baddies down.
151. for b in baddies:
152. if not reverseCheat and not slowCheat:
153. b['rect'].move_ip(0, b['speed'])
If neither of the cheats has been activated, then the baddie’s location moves down a number of pixels equal to its speed (stored in the 'speed' key).
If the reverse cheat is activated, then the baddie should move up by 5 pixels:
154. elif reverseCheat:
155. b['rect'].move_ip(0, -5)
Passing -5 for the second argument to move_ip() will move the Rect object upward by 5 pixels.
If the slow cheat has been activated, then the baddie should still move downward, but at the slow speed of 1 pixel per iteration through the game loop:
156. elif slowCheat:
157. b['rect'].move_ip(0, 1)
The baddie’s normal speed (again, this is stored in the 'speed' key of the baddie’s data structure) is ignored when the slow cheat is activated.
Any baddies that fall below the bottom edge of the window should be removed from the baddies list. Remember that you shouldn’t add or remove list items while also iterating through the list. Instead of iterating through the baddies list with the for loop, iterate through a copy of the baddies list. To make this copy, use the blank slicing operator [:]:
159. # Delete baddies that have fallen past the bottom.
160. for b in baddies[:]:
The for loop on line 160 uses the variable b for the current item in the iteration through baddies[:]. If the baddie is below the bottom edge of the window, we should remove it, which we do on line 162:
161. if b['rect'].top > WINDOWHEIGHT:
162. baddies.remove(b)
The b dictionary is the current baddie data structure from the baddies[:] list. Each baddie data structure in the list is a dictionary with a 'rect' key, which stores a Rect object. So b['rect'] is the Rect object for the baddie. Finally, the top attribute is the y-coordinate of the top edge of the rectangular area. Remember that the y-coordinates increase going down. So b['rect'].top > WINDOWHEIGHT will check whether the top edge of the baddie is below the bottom of the window. If this condition is True, then line 162 removes the baddie data structure from the baddies list.
After all the data structures have been updated, the game world should be drawn using pygame’s image functions. Because the game loop is executed several times a second, when the baddies and player are drawn in new positions, they look like they’re moving smoothly.
Before anything else is drawn, line 165 fills the entire screen to erase anything drawn on it previously:
164. # Draw the game world on the window.
165. windowSurface.fill(BACKGROUNDCOLOR)
Remember that the Surface object in windowSurface is special because it is the one returned by pygame.display.set_mode(). Therefore, anything drawn on that Surface object will appear on the screen after pygame.display.update() is called.
Lines 168 and 169 render the text for the current score and top score to the top-left corner of the window.
167. # Draw the score and top score.
168. drawText('Score: %s' % (score), font, windowSurface, 10, 0)
169. drawText('Top Score: %s' % (topScore), font, windowSurface,
10, 40)
The 'Score: %s' % (score) expression uses string interpolation to insert the value in the score variable into the string. This string, the Font object stored in the font variable, the Surface object to draw the text on, and the x- and y-coordinates of where the text should be placed are passed to the drawText() method, which will handle the call to the render() and blit() methods.
For the top score, do the same thing. Pass 40 for the y-coordinate instead of 0 so that the top score’s text appears beneath the current score’s text.
Information about the player is kept in two different variables. playerImage is a Surface object that contains all the colored pixels that make up the player character’s image. playerRect is a Rect object that stores the size and location of the player’s character.
The blit() method draws the player character’s image (in playerImage) on windowSurface at the location in playerRect:
171. # Draw the player's rectangle.
172. windowSurface.blit(playerImage, playerRect)
Line 175’s for loop draws every baddie on the windowSurface object:
174. # Draw each baddie.
175. for b in baddies:
176. windowSurface.blit(b['surface'], b['rect'])
Each item in the baddies list is a dictionary. The dictionaries’ 'surface' and 'rect' keys contain the Surface object with the baddie image and the Rect object with the position and size information, respectively.
Now that everything has been drawn to windowSurface, we need to update the screen so the player can see what’s there:
178. pygame.display.update()
Draw this Surface object to the screen by calling update().
Line 181 checks whether the player has collided with any baddies by calling playerHasHitBaddie(). This function will return True if the player’s character has collided with any of the baddies in the baddies list. Otherwise, the function returns False.
180. # Check if any of the baddies have hit the player.
181. if playerHasHitBaddie(playerRect, baddies):
182. if score > topScore:
183. topScore = score # Set new top score.
184. break
If the player’s character has hit a baddie and if the current score is higher than the top score, then lines 182 and 183 update the top score. The program’s execution breaks out of the game loop at line 184 and moves to line 189, ending the game.
To keep the computer from running through the game loop as fast as possible (which would be much too fast for the player to keep up with), call mainClock.tick() to pause the game very briefly:
186. mainClock.tick(FPS)
This pause will be long enough to ensure that about 40 (the value stored inside the FPS variable) iterations through the game loop occur each second.
When the player loses, the game stops playing the background music and plays the “game over” sound effect:
188. # Stop the game and show the "Game Over" screen.
189. pygame.mixer.music.stop()
190. gameOverSound.play()
Line 189 calls the stop() function in the pygame.mixer.music module to stop the background music. Line 190 calls the play() method on the Sound object stored in gameOverSound.
Then lines 192 and 193 call the drawText() function to draw the “game over” text to the windowSurface object:
192. drawText('GAME OVER', font, windowSurface, (WINDOWWIDTH / 3),
(WINDOWHEIGHT / 3))
193. drawText('Press a key to play again.', font, windowSurface,
(WINDOWWIDTH / 3) - 80, (WINDOWHEIGHT / 3) + 50)
194. pygame.display.update()
195. waitForPlayerToPressKey()
Line 194 calls update() to draw this Surface object to the screen. After displaying this text, the game stops until the player presses a key by calling the waitForPlayerToPressKey() function.
After the player presses a key, the program execution returns from the waitForPlayerToPressKey() call on line 195. Depending on how long the player takes to press a key, the “game over” sound effect may or may not still be playing. To stop this sound effect before a new game starts, line 197 calls gameOverSound.stop():
197. gameOverSound.stop()
That’s it for our graphical game!
You may find that the game is too easy or too hard. Fortunately, the game is easy to modify because we took the time to use constant variables instead of entering the values directly. Now all we need to do to change the game is modify the values set in the constant variables.
For example, if you want the game to run slower in general, change the FPS variable on line 8 to a smaller value, such as 20. This will make both the baddies and the player’s character move slower, since the game loop will be executed only 20 times a second instead of 40.
If you just want to slow down the baddies and not the player, then change BADDIEMAXSPEED to a smaller value, such as 4. This will make all the baddies move between 1 (the value in BADDIEMINSPEED) and 4 pixels per iteration through the game loop, instead of between 1 and 8.
If you want the game to have fewer but larger baddies instead of many smaller baddies, then increase ADDNEWBADDIERATE to 12, BADDIEMINSIZE to 40, and BADDIEMAXSIZE to 80. Now baddies are being added every 12 iterations through the game loop instead of every 6 iterations, so there will be half as many baddies as before. But to keep the game interesting, the baddies are much larger.
Keeping the basic game the same, you can modify any of the constant variables to dramatically affect how the game plays. Keep trying out new values for the constant variables until you find the set of values you like best.
Unlike our text-based games, Dodger really looks like a modern computer game. It has graphics and music and uses the mouse. While pygame provides functions and data types as building blocks, it’s you the programmer who puts them together to create fun, interactive games.
And you can do all of this because you know how to instruct the computer to do it, step by step, line by line. By speaking the computer’s language, you can get it to do the number crunching and drawing for you. This is a useful skill, and I hope you’ll continue to learn more about Python programming. (And there’s still much more to learn!)
Now get going and invent your own games. Good luck!