Collision detection involves figuring out when two things on the screen have touched (that is, collided with) each other. Collision detection is really useful for games. For example, if the player touches an enemy, they may lose health. Or if the player touches a coin, they should automatically pick it up. Collision detection can help determine whether the game character is standing on solid ground or there’s nothing but empty air beneath them.
In our games, collision detection will determine whether two rectangles are overlapping each other. This chapter’s example program will cover this basic technique. We’ll also look at how our pygame programs can accept input from the player through the keyboard and the mouse. It’s a bit more complicated than calling the input() function, as we did for our text programs. But using the keyboard is much more interactive in GUI programs, and using the mouse isn’t even possible in our text games. These two concepts will make your games more exciting!
In this program, the player uses the keyboard’s arrow keys to move a black box around the screen. Smaller green squares, which represent food, appear on the screen, and the box “eats” them as it touches them. The player can click anywhere in the window to create new food squares. In addition, ESC quits the program, and the X key teleports the player to a random place on the screen.
Figure 19-1 shows what the program will look like once finished.
Figure 19-1: A screenshot of the pygame Collision Detection program
Start a new file, enter the following code, and then save it as collisionDetection.py. If you get errors after typing in this code, compare the code you typed to the book’s code with the online diff tool at https://www.nostarch.com/inventwithpython#diff.
collision Detection.py
1. import pygame, sys, random
2. from pygame.locals import *
3.
4. # Set up pygame.
5. pygame.init()
6. mainClock = pygame.time.Clock()
7.
8. # Set up the window.
9. WINDOWWIDTH = 400
10. WINDOWHEIGHT = 400
11. windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT),
0, 32)
12. pygame.display.set_caption('Collision Detection')
13.
14. # Set up the colors.
15. BLACK = (0, 0, 0)
16. GREEN = (0, 255, 0)
17. WHITE = (255, 255, 255)
18.
19. # Set up the player and food data structures.
20. foodCounter = 0
21. NEWFOOD = 40
22. FOODSIZE = 20
23. player = pygame.Rect(300, 100, 50, 50)
24. foods = []
25. for i in range(20):
26. foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH - FOODSIZE),
random.randint(0, WINDOWHEIGHT - FOODSIZE), FOODSIZE, FOODSIZE))
27.
28. # Set up movement variables.
29. moveLeft = False
30. moveRight = False
31. moveUp = False
32. moveDown = False
33.
34. MOVESPEED = 6
35.
36.
37. # Run the game loop.
38. while True:
39. # Check for events.
40. for event in pygame.event.get():
41. if event.type == QUIT:
42. pygame.quit()
43. sys.exit()
44. if event.type == KEYDOWN:
45. # Change the keyboard variables.
46. if event.key == K_LEFT or event.key == K_a:
47. moveRight = False
48. moveLeft = True
49. if event.key == K_RIGHT or event.key == K_d:
50. moveLeft = False
51. moveRight = True
52. if event.key == K_UP or event.key == K_w:
53. moveDown = False
54. moveUp = True
55. if event.key == K_DOWN or event.key == K_s:
56. moveUp = False
57. moveDown = True
58. if event.type == KEYUP:
59. if event.key == K_ESCAPE:
60. pygame.quit()
61. sys.exit()
62. if event.key == K_LEFT or event.key == K_a:
63. moveLeft = False
64. if event.key == K_RIGHT or event.key == K_d:
65. moveRight = False
66. if event.key == K_UP or event.key == K_w:
67. moveUp = False
68. if event.key == K_DOWN or event.key == K_s:
69. moveDown = False
70. if event.key == K_x:
71. player.top = random.randint(0, WINDOWHEIGHT -
player.height)
72. player.left = random.randint(0, WINDOWWIDTH -
player.width)
73.
74. if event.type == MOUSEBUTTONUP:
75. foods.append(pygame.Rect(event.pos[0], event.pos[1],
FOODSIZE, FOODSIZE))
76.
77. foodCounter += 1
78. if foodCounter >= NEWFOOD:
79. # Add new food.
80. foodCounter = 0
81. foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH -
FOODSIZE), random.randint(0, WINDOWHEIGHT - FOODSIZE),
FOODSIZE, FOODSIZE))
82.
83. # Draw the white background onto the surface.
84. windowSurface.fill(WHITE)
85.
86. # Move the player.
87. if moveDown and player.bottom < WINDOWHEIGHT:
88. player.top += MOVESPEED
89. if moveUp and player.top > 0:
90. player.top -= MOVESPEED
91. if moveLeft and player.left > 0:
92. player.left -= MOVESPEED
93. if moveRight and player.right < WINDOWWIDTH:
94. player.right += MOVESPEED
95.
96. # Draw the player onto the surface.
97. pygame.draw.rect(windowSurface, BLACK, player)
98.
99. # Check whether the player has intersected with any food squares.
100. for food in foods[:]:
101. if player.colliderect(food):
102. foods.remove(food)
103.
104. # Draw the food.
105. for i in range(len(foods)):
106. pygame.draw.rect(windowSurface, GREEN, foods[i])
107.
108. # Draw the window onto the screen.
109. pygame.display.update()
110. mainClock.tick(40)
The pygame Collision Detection program imports the same modules as the Animation program in Chapter 18, plus the random module:
1. import pygame, sys, random
2. from pygame.locals import *
Lines 5 to 17 mostly do the same things that the Animation program did: they initialize pygame, set WINDOWHEIGHT and WINDOWWIDTH, and assign the color and direction constants.
However, line 6 is new:
6. mainClock = pygame.time.Clock()
In the Animation program, a call to time.sleep(0.02) slowed down the program so that it wouldn’t run too fast. While this call will always pause for 0.02 seconds on all computers, the speed of the rest of the program depends on how fast the computer is. If we want this program to run at the same speed on any computer, we need a function that pauses longer on fast computers and shorter on slow computers.
A pygame.time.Clock object can pause an appropriate amount of time on any computer. Line 110 calls mainClock.tick(40) inside the game loop. This call to the Clock object’s tick() method waits enough time so that it runs at about 40 iterations a second, no matter what the computer’s speed is. This ensures that the game never runs faster than you expect. A call to tick() should appear only once in the game loop.
Lines 19 to 22 set up a few variables for the food squares that appear on the screen:
19. # Set up the player and food data structures.
20. foodCounter = 0
21. NEWFOOD = 40
22. FOODSIZE = 20
The foodCounter variable will start at the value 0, NEWFOOD at 40, and FOODSIZE at 20. We’ll see how these are used later when we create the food.
Line 23 sets up a pygame.Rect object for the player’s location:
23. player = pygame.Rect(300, 100, 50, 50)
The player variable has a pygame.Rect object that represents the box’s size and position. The player’s box will move like the boxes did in the Animation program (see “Moving Each Box” on page 280), but in this program, the player can control where the box moves.
Next, we set up some code to keep track of the food squares:
24. foods = []
25. for i in range(20):
26. foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH - FOODSIZE),
random.randint(0, WINDOWHEIGHT - FOODSIZE), FOODSIZE, FOODSIZE))
The program will keep track of every food square with a list of Rect objects in foods. Lines 25 and 26 create 20 food squares randomly placed around the screen. You can use the random.randint() function to come up with random x- and y-coordinates.
On line 26, the program calls the pygame.Rect() constructor function to return a new pygame.Rect object. It will represent the position and size of a new food square. The first two parameters for pygame.Rect() are the x- and y-coordinates of the top-left corner. You want the random coordinate to be between 0 and the size of the window minus the size of the food square. If you set the random coordinate between 0 and the size of the window, then the food square might be pushed outside of the window altogether, as in Figure 19-2.
The third and fourth parameters for pygame.Rect() are the width and height of the food square. Both the width and height are the values in the FOODSIZE constant.
Figure 19-2: For a 100×100 square in a 400×400 window, setting the top-left edge at 400 would place the rectangle outside of the window. To be inside, the left edge should be set at 300 instead.
The third and fourth parameters for pygame.Rect() are the width and height of the food square. Both the width and height are the values in the FOODSIZE constant.
Starting at line 29, the code sets up some variables that track the movement of the player’s box for each direction the box can move:
28. # Set up movement variables.
29. moveLeft = False
30. moveRight = False
31. moveUp = False
32. moveDown = False
The four variables have Boolean values to keep track of which arrow key is being pressed and are initially set to False. For example, when the player presses the left arrow key on their keyboard to move the box, moveLeft is set to True. When they let go of the key, moveLeft is set back to False.
Lines 34 to 43 are nearly identical to code in the previous pygame programs. These lines handle the start of the game loop and what to do when the player quits the program. We’ll skip the explanation for this code since we covered it in the previous chapter.
The pygame module can generate events in response to user input from the mouse or keyboard. The following are the events that can be returned by pygame.event.get():
QUIT Generated when the player closes the window.
KEYDOWN Generated when the player presses a key. Has a key attribute that tells which key was pressed. Also has a mod attribute that tells whether the SHIFT, CTRL, ALT, or other keys were held down when this key was pressed.
KEYUP Generated when the player releases a key. Has key and mod attributes that are similar to those for KEYDOWN.
MOUSEMOTION Generated whenever the mouse moves over the window. Has a pos attribute (short for position) that returns a tuple (x, y) for the coordinates of where the mouse is in the window. The rel attribute also returns an (x, y) tuple, but it gives relative coordinates since the last MOUSEMOTION event. For example, if the mouse moves left by 4 pixels from (200, 200) to (196, 200), then rel will be the tuple value (-4, 0). The button attribute returns a tuple of three integers. The first integer in the tuple is for the left mouse button, the second integer is for the middle mouse button (if one exists), and the third integer is for the right mouse button. These integers will be 0 if they are not being pressed when the mouse is moved and 1 if they are pressed.
MOUSEBUTTONDOWN Generated when a mouse button is pressed in the window. This event has a pos attribute, which is an (x, y) tuple for the coordinates of where the mouse was positioned when the button was pressed. There is also a button attribute, which is an integer from 1 to 5 that tells which mouse button was pressed, as explained in Table 19-1.
MOUSEBUTTONUP Generated when the mouse button is released. This has the same attributes as MOUSEBUTTONDOWN.
When the MOUSEBUTTONDOWN event is generated, it has a button attribute. The button attribute is a value that is associated with the different types of buttons a mouse might have. For instance, the left button has the value 1, and the right button has the value 3. Table 19-1 lists all of the button attributes for mouse events, but note that a mouse might not have all the button values listed here.
Table 19-1: The button Attribute Values
Value of button |
Mouse button |
1 |
Left button |
2 |
Middle button |
3 |
Right button |
4 |
Scroll wheel moved up |
5 |
Scroll wheel moved down |
We’ll use these events to let the player control the box with KEYDOWN events and with mouse button clicks.
The code to handle the keypress and key release events starts on line 44; it includes the KEYDOWN event type:
44. if event.type == KEYDOWN:
If the event type is KEYDOWN, then the Event object has a key attribute that indicates which key was pressed. When the player presses an arrow key or a WASD key (pronounced wazz-dee, these keys are in the same layout as the arrow keys but on the left side of the keyboard), then we want the box to move. We’ll use if statements to check the pressed key in order to tell which direction the box should move.
Line 46 compares this key attribute to K_LEFT and K_a, which are the pygame.locals constants that represent the left arrow key on the keyboard and the A in WASD, respectively. Lines 46 to 57 check for each of the arrow and WASD keys:
45. # Change the keyboard variables.
46. if event.key == K_LEFT or event.key == K_a:
47. moveRight = False
48. moveLeft = True
49. if event.key == K_RIGHT or event.key == K_d:
50. moveLeft = False
51. moveRight = True
52. if event.key == K_UP or event.key == K_w:
53. moveDown = False
54. moveUp = True
55. if event.key == K_DOWN or event.key == K_s:
56. moveUp = False
57. moveDown = True
When one of these keys is pressed, the code tells Python to set the corresponding movement variable to True. Python will also set the movement variable of the opposite direction to False.
For example, the program executes lines 47 and 48 when the left arrow key has been pressed. In this case, Python will set moveLeft to True and moveRight to False (even though moveRight might already be False, Python will set it to False again just to be sure).
On line 46, event.key can either be equal to K_LEFT or K_a. The value in event.key is set to the same value as K_LEFT if the left arrow key is pressed or the same value as K_a if the A key is pressed.
By executing the code on lines 47 and 48 if the keystroke is either K_LEFT or K_a, you make the left arrow key and the A key do the same thing. The W, A, S, and D keys are used as alternates for changing the movement variables, letting the player use their left hand instead of their right if they prefer. You can see an illustration of both sets of keys in Figure 19-3.
Figure 19-3: The WASD keys can be programmed to do the same thing as the arrow keys.
The constants for letter and number keys are easy to figure out: the A key’s constant is K_a, the B key’s constant is K_b, and so on. The 3 key’s constant is K_3. Table 19-2 lists commonly used constant variables for the other keyboard keys.
Table 19-2: Constant Variables for Keyboard Keys
pygame constant variable |
Keyboard key |
K_LEFT |
Left arrow |
K_RIGHT |
Right arrow |
K_UP |
Up arrow |
K_DOWN |
Down arrow |
K_ESCAPE |
ESC |
K_BACKSPACE |
Backspace |
K_TAB |
TAB |
K_RETURN |
RETURN or ENTER |
K_SPACE |
Spacebar |
K_DELETE |
DEL |
K_LSHIFT |
Left SHIFT |
K_RSHIFT |
Right SHIFT |
K_LCTRL |
Left CTRL |
K_RCTRL |
Right CTRL |
K_LALT |
Left ALT |
K_RALT |
Right ALT |
K_HOME |
HOME |
K_END |
END |
K_PAGEUP |
PGUP |
K_PAGEDOWN |
PGDN |
K_F1 |
F1 |
K_F2 |
F2 |
K_F3 |
F3 |
K_F4 |
F4 |
K_F5 |
F5 |
K_F6 |
F6 |
K_F7 |
F7 |
K_F8 |
F8 |
K_F9 |
F9 |
K_F10 |
F10 |
K_F11 |
F11 |
K_F12 |
F12 |
When the player releases the key that they were pressing, a KEYUP event is generated:
58. if event.type == KEYUP:
If the key that the player released was ESC, then Python should terminate the program. Remember, in pygame you must call the pygame.quit() function before calling the sys.exit() function, which we do in lines 59 to 61:
59. if event.key == K_ESCAPE:
60. pygame.quit()
61. sys.exit()
Lines 62 to 69 set a movement variable to False if that direction’s key was released:
62. if event.key == K_LEFT or event.key == K_a:
63. moveLeft = False
64. if event.key == K_RIGHT or event.key == K_d:
65. moveRight = False
66. if event.key == K_UP or event.key == K_w:
67. moveUp = False
68. if event.key == K_DOWN or event.key == K_s:
69. moveDown = False
Setting the movement variable to False through a KEYUP event makes the box stop moving.
You can also add teleportation to the game. If the player presses the X key, lines 71 and 72 set the position of the player’s box to a random place on the window:
70. if event.key == K_x:
71. player.top = random.randint(0, WINDOWHEIGHT -
player.height)
72. player.left = random.randint(0, WINDOWWIDTH -
player.width)
Line 70 checks whether the player pressed the X key. Then, line 71 sets a random x-coordinate to teleport the player to between 0 and the window’s height minus the player rectangle’s height. Line 72 executes similar code, but for the y-coordinate. This enables the player to teleport around the window by pushing the X key, but they can’t control where they will teleport—it’s completely random.
There are two ways the player can add new food squares to the screen. They can click a spot in the window where they want the new food square to appear, or they can wait until the game loop has iterated NEWFOOD number of times, in which case a new food square will be randomly generated on the window.
We’ll look at how food is added through the player’s mouse input first:
74. if event.type == MOUSEBUTTONUP:
75. foods.append(pygame.Rect(event.pos[0], event.pos[1],
FOODSIZE, FOODSIZE))
Mouse input is handled by events just like keyboard input. The MOUSEBUTTONUP event occurs when the player releases the mouse button after clicking it.
On line 75, the x-coordinate is stored in event.pos[0], and the y-coordinate is stored in event.pos[1]. Line 75 creates a new Rect object to represent a new food square and places it where the MOUSEBUTTONUP event occurred. By adding a new Rect object to the foods list, the code displays a new food square on the screen.
In addition to being added manually at the player’s discretion, food squares are generated automatically through the code on lines 77 to 81:
77. foodCounter += 1
78. if foodCounter >= NEWFOOD:
79. # Add new food.
80. foodCounter = 0
81. foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH -
FOODSIZE), random.randint(0, WINDOWHEIGHT - FOODSIZE),
FOODSIZE, FOODSIZE))
The variable foodCounter keeps track of how often food should be added. Each time the game loop iterates, foodCounter is incremented by 1 on line 77.
Once foodCounter is greater than or equal to the constant NEWFOOD, foodCounter is reset and a new food square is generated by line 81. You can change the rate at which new food squares are added by adjusting NEWFOOD back on line 21.
Line 84 just fills the window surface with white, which we covered in “Handling When the Player Quits” on page 279, so we’ll move on to discussing how the player moves around the screen.
We’ve set the movement variables (moveDown, moveUp, moveLeft, and moveRight) to True or False depending on what keys the player has pressed. Now we need to move the player’s box, which is represented by the pygame.Rect object stored in player. We’ll do this by adjusting the x- and y-coordinates of player.
86. # Move the player.
87. if moveDown and player.bottom < WINDOWHEIGHT:
88. player.top += MOVESPEED
89. if moveUp and player.top > 0:
90. player.top -= MOVESPEED
91. if moveLeft and player.left > 0:
92. player.left -= MOVESPEED
93. if moveRight and player.right < WINDOWWIDTH:
94. player.right += MOVESPEED
If moveDown is set to True (and the bottom of the player’s box isn’t below the bottom edge of the window), then line 88 moves the player’s box down by adding MOVESPEED to the player’s current top attribute. Lines 89 to 94 do the same thing for the other three directions.
Line 97 draws the player’s box on the window:
96. # Draw the player onto the surface.
97. pygame.draw.rect(windowSurface, BLACK, player)
After the box is moved, line 97 draws it in its new position. The windowSurface passed for the first parameter tells Python which Surface object to draw the rectangle on. The BLACK variable, which has (0, 0, 0) stored in it, tells Python to draw a black rectangle. The Rect object stored in the player variable tells Python the position and size of the rectangle to draw.
Before drawing the food squares, the program needs to check whether the player’s box has overlapped with any of the squares. If it has, then that square needs to be removed from the foods list. This way, Python won’t draw any food squares that the box has already eaten.
We’ll use the collision detection method that all Rect objects have, colliderect(), in line 101:
99. # Check whether the player has intersected with any food squares.
100. for food in foods[:]:
101. if player.colliderect(food):
102. foods.remove(food)
On each iteration through the for loop, the current food square from the foods (plural) list is placed in the variable food (singular). The colliderect() method for pygame.Rect objects is passed the player rectangle’s pygame.Rect object as an argument and returns True if the two rectangles collide and False if they do not. If True, line 102 removes the overlapping food square from the foods list.
The code on lines 105 and 106 is similar to the code we used to draw the black box for the player:
104. # Draw the food.
105. for i in range(len(foods)):
106. pygame.draw.rect(windowSurface, GREEN, foods[i])
Line 105 loops through each food square in the foods list, and line 106 draws the food square onto windowSurface.
Now that the player and food squares are on the screen, the window is ready to be updated, so we call the update() method on line 109 and finish the program by calling the tick() method on the Clock object we created earlier:
108. # Draw the window onto the screen.
109. pygame.display.update()
110. mainClock.tick(40)
The program will continue through the game loop and keep updating until the player quits.
This chapter introduced the concept of collision detection. Detecting collisions between two rectangles is so common in graphical games that pygame provides its own collision detection method named colliderect() for pygame.Rect objects.
The first several games in this book were text based. The program’s output was text printed to the screen, and the input was text typed by the player on the keyboard. But graphical programs can also accept keyboard and mouse inputs.
Furthermore, these programs can respond to single keystrokes when the player presses or releases a single key. The player doesn’t have to type in an entire response and press ENTER. This allows for immediate feedback and much more interactive games.
This interactive program is fun, but let’s move beyond drawing rectangles. In Chapter 20, you’ll learn how to load images and play sound effects with pygame.