CS 269: Computer Game Design

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.

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.

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.