A Quick Pygame Tutorial
Pygame is a Python wrapper for an API called SDL, or Simple Direct Layer. SDL is a wrapper for a number of basic hardware operations, mostly involving images, but SDL also incorporates functions to handle input devices--joysticks, keyboards, mice, cameras--and output channels such as sound. Pygame further abstracts SDL, and provides a set of tools that are useful for making games or other interactive interfaces.
As a game engine, Pygame is a very low level API. The programmer is responsible for handling events, for moving the elements of the game, for checking for collisions between elements, and for making sure the elements get drawn in the proper order. There is no gravity or physics implemented as part of Pygame, nor is there any framework for managing different screens, levels, or game phases. All of these things you will need to code yourselves or else use a package someone else has written on top of Pygame.
Nevertheless, the Pygame API is fairly easy to use and it is an effective base on which to build a 2D or 2.5D game. While the top level code is being run in Python, most of the heavy-duty computation is done within the compiled Pygame modules. Therefore, you can write some fairly complicated games and still expect decent framerates, so long as you follow some common sense guidelines about where the real work needs to happen.
The following is a simple tutorial designed to get you working with the basic capabilities of Pygame. It explores the concepts of surfaces, text, main program loops, moving characters and sprites, events, collisions, and sound.
Inspiration and much of the knowledge provided in this tutorial comes from other tutorials available on the web, particularly those on the Pygame web site. The following are probably all worth reading as you start to learn Pygame.
- A Newbie Guide to Pygame
- Python Pygame Introduction
- Sprite Module Introduction
- Invent with Python
- Pygame Documentation Front Page
Surfaces: Surfaces form the core of the visual part of your game. A surface is a 2D rectangle of image data. Except for the display screen, a surface is not anchored to any particular location on the screen; it is just a bunch of image data stored in memory.
There are many ways to create/modify surfaces. You can read image data from a file, you can draw simple shapes on the surface, or you can render text onto a surface. Once you have a surface, you can draw its contents, or just parts of its contents, onto another surface in an arbitrary location using a blit operation.
Download the first pygame tutorial to go through how to draw various things onto a screen. This example creates a static screen with various types of content on it. To run the program, you will also need to download the images below. Put them in the same directory as the Python file. When you run the program it should create a screen like the image below.
The first section of the file sets up the various components of Pygame. This includes importing the pygame module, initializing pygame, initializing the pygame font manager, and creating a screen, which makes a new window for the program.
The second section loads two images using the pygame.image.load function. Note that before assigning the surfaces it also calls the function convert_alpha(). The convert function adjusts the way the image data is stored so that it best matches the hardware. The convert_alpha variation is useful when reading in an image with an alpha mask. For example, the two images Spider.png and Broom.png were both stored with a transparency layer, or alpha mask. That means some of the image should be invisiible when the Spider or Broom images are drawn (the parts that are not part of the spider or the broom). The convert_alpha function takes that alpha channel information and readjusts the way it is stored to optimize the speed of drawing the image onto the screen. Small details like this are important when you are trying to make a game run fast.
The second section also creates a font object and then uses that font object to render some text onto a new surface. Once we have the surface with the text on it, we can draw that text onto other surfaces, as needed.
The third section of this first example shows how to put the content onto the screen. There are two components to this. First, you must blit the data from its source surface onto the target surface (the screen). Then you have to tell the screen to update. In this case, since time is not an issue, we use the pygame.display.update() function, which updates the entire screen.
The last section of the program is a simple event loop that waits for the user to do one of three things: click the mouse, press a key, or close the window. If any of these events occur, the program calls sys.exit() to terminate the program.
Main Loop: the main loop of any game program needs to run continuously and needs to run fast. Unlike some other types of programs that first accomplish one task, then accomplish another, a computer game must always be able to react quickly to inputs and refresh the screen so the motion of elements of the game appears smooth. Any delays in reacting to user input or displaying the results of that interaction creates a disconnect between the player and the game world. Any action that takes a long time must be broken into small pieces and executed as a series of fast steps.
During gameplay, your game will have a main loop that needs to run continuously for the duration of the game. Ideally, the loop should always run at the same speed. One time through the loop is often called a tick in game design parlance. A tick is usually 1/30th of a second, which means your program needs to be updating the screen and be responding to events 30 times per second. At that framerate, most motions will appear smooth.
Your main loop has to do many things, but you have only 33ms in which to do them. If your main loop takes longer that 33ms to execute, then your game will run slower than 30Hz, which may cause problems with the animations or response of the game to user inputs.
There are four main tasks your game has to do each time through the main loop.
- Handle any events, such as key presses or mouse clicks.
- Erase any parts of the screen that need to change
- Update the state of the game, including the locations of any moving actors.
- Draw all of the new or changed content and update the screen.
Download the second example program.
There is one small change to the Setup section of the program. In order to control the framerate of the main loop, we need a clock. The pygame.time.Clock() call creates a clock we can use for that purpose.
The second section, as with the first example, reads in two images and creates a surface with the text "Clean up time".
The third section is a function that draws the static elements of the background. The function takes in four arguments. The first is the screen surface, the second is the text surface, the third is a list of rectangles, and the fourth is an optional rectangle.
The refresh list is used when we are updating the screen. It contains all of the areas of the screen that might have changed during one pass of the main loop. Any time we blit one surface onto another or draw something onto the screen we need to tell the system that part of the image needs to be updated at the end of the loop.
The last argument is a single rectangle that indicates what part of the background needs to be drawn. The key to a fast game is to draw only those parts of the screen that change from one tick to the next.
Inside the function, if no drawing rectangle is provided, then the function clears the entire screen and then blits the text into its location. A rectangle the size of the entire screen is added to the refresh list so the whole screen is updated. If a drawing rectangle is provided, then only the contents of that rectangle get updated. In particular, we use Rect functions to identify the part of the text that needs to be updated, if any, and only that part of the text gets blitted to the screen and added to the refresh list.
The fourth section sets up the broom as a sprite that follows the mouse as it moves over the screen. It gets the mouse location, then moves the broom's rectangle to be centered on the mouse and stores it in the broomRect variable. Then it blits the broom to the screen.
The final section is the main loop. Inside the event loop we have added the MOUSEMOTION event. If the mouse moves, then we draw the background over the old mouse location, erasing the old broom image.
After checking for events, the main loop draws the spider. Then, if the game window is in focus, it updates the position of the broom and blits the broom onto the screen. Finally, the pygame.display.update( refresh ) function call updates those parts of the screen indicated by the rectangles in the refresh list.
The last thing in the main loop is a call to the gameClock.tick(30) function with the argument of 30, which means it should throttle the speed of the main loop so it is no faster than 30Hz, or 30 frames per second. Your loop could always go slower if you are doing too much processing, but with the tick function call it will never go faster than that.
Collisions: Collisions are an important part of any game. In Pygame, it is the Rect class that provides the tools for quick collision testing.
Download the third example program. The setup, background, and broom setup sections do not change from example two.
The content section adds only a line to specify the collision box for the spider. If we use the entire surface rectangle as a collision rectangle then it will look like the broom and spider never touch when a collision is detected. The collision box is a smaller area that surrounds only the spider's body and legs. Note this rectangle is in the coordinate system of the spider, so we'll need to move this around as the spiders change locations on the screen.
To set up a group of spiders that appear on the screen we first specify how many spiders we want (maximum) as well as how much delay there is between spiders spawning on the screen. We also create variables to hold the list of Rect objects of active spiders and a variable remembering the number of ticks since the last spawn event.
The changes to the main event loop are primarily in the middle. The first section updates the state of the program by checking if we need to spawn a new spider. If the spawn conditions are met, then a new randomly placed rectangle is put into the activeSpiders list and the ticksSinceLastSpawn variable is reset to zero.
The next step is to create the background behind each of the current spiders. Another way to think about this step is that all of the spiders are erased and the background gets filled in at their locations.
The next step is to figure out if the broom is intersecting any of the spiders. If so, the spider should be removed from the activeSpider list. Another way to think about it is that we can create a new temporary list and put any live spiders into it, copying it back to the activeSpiders list after the test. In order to test for collisions, we use the colliderect function to check of the broom and spider collision boxes overlap. Note that we use the move function to adjust the collision boxes from the surface coordinate system to the screen coordinate system. The move function does not modify the Rect; it returns a new Rect with the modification.
The final step is to blit all the active, still alive spiders onto the screen. If we wanted to update their locations, or have the spiders move around, this loop would be a reasonable place to do that.
We add only two lines of code to bring sound to this example. The first line is in the content section where we load the sweep.wave file and store it in a sound object. The second line of code is in the case where the broom collides with a spider. Then we call sweep.play() to play the short sweeping sound.
Clearly, this is not a complete game (no score, no opposition, no purpose), but it has most of the visual elements and capabilities of a game. Now you can get started on real game and have some fun.