So far, all of our games have used only text. Text is displayed on the screen as output, and the player enters text as input. Just using text makes programming easy to learn. But in this chapter, we’ll make some more exciting programs with advanced graphics using the pygame module.
Chapters 17, 18, 19, and 20 will teach you how to use pygame to make games with graphics, animations, mouse input, and sound. In these chapters, we’ll write source code for simple programs that demonstrate pygame concepts. Then in Chapter 21, we’ll put together all the concepts we learned to create a game.
The pygame module helps developers create games by making it easier to draw graphics on your computer screen or add music to programs. The module doesn’t come with Python, but like Python, it’s free to download. Download pygame at https://www.nostarch.com/inventwithpython/, and follow the instructions for your operating system.
After the installer file finishes downloading, open it and follow the instructions until pygame has finished installing. To check that it installed correctly, enter the following into the interactive shell:
>>> import pygame
If nothing appears after you press ENTER, then you know pygame was successfully installed. If the error ImportError: No module named pygame appears, try to install pygame again (and make sure you typed import pygame correctly).
NOTE
When writing your Python programs, don’t save your file as pygame.py. If you do, the import pygame line will import your file instead of the real pygame module, and none of your code will work.
First, we’ll make a new pygame Hello World program like the one you created at the beginning of the book. This time, you’ll use pygame to make “Hello world!” appear in a graphical window instead of as text. We’ll just use pygame to draw some shapes and lines on the window in this chapter, but you’ll use these skills to make your first animated game soon.
The pygame module doesn’t work well with the interactive shell, so you can only write programs using pygame in a file editor; you can’t send instructions one at a time through the interactive shell.
Also, pygame programs don’t use the print() or input() functions. There is no text input or output. Instead, pygame displays output by drawing graphics and text in a separate window. Input to pygame comes from the keyboard and the mouse through events, which are covered in “Events and the Game Loop” on page 270.
When you run the graphical Hello World program, you should see a new window that looks like Figure 17-1.
Figure 17-1: The pygame Hello World program
The nice thing about using a window instead of a console is that text can appear anywhere in the window, not just after the previous text you have printed. The text can also be any color and size. The window is like a canvas, and you can draw whatever you like on it.
Enter the following code into the file editor and save it as pygameHelloWorld.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.
pygame HelloWorld.py
1. import pygame, sys
2. from pygame.locals import *
3.
4. # Set up pygame.
5. pygame.init()
6.
7. # Set up the window.
8. windowSurface = pygame.display.set_mode((500, 400), 0, 32)
9. pygame.display.set_caption('Hello world!')
10.
11. # Set up the colors.
12. BLACK = (0, 0, 0)
13. WHITE = (255, 255, 255)
14. RED = (255, 0, 0)
15. GREEN = (0, 255, 0)
16. BLUE = (0, 0, 255)
17.
18. # Set up the fonts.
19. basicFont = pygame.font.SysFont(None, 48)
20.
21. # Set up the text.
22. text = basicFont.render('Hello world!', True, WHITE, BLUE)
23. textRect = text.get_rect()
24. textRect.centerx = windowSurface.get_rect().centerx
25. textRect.centery = windowSurface.get_rect().centery
26.
27. # Draw the white background onto the surface.
28. windowSurface.fill(WHITE)
29.
30. # Draw a green polygon onto the surface.
31. pygame.draw.polygon(windowSurface, GREEN, ((146, 0), (291, 106),
(236, 277), (56, 277), (0, 106)))
32.
33. # Draw some blue lines onto the surface.
34. pygame.draw.line(windowSurface, BLUE, (60, 60), (120, 60), 4)
35. pygame.draw.line(windowSurface, BLUE, (120, 60), (60, 120))
36. pygame.draw.line(windowSurface, BLUE, (60, 120), (120, 120), 4)
37.
38. # Draw a blue circle onto the surface.
39. pygame.draw.circle(windowSurface, BLUE, (300, 50), 20, 0)
40.
41. # Draw a red ellipse onto the surface.
42. pygame.draw.ellipse(windowSurface, RED, (300, 250, 40, 80), 1)
43.
44. # Draw the text's background rectangle onto the surface.
45. pygame.draw.rect(windowSurface, RED, (textRect.left - 20,
textRect.top - 20, textRect.width + 40, textRect.height + 40))
46.
47. # Get a pixel array of the surface.
48. pixArray = pygame.PixelArray(windowSurface)
49. pixArray[480][380] = BLACK
50. del pixArray
51.
52. # Draw the text onto the surface.
53. windowSurface.blit(text, textRect)
54.
55. # Draw the window onto the screen.
56. pygame.display.update()
57.
58. # Run the game loop.
59. while True:
60. for event in pygame.event.get():
61. if event.type == QUIT:
62. pygame.quit()
63. sys.exit()
Let’s go over each of these lines of code and find out what they do.
First, you need to import the pygame module so you can call its functions. You can import several modules on the same line by separating the module names with commas. Line 1 imports both the pygame and sys modules:
1. import pygame, sys
2. from pygame.locals import *
The second line imports the pygame.locals module. This module contains many constant variables that you’ll use with pygame, such as QUIT, which helps you quit the program, and K_ESCAPE, which represents the ESC key.
Line 2 also lets you use the pygames.locals module without having to type pygames.locals. in front of every method, constant, or anything else you call from the module.
If you have from sys import * instead of import sys in your program, you could call exit() instead of sys.exit() in your code. But most of the time it’s better to use the full function name so you know which module the function is in.
All pygame programs must call pygame.init() after importing the pygame module but before calling any other pygame functions:
4. # Set up pygame.
5. pygame.init()
This initializes pygame so it’s ready to use. You don’t need to know what init() does; you just need to remember to call it before using any other pygame functions.
Line 8 creates a graphical user interface (GUI) window by calling the set_mode() method in the pygame.display module. (The display module is a module inside the pygame module. Even the pygame module has its own modules!)
7. # Set up the window.
8. windowSurface = pygame.display.set_mode((500, 400), 0, 32)
9. pygame.display.set_caption('Hello world!')
These methods help set up a window for pygame to run in. As in the Sonar Treasure Hunt game, windows use a coordinate system, but the window’s coordinate system is organized in pixels.
A pixel is the tiniest dot on your computer screen. A single pixel on your screen can light up in any color. All the pixels on your screen work together to display the pictures you see. We’ll create a window 500 pixels wide and 400 pixels tall by using a tuple.
Tuple values are similar to lists, except they use parentheses instead of square brackets. Also, like strings, tuples can’t be modified. For example, enter the following into the interactive shell:
>>> spam = ('Life', 'Universe', 'Everything', 42)
➊ >>> spam[0]
'Life'
>>> spam[3]
42
➋ >>> spam[1:3]
('Universe', 'Everything')
➌ >>> spam[3] = 'Hello'
➍ Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
As you can see from the example, if you want to get just one item of a tuple ➊ or a range of items ➋, you still use square brackets as you would with a list. However, if you try to change the item at index 3 to the string 'Hello' ➌, Python will raise an error ➍.
We’ll use tuples to set up pygame windows. There are three parameters to the pygame.display.set_mode() method. The first is a tuple of two integers for the width and height of the window, in pixels. To set up a 500- by 400-pixel window, you use the tuple (500, 400) for the first argument to set_mode(). The second and third parameters are advanced options that are beyond the scope of this book. Just pass 0 and 32 for them, respectively.
The set_mode() function returns a pygame.Surface object (which we’ll call Surface objects for short). Object is just another name for a value of a data type that has methods. For example, strings are objects in Python because they have data (the string itself) and methods (such as lower() and split()). The Surface object represents the window.
Variables store references to objects just as they store references for lists and dictionaries (see “List References” on page 132).
The set_caption() method on line 9 just sets the window’s caption to read 'Hello World!'. The caption is in the top left of the window.
There are three primary colors of light for pixels: red, green, and blue. By combining different amounts of these three colors (which is what your computer screen does), you can form any other color. In pygame, colors are represented by tuples of three integers. These are called RGB color values, and we’ll use them in our program to assign colors to pixels. Since we don’t want to rewrite a three-number tuple every time we want to use a specific color in our program, we’ll make constants to hold tuples that are named after the color the tuple represents:
11. # Set up the colors.
12. BLACK = (0, 0, 0)
13. WHITE = (255, 255, 255)
14. RED = (255, 0, 0)
15. GREEN = (0, 255, 0)
16. BLUE = (0, 0, 255)
The first value in the tuple determines how much red is in the color. A value of 0 means there’s no red in the color, and a value of 255 means there’s a maximum amount of red in the color. The second value is for green, and the third value is for blue. These three integers form an RGB tuple.
For example, the tuple (0, 0, 0) has no red, green, or blue. The resulting color is completely black, as in line 12. The tuple (255, 255, 255) has a maximum amount of red, green, and blue, resulting in white, as in line 13.
We’ll also use red, green, and blue, which are assigned in lines 14 to 16. The tuple (255, 0, 0) represents the maximum amount of red but no green or blue, so the resulting color is red. Similarly, (0, 255, 0) is green and (0, 0, 255) is blue.
You can mix the amount of red, green, and blue to get any shade of any color. Table 17-1 has some common colors and their RGB values. The web page https://www.nostarch.com/inventwithpython/ lists several more tuple values for different colors.
Table 17-1: Colors and Their RGB Values
Color |
RGB value |
Black |
(0, 0, 0) |
Blue |
(0, 0, 255) |
Gray |
(128, 128, 128) |
Green |
(0, 128, 0) |
Lime |
(0, 255, 0) |
Purple |
(128, 0, 128) |
Red |
(255, 0, 0) |
Teal |
(0, 128, 128) |
White |
(255, 255, 255) |
Yellow |
(255, 255, 0) |
We’ll just use the five colors we’ve already defined, but in your programs, you can use any of these colors or even make up different colors.
Writing text on a window is a little different from just using print(), as we’ve done in our text-based games. In order to write text on a window, we need to do some setup first.
A font is a complete set of letters, numbers, symbols, and characters drawn in a single style. We’ll use fonts anytime we need to print text on a pygame window. Figure 17-2 shows the same sentence printed in different fonts.
Figure 17-2: Examples of different fonts
In our earlier games, we only told Python to print text. The color, size, and font that were used to display this text were completely determined by your operating system. The Python program couldn’t change the font. However, pygame can draw text in any font on your computer.
Line 19 creates a pygame.font.Font object (called a Font object for short) by calling the pygame.font.SysFont() function with two parameters:
18. # Set up the fonts.
19. basicFont = pygame.font.SysFont(None, 48)
The first parameter is the name of the font, but we’ll pass the None value to use the default system font. The second parameter is the size of the font (which is measured in units called points). We’ll draw 'Hello world!' on the window in the default font at 48 points. Generating an image of letters for text like “Hello world!” is called rendering.
The Font object that you’ve stored in the basicFont variable has a method called render(). This method will return a Surface object with the text drawn on it. The first parameter to render() is the string of the text to draw. The second parameter is a Boolean for whether or not to anti-alias the font. Anti-aliasing blurs your text slightly to make it look smoother. Figure 17-3 shows what a line looks like with and without anti-aliasing.
Figure 17-3: An enlarged view of an aliased line and an anti-aliased line
On line 22, we pass True to use anti-aliasing:
21. # Set up the text.
22. text = basicFont.render('Hello world!', True, WHITE, BLUE)
The third and fourth parameters in line 22 are both RGB tuples. The third parameter is the color the text will be rendered in (white, in this case), and the fourth is the background color behind the text (blue). We assign the Surface object to the variable text.
Once we’ve set up the Font object, we need to place it in a location on the window.
The pygame.Rect data type (called Rect for short) represents rectangular areas of a certain size and location. This is what we use to set the location of objects on a window.
To create a new Rect object, you call the function pygame.Rect(). Notice that the pygame.Rect() function has the same name as the pygame.Rect data type. Functions that have the same name as their data type and create objects or values of their data type are called constructor functions. The parameters for the pygame.Rect() function are integers for the x- and y-coordinates of the top-left corner, followed by the width and height, all in pixels. The function name with the parameters looks like this: pygame.Rect(left, top, width, height).
When we created the Font object, a Rect object was already made for it, so all we need to do now is retrieve it. To do that, we use the get_rect() method on text and assign the Rect to textRect:
23. textRect = text.get_rect()
24. textRect.centerx = windowSurface.get_rect().centerx
25. textRect.centery = windowSurface.get_rect().centery
Just as methods are functions that are associated with an object, attributes are variables that are associated with an object. The Rect data type has many attributes that describe the rectangle they represent. In order to set the location of textRect on the window, we need to assign its center x and y values to pixel coordinates on the window. Since each Rect object already has attributes that store the x- and y-coordinates of the Rect’s center, called centerx and centery, respectively, all we need to do is assign those coordinates values.
We want to put textRect in the center of the window, so we need to get the windowSurface Rect, get its centerx and centery attributes, and then assign those to the centerx and centery attributes of textRect. We do that in lines 24 and 25.
There are many other Rect attributes that we can use. Table 17-2 is a list of attributes of a Rect object named myRect.
pygame.Rect attribute |
Description |
myRect.left |
Integer value of the x-coordinate of the left side of the rectangle |
myRect.right |
Integer value of the x-coordinate of the right side of the rectangle |
myRect.top |
Integer value of the y-coordinate of the top side of the rectangle |
myRect.bottom |
Integer value of the y-coordinate of the bottom side of the rectangle |
myRect.centerx |
Integer value of the x-coordinate of the center of the rectangle |
myRect.centery |
Integer value of the y-coordinate of the center of the rectangle |
myRect.width |
Integer value of the width of the rectangle |
myRect.height |
Integer value of the height of the rectangle |
myRect.size |
A tuple of two integers: (width, height) |
myRect.topleft |
A tuple of two integers: (left, top) |
myRect.topright |
A tuple of two integers: (right, top) |
myRect.bottomleft |
A tuple of two integers: (left, bottom) |
myRect.bottomright |
A tuple of two integers: (right, bottom) |
myRect.midleft |
A tuple of two integers: (left, centery) |
myRect.midright |
A tuple of two integers: (right, centery) |
myRect.midtop |
A tuple of two integers: (centerx, top) |
myRect.midbottom |
A tuple of two integers: (centerx, bottom) |
The great thing about Rect objects is that if you modify any of these attributes, all the other attributes will automatically modify themselves, too. For example, if you create a Rect object that is 20 pixels wide and 20 pixels tall and has the top-left corner at the coordinates (30, 40), then the x-coordinate of the right side will automatically be set to 50 (because 20 + 30 = 50).
Or if you instead change the left attribute with the line myRect.left = 100, then pygame will automatically change the right attribute to 120 (because 20 + 100 = 120). Every other attribute for that Rect object is also updated.
For our program, we want to fill the entire surface stored in windowSurface with the color white. The fill() function will completely cover the surface with the color you pass as the parameter. (Remember, the WHITE variable was set to the value (255, 255, 255) on line 13.)
27. # Draw the white background onto the surface.
28. windowSurface.fill(WHITE)
Note that in pygame, the window on the screen won’t change when you call the fill() method or any of the other drawing functions. Rather, these will change the Surface object, and you have to render the new Surface object to the screen with the pygame.display.update() function to see changes.
This is because modifying the Surface object in the computer’s memory is much faster than modifying the image on the screen. It is much more efficient to draw onto the screen once after all of the drawing functions have been drawn to the Surface object.
So far, we’ve learned how to fill a pygame window with a color and add text, but pygame also has functions that let you draw shapes and lines. Each shape has its own function, and you can combine these shapes into different pictures for your graphical game.
The pygame.draw.polygon() function can draw any polygon shape you give it. A polygon is a multisided shape with sides that are straight lines. Circles and ellipses are not polygons, so we need to use different functions for those shapes.
The function’s arguments, in order, are as follows:
The Surface object to draw the polygon on.
The color of the polygon.
A tuple of tuples that represents the x- and y-coordinates of the points to draw in order. The last tuple will automatically connect to the first tuple to complete the shape.
Optionally, an integer for the width of the polygon lines. Without this, the polygon will be filled in.
On line 31, we draw a green polygon onto our white Surface object.
30. # Draw a green polygon onto the surface.
31. pygame.draw.polygon(windowSurface, GREEN, ((146, 0), (291, 106),
(236, 277), (56, 277), (0, 106)))
We want our polygon to be filled, so we don’t give the last optional integer for line widths. Figure 17-4 shows some examples of polygons.
Figure 17-4: Examples of polygons
The pygame.draw.line() function just draws a line from one point on the screen to another point. The parameters for pygame.draw.line(), in order, are as follows:
The Surface object to draw the line on.
The color of the line.
A tuple of two integers for the x- and y-coordinates of one end of the line.
A tuple of two integers for the x- and y-coordinates of the other end of the line.
Optionally, an integer for the width of the line in pixels.
In lines 34 to 36, we call pygame.draw.line() three times:
33. # Draw some blue lines onto the surface.
34. pygame.draw.line(windowSurface, BLUE, (60, 60), (120, 60), 4)
35. pygame.draw.line(windowSurface, BLUE, (120, 60), (60, 120))
36. pygame.draw.line(windowSurface, BLUE, (60, 120), (120, 120), 4)
If you don’t specify the width parameter, it will take on the default value of 1. In lines 34 and 36, we pass 4 for the width, so the lines will be 4 pixels thick. The three pygame.draw.line() calls on lines 34, 35, and 36 draw the blue Z on the Surface object.
The pygame.draw.circle() function draws circles on Surface objects. Its parameters, in order, are as follows:
The Surface object to draw the circle on.
The color of the circle.
A tuple of two integers for the x- and y-coordinates of the center of the circle.
An integer for the radius (that is, the size) of the circle.
Optionally, an integer for the width of the line. A width of 0 means that the circle will be filled in.
Line 39 draws a blue circle on the Surface object:
38. # Draw a blue circle onto the surface.
39. pygame.draw.circle(windowSurface, BLUE, (300, 50), 20, 0)
This circle’s center is at an x-coordinate of 300 and y-coordinate of 50. The radius of the circle is 20 pixels, and it is filled in with blue.
The pygame.draw.ellipse() function is similar to the pygame.draw.circle() function, but instead it draws an ellipse, which is like a squished circle. The pygame.draw.ellipse() function’s parameters, in order, are as follows:
The Surface object to draw the ellipse on.
The color of the ellipse.
A tuple of four integers for the left and top corner of the ellipse’s Rect object and the width and height of the ellipse.
Optionally, an integer for the width of the line. A width of 0 means that the ellipse will be filled in.
Line 42 draws a red ellipse on the Surface object:
41. # Draw a red ellipse onto the surface.
42. pygame.draw.ellipse(windowSurface, RED, (300, 250, 40, 80), 1)
The ellipse’s top-left corner is at an x-coordinate of 300 and y-coordinate of 250. The shape is 40 pixels wide and 80 pixels tall. The ellipse’s outline is 1pixel wide.
The pygame.draw.rect() function will draw a rectangle. The pygame.draw.rect() function’s parameters, in order, are as follows:
The Surface object to draw the rectangle on.
The color of the rectangle.
A tuple of four integers for the x- and y-coordinates of the top-left corner and the width and height of the rectangle. Instead of a tuple of four integers for the third parameter, you can also pass a Rect object.
In the Hello World program, we want the rectangle we draw to be visible 20 pixels around all the sides of text. Remember, in line 23 we created a textRect to contain our text. On line 45 we set the left and top points of the rectangle as the left and top of textRect minus 20 (we subtract because coordinates decrease as you go left and up):
44. # Draw the text's background rectangle onto the surface.
45. pygame.draw.rect(windowSurface, RED, (textRect.left - 20,
textRect.top - 20, textRect.width + 40, textRect.height + 40))
The width and height of the rectangle are equal to the width and height of the textRect plus 40. We use 40 and not 20 because the left and top were moved back 20 pixels, so you need to make up for that space.
Line 48 creates a pygame.PixelArray object (called a PixelArray object for short). The PixelArray object is a list of lists of color tuples that represents the Surface object you passed it.
The PixelArray object gives you a high per-pixel level of control, so it’s a good choice if you need to draw very detailed or customized images to the screen instead of just large shapes.
We’ll use a PixelArray to color one pixel on windowSurface black. You can see this pixel on the bottom right of the window when you run pygame Hello World.
Line 48 passes windowSurface to the pygame.PixelArray() call, so assigning BLACK to pixArray[480][380] on line 49 will make the pixel at the coordinates (480, 380) black:
47. # Get a pixel array of the surface.
48. pixArray = pygame.PixelArray(windowSurface)
49. pixArray[480][380] = BLACK
The pygame module will automatically modify the windowSurface object with this change.
The first index in the PixelArray object is for the x-coordinate. The second index is for the y-coordinate. PixelArray objects make it easy to set individual pixels on a Surface object to a specific color.
Every time you create a PixelArray object from a Surface object, that Surface object is locked. That means no blit() method calls (described next) can be made on that Surface object. To unlock the Surface object, you must delete the PixelArray object with the del operator:
50. del pixArray
If you forget to do so, you’ll get an error message that says pygame.error: Surfaces must not be locked during blit.
The blit() method will draw the contents of one Surface object onto another Surface object. All text objects created by the render() method exist on their own Surface object. The pygame drawing methods can all specify the Surface object to draw a shape or a line on, but our text was stored into the text variable rather than drawn onto windowSurface. In order to draw text on the Surface we want it to appear on, we must use the blit() method:
52. # Draw the text onto the surface.
53. windowSurface.blit(text, textRect)
Line 53 draws the 'Hello world!' Surface object in the text variable (defined on line 22) onto the Surface object stored in the windowSurface variable.
The second parameter to blit() specifies where on windowSurface the text surface should be drawn. The Rect object you got from calling text.get_rect() on line 23 is passed for this parameter.
Since in pygame nothing is actually drawn to the screen until the function pygame.display.update() is called, we call it on line 56 to display our updated Surface object:
55. # Draw the window onto the screen.
56. pygame.display.update()
To save memory, you don’t want to update to the screen after every single drawing function; instead, you want to update the screen only once, after all the drawing functions have been called.
In our previous games, all of the programs would print everything immediately until they reached an input() function call. At that point, the program would stop and wait for the user to type something in and press ENTER. But pygame programs are constantly running through a game loop, which executes every line of code in the loop about 100 times a second.
The game loop constantly checks for new events, updates the state of the window, and draws the window on the screen. Events are generated by pygame whenever the user presses a key, clicks or moves the mouse, or performs some other action recognized by the program that should make something happen in the game. An Event is an object of the pygame.event.Event data type.
Line 59 is the start of the game loop:
58. # Run the game loop.
59. while True:
The condition for the while statement is set to True so that it loops forever. The only time the loop will exit is if an event causes the program to terminate.
The function pygame.event.get() checks for any new pygame.event.Event objects (called Event objects for short) that have been generated since the last call to pygame.event.get(). These events are returned as a list of Event objects, which the program will then execute to perform some action in response to the event. All Event objects have an attribute called type, which tell us the type of the event. In this chapter, we only need to use the QUIT event type, which tells us when the user quits the program:
60. for event in pygame.event.get():
61. if event.type == QUIT:
In line 60, we use a for loop to iterate over each Event object in the list returned by pygame.event.get(). If the type attribute of the event is equal to the constant variable QUIT—which was in the pygame.locals module we imported at the start of the program—then you know the QUIT event has been generated.
The pygame module generates the QUIT event when the user closes the program’s window or when the computer shuts down and tries to terminate all the running programs. Next, we’ll tell the program what to do when it detects the QUIT event.
If the QUIT event has been generated, the program will call both pygame.quit() and sys.exit():
62. pygame.quit()
63. sys.exit()
The pygame.quit() function is sort of the opposite of init(). You need to call it before exiting your program. If you forget, you may cause IDLE to hang after your program has ended. Lines 62 and 63 quit pygame and end the program.
In this chapter, we’ve covered many new topics that will let us do a lot more than we could with our previous games. Instead of just working with text by calling print() and input(), a pygame program has a blank window—created by pygame.display.set_mode()—that we can draw on. pygame’s drawing functions let you draw shapes in many colors in this window. You can create text of various sizes as well. These drawings can be at any x- and y-coordinate inside the window, unlike the text created by print().
Even though the code is more complicated, pygame programs can be much more fun than text games. Next, let’s learn how to create games with animated graphics.