Dodger
Moving the Player's Character
140. # Move the player around. 141. if moveLeft and playerRect.left > 0: 142. playerRect.move_ip(-1 * PLAYERMOVERATE, 0)
The four movement variables moveLeft, moveRight, moveUp and moveDown are set to True and False when Pygame generates the KEYDOWN and KEYUP events, respectively. (This code is from line 86 to line 121.)
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 we want to move the character's Rect object (stored in playerRect).
We will always move the playerRect object by the number of pixels in PLAYERMOVERATE. To get the negative form of an integer, you can simply multiple it by -1. So on line 142, since 5 is stored in PLAYERMOVERATE, the expression -1 * PLAYERMOVERATE evaluates to -5.
This means that calling playerRect.move_ip(-1 * PLAYERMOVERATE, 0) will change the location of playerRect by 5 pixels to the left of its current location.
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)
We want to do the same thing for the other three directions: right, up, and down. Each of the three if statements in lines 143 to 148 checks that their movement variable is set to True and that the edge of the Rect object of the player is inside the window before calling the move_ip() method to move the Rect object.
The pygame.mouse.set_pos() Function
150. # Move the mouse cursor to match the player. 151. pygame.mouse.set_pos(playerRect.centerx, playerRect.centery)
Line 151 moves the mouse cursor to the same position as the player's character. The pygame.mouse.set_pos() function moves the mouse cursor to the X and Y coordinates that you pass it. Specifically, the cursor will be right in the middle of the character's Rect object because we pass the centerx and centery attributes of playerRect for the coordinates. The mouse cursor still exists and can be moved, even though it is invisible because we called pygame.mouse.set_visible(False) on line 47.
The reason we want the mouse cursor to match the location of the player's character is to avoid sudden jumps. Imagine that the mouse cursor and the player's character are at the same location on the left side of the window. When the player holds down the right arrow key, the character moves to the right edge of the window but the mouse cursor would stay at the left edge of the screen. If the player then moves the mouse just a little bit, the player's character would immediately jump to the location of the mouse cursor on the left edge of the screen. By moving the mouse cursor along with the player's character, any mouse movements would not result in a sudden jump across the window.
153. # Move the baddies down. 154. for b in baddies:
Now we want to loop through each baddie data structure in the baddies list to move them down a little.
155. if not reverseCheat and not slowCheat: 156. b['rect'].move_ip(0, b['speed'])
If neither of the cheats have been activated (by the player pushing the "z" or "x" keys which sets reverseCheat or slowCheat to True, respectively), then move the baddie's location down a number of pixels equal to its speed, which is stored in the 'speed' key.
Implementing the Cheat Codes
157. elif reverseCheat: 158. b ['rect'] .move_ip(0, -5)
If the reverse cheat has been activated, then the baddie should actually be moved up by five pixels. Passing -5 for the second argument to move_ip() will move the Rect object upwards by five pixels.
159. elif slowCheat: 160. b['rect'].move_ip(0, 1)
If the slow cheat has been activated, then the baddie should move downwards, but only by the slow speed of one pixel per iteration through the game loop. The baddie's normal speed (which is stored in the 'speed' key of the baddie's data structure) will be ignored while the slow cheat is activated.
Removing the Baddies
162. # Delete baddies that have fallen past the bottom. 163. for b in baddies [:] :
After moving the baddies down the window, we want to remove any baddies that fell below the bottom edge of the window from the baddies list. Remember that we while we are iterating through a list, we should not modify the contents of the list by adding or removing items. So instead of iterating through the baddies list with our baddies loop we will iterate through a copy of the baddies list.
Remember that a list slice will evaluate a copy of a list's items. For example, spam [2:4] will return a new list with the items from index 2 up to (but not including) index 4. Leaving the first index blank will indicate that index 0 should be used. For example, spam [:4] will return a list with items from the start of the list up to (but not including) the item at index 4. Leaving the second index blank will indicate that up to (and including) the last index should be used. For example, spam[2:] will return a list with items from index 2 all the way to (and including) the last item in the list.
But leaving both indexes in the slice blank is a way to represent the entire list. The baddies[:] expression is a list slice of the whole list, so it evaluates to a copy of the entire list. This is useful because while we are iterating on the copy of the list, we can modify the original list and remove any baddie data structures that have fallen past the bottom edge of the window.
Our for loop on line 163 uses a variable b for the current item in the iteration through baddies[:].
164. if b [ 'rect'] .top > WINDOWHEIGHT: 165. baddies.remove(b)
Let's evaluate the expression b['rect'].top. b 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 is the Y-coordinate of the top edge of the rectangular area. Remember that in the coordinate system, the Y-coordinates increase going down. So b['rect'].top > WINDOWHEIGHT will check if the top edge of the baddie is below the bottom of the window.
If this condition is True, then the we will remove the baddie data structure from the baddies list.
Drawing the Window
It isn't enough that our game updates the state of the game world in its memory. Our program will also have to display the game world to the player. We can do this by drawing the graphics of the baddies and player's character on the screen. Because the game loop is executed several times a second, drawing the baddies and player in new positions makes their movement look smooth and natural. But every element on the screen must be drawn one at a time by calling the appropriate Pygame function.
167. # Draw the game world on the window. 168. windowSurface.fill(BACKGROUNDCOLOR)
Now that we have updated all the data structures for the baddies and the player's character, let's draw everything on the screen. First, before we draw anything else on the Surface object referred to by windowSurface, we want to black out the entire screen to erase anything drawn on it in a previous iteration through the game loop.
Remember that the Surface object in windowSurface is the special Surface object because it was the one returned by pygame.display.set_mode(). This mean that anything drawn on that Surface object will appear on the screen, but only after the pygame.display.update() function is called.
Drawing the Player's Score
170. # Draw the score and top score. 171. drawText('Score: %s' % (score), font, windowSurface, 10, 0) 172. drawText('Top Score: %s' % (topScore), font, windowSurface, 10, 40)
Next we will render the text for score and top score to the top left corner of the window. The 'Score: %s' % (score) uses string interpolation to insert the value in the score variable into the string. This is the same thing as 'Score: ' + str(score). We pas this string, the Font object stored in the font variable, the Surface object on which to draw the text on, and the X and Y coordinates of where the text should be placed. Remember that our drawText() will handle the call to the render() and blit() methods.
For the top score, we do the exact same thing. We pass 40 for the Y-coordinate instead of 0 (like we do for the score) so that the top score text appears beneath the score text.
Drawing the Player's Character
174. # Draw the player's rectangle 175. windowSurface.blit(playerImage, playerRect)
Remember that the 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's character's image. playerRect is a Rect object that stores the information about the size and location of the player's character.
We call the blit() method on windowSurface and pass playerImage and playerRect. This draws the player character's image on windowSurface at the appropriate location.
177. # Draw each baddie 178. for b in baddies: 179. windowSurface.blit(b['surface'], b['rect'])
We use a for loop here to draw every baddie on the windowSurface object. Remember that each item in the baddies list is a dictionary with 'surface' and 'rect' keys containing the Surface object with the baddie image and the Rect object with the position and size information, respectively.
181. pygame.display.update()
Now that we have finished drawing everything to the windowSurface object, we should draw this surface to the screen with a call to pygame.display.update().
Collision Detection
183. # Check if any of the baddies have hit the player. 184. if playerHasHitBaddie(playerRect, baddies): 185. if score > topScore: 186. topScore = score # set new top score 187. break
Now let's check if the player has collided with any of the baddies. We already wrote a function to check for this: 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 will return False.
If the player's character has hit a baddie, then we check if the player's current score is greater than the top score. If it is, we set the new top score to be the player's current score. Either way, we break out of the game loop. The program's execution will jump down to line 191.
189. mainClock.tick(FPS)
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), we call mainClock.tick() to pause for a brief amount of time. The pause will be long enough to ensure that about 40 (the value we stored inside the FPS variable) iterations through the game loop occur each second.
The Game Over Screen
191. # Stop the game and show the "Game Over" screen. 192. pygame.mixer.music.stop() 193. gameOverSound.play()
When the player loses, we want to stop playing the background music and play the "game over" sound effect. We call the stop() function in the pygame.mixer.music module to stop the background music. Then we call the play() method on the Sound object stored in gameOverSound.
195. drawText('GAME OVER', font, windowSurface, (WINDOWWIDTH / 3), (WINDOWHEIGHT / 3)) 196. drawText('Press a key to play again.', font, windowSurface, (WINDOWWIDTH / 3) - 80, (WINDOWHEIGHT / 3) + 50) 197. pygame.display.update() 198. waitForPlayerToPressKey()
Now we want to display text on the window to tell the player that the game is over, and they should press a key to start playing a new game. The two calls to our drawText() function will draw this text to the windowSurface object, and the call to pygame.display.update() will draw this Surface object to the screen.
After displaying this text, we want the game to stop until the player presses a key, so we call our waitForPlayerToPressKey() function.
200. gameOverSound.stop()
After the player presses a key, the program execution will return from the waitForPlayerToPressKey() call on line 198. Depending on how long the player takes to press a key, the "game over" sound effect may or may not still be playing. We want to stop this sound effect before this loop ends and we start a new game, so we have a call to gameOverSound.stop() here.
Modifying the Dodger Game
That's it for our graphical game. You may find that the game is too easy or too hard. But the game is very easy to modify because we took the time to use constant variables instead of typing in the values directly. Now all we need to do to change the game is modify the value 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 only be executed 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 1 and 8.
If you want the game to have fewer but larger baddies instead of many fast baddies, then increase ADDNEWBADDIERATE to 12, BADDIEMINSIZE to 40, and BADDIEMAXSIZE to 80. Now that baddies are being added every 12 iterations through the game loop instead of every 6 iterations, there will be half as many baddies as before. But to keep the game interesting, the baddies are now much larger than before.
While the basic game remains the same, you can modify any of the constant variables to drastically affect the behavior of the game. Keep trying out new values for the constant variables until you find a set of values you like the best.
Summary: Creating Your Own Games
Unlike our previous text-based games, Dodger really looks like the kind of modern computer game we usually play. It has graphics and music and uses the mouse. While Pygame provides functions and data types as building blocks, it is you the programmer who puts them together to create fun, interactive games.
And it is all because you know exactly how to instruct the computer to do it, step by step, line by line. You can speak the computer's language, and get it to do large amounts of number crunching and drawing for you. This is a very useful skill, and I hope you will continue to learn more about Python programming. (And there is still more to learn!)
Here are several websites that can teach you more about programming Python:
http://www.python.org/doc/ | More Python tutorials and the documentation of all the Python modules and functions. |
http://www.pygame.org/docs/ | Complete documentation on the modules and functions for Pygame. |
http://inventwithpython.com | This book's website, which includes all the source code for these programs and additional information. This site also has the image and sound files used in the Pygame programs. |
http://inventwithpython.com/traces | A web application that helps you trace through the execution of the programs in this book, step by step. |
http://inventwithpython.com/videos | Videos that accompany the programs in this book. |
http://gamedevlessons.com | A helpful website about how to design and program video games. |
al@inventwithpython.com | The author's email address. Feel free to email Al your questions about this book or about Python programming. |
Or you can find out more about Python by searching the World Wide Web. Go to the search engine website http://google.com and search for "Python programming" or "Python tutorials" to find web sites that can teach you more about Python programming.