What’s better than animation? Interactivity!
Now that you know how to put images on the screen and move them around ( [Hack #91] ), you’re two steps away from creating games. Step 1 is to make things interactive; step 2 is to add gameplay ( [Hack #93] ). Fortunately, with PyGame, it’s easy to make your animations respond to the human touch.
Building a small program to move a robot around on the screen with the cursor keys takes only a few minutes, yet it demonstrates the larger concepts of almost any interactive and visual game.
PyGame relies on SDL for its input, so to work with input in PyGame, you need to understand SDL events. Every time an input source generates input, such as a timer going off, the user pressing a key, or someone moving the mouse, SDL turns this into an event. It then puts this event in a queue for your program to handle later. Think of SDL events as an answering service; you don’t want to answer the telephone every time something happens, so you let your answering service buffer the interruptions so that you can filter through the list of messages when it’s convenient.
SDL events have one vitally important attribute, the
type designator. Every possible input type SDL
supports has its own type. The most important include
QUIT, KEYDOWN, and
KEYUP, though there are other types for mice,
joysticks, and window manager events. For keyboard events, both
KEYDOWN and KEYUP have a
subtype that represents the actual key pressed.
Why are there separate events for keyboard presses and releases?
Consider a space game that applies thrust to a rocket ship as long as
you hold down the Fire Engines key. Set a flag when you process the
KEYDOWN event, unset the flag when you process the
KEYUP event, and fire the engines only while the
flag is set.
PyGame’s event-handling support is in the
pygame.event class. The core of a main loop to
handle events is:
def loop( screen, background, player ):
key_events = {
K_UP: player.move_up,
K_DOWN: player.move_down,
K_LEFT: player.move_left,
K_RIGHT: player.move_right,
K_SPACE: player.stop,
}
while 1:
for event in pygame.event.get( ):
if event.type = = QUIT:
return
if event.type = = KEYDOWN:
if key_events.has_key( event.key ):
key_events[ event.key ]( )
elif event.key = = K_ESCAPE:
return
background.fill(( 255, 255, 255 ))
surface.blit( background, background.get_rect( ) )
player.draw( background )
screen.blit( background, ( 0, 0 ))
pygame.display.flip( )The first several lines define the key_events
dictionary that maps keys to actions. The uppercase symbols come from
the PyGame events library. The
player.move_
direction entries
are method calls on a Player object.
Don’t worry about the details right now; just think
of them as actions the player character should do. It can move up,
down, left, and right. It can also stop moving.
Within the main game loop, there’s another loop over
all of the events PyGame has processed. The two if
blocks check the type of the event. If the game receives a
QUIT event, such as when the player closes the
game window, the game returns from this function and exits the
program.
If the game receives a KEYDOWN event, it checks
the key_events dict to see if
there’s an action to perform for that key. If so, it
does that action (telling the Player object what
to do). Otherwise, if the user presses the Esc key, the game exits.
After this has processed all pending events, the loop redraws the screen—background first, then player—then updates it. The loop will continue from the start until the player quits.
Okay, that’s how to detect actions. How does the
player actually move? That’s the job of the
Player object.
Clearly, all of this input handling implies that there is some entity
in the program, a player, that knows how to move and draw itself.
That’s the purpose of the Player
class.
For the Player to do its job, it needs to keep
track of several pieces of information: its current position, the
maximum X and Y coordinates allowed, the image to display, and the
Player’s velocities along the X
and Y axes. Since there’s only one
Player at a time in this program, these could be
global variables. However, it’s easier to organize
them into separate units of behavior. This allows you to add new
Player objects in the future with minimal code
changes.
When you create a new Player object, Python calls
the special method _ _init_ _ to initialize values
for that object. Here’s the start of the class:
class Player:
def _ _init_ _( self, image, max_x, max_y ):
self.image = pygame.image.load( image ).convert_alpha( )
self.rect = self.image.get_rect( )
self.max_x = max_x - self.rect.width
self.max_y = max_y - self.rect.height
self.x_vel = 0
self.y_vel = 0
self.rect.x = random.randint( 0, self.max_x )
self.rect.y = random.randint( 0, self.max_y )The first line gives the name of this class. The second declares the information this class needs to initialize an object, specifically the object itself (handled automatically for you), the name of an image to load, and the maximum X and Y coordinates of the world.
The next line loads the image and converts its transparency information to work with SDL. The method then fetches the SDL rectangle representing this image and stores it in the class itself to make things more convenient.
The max_x and max_y coordinates
help keep the player from running off the right side and bottom of
the screen. Because the image coordinates start at the upper-left
corner of the image, you need to subtract the
image’s width and height from the maximum X and Y
coordinates, respectively, to figure out how far to the right and
down the player can travel without hitting a wall. Without this step,
the player can move all of the way off the screen.
x_vel and y_vel represent
velocities along the X and Y axes, respectively. When the player
presses a direction key, the input-handling code sets one of these
velocities. The player should start out stationary, so these values
start as zero.
As implied by the main loop earlier, the Player
needs several methods to perform its moments. They are:
def move_up( self ):
self.y_vel = -1
def move_down( self ):
self.y_vel = +1
def move_left( self ):
self.x_vel = -1
def move_right( self ):
self.x_vel = +1
def stop( self ):
self.x_vel = 0
self.y_vel = 0These are pretty straightforward; the Y coordinate controls the
player’s up and down movements, and the X coordinate
governs left and right. stop, of course, ceases
all motion.
What good does all this do? It doesn’t directly
affect the player’s position.
That’s the job of move():
def move( self ):
x = self.rect.x
y = self.rect.y
x_vel = self.x_vel
y_vel = self.y_vel
if ( x_vel and 0 < x < self.max_x ):
self.rect.x += x_vel
if ( y_vel and 0 < y < self.max_y ):
self.rect.y += y_velThis method finally calculates the player’s new
position. If x_vel isn’t zero (it
can be -1, 0, or 1), and if the player isn’t at the
farthest left or right edge of the screen, it adds that value to the
current X coordinate. The same goes for the Y velocity and
coordinate.
Finally, the image has to make it to the screen somehow.
That’s what draw() does:
def draw( self, surface ):
self.move( )
surface.blit( self.image, self.rect )The second argument to this method is the SDL surface to which to draw this image. Note that it uses the SDL rectangle within this object to govern its position. That’s all the magic.
Of course, that’s not all of the code it takes to
make the program work. There’s a little bit of
initialization to go, namely loading the appropriate PyGame modules
and creating a couple of surfaces and the Player
object:
#!/usr/bin/python import pygame import random from pygame.locals import *
The program starts by loading the pygame and
random modules then importing all symbols from
pygame.locals. These include the SDL key constants
used in the main loop.
Next, the program needs to initialize PyGame and the drawable objects:
def main( ):
pygame.init( )
pygame.display.set_caption( 'Robot Moves Around' )
max_x = 640
max_y = 480
screen = pygame.display.set_mode(( max_x, max_y ))
background = pygame.Surface( screen.get_size( ) ).convert( )
player = Player( 'robot.png', max_x, max_y )
loop( screen, background, player )
if _ _name_ _ = = '_ _main_ _': main( )The first two lines initialize PyGame and set the caption of the
window appropriately. max_x and
max_y define the size of the window to create.
background is an SDL surface the same size as the
screen.
The Player() call creates a new
Player object, passing the
robot.png filename and the maximum X and Y
values. The function then kicks off the game by calling
loop(), passing in the three drawable objects.
Finally, if you call the program directly, the last line of code
launches the main() function:
$ python move_robot.pyor the equivalent for your platform.
There’s plenty of room to experiment with other
options for handling movement. One approach is to move the player
when encountering a KEYDOWN event. However, if the
player moves only one pixel at a time (for smoothest movement),
it’ll take several hundred keypresses to reach the
other side of the screen.
Another approach is to set a movement flag for each
KEYDOWN event seen, and unset it when a
corresponding KEYUP event is received. This allows
players to hold down a key for as long as the character should move.
More sophisticated systems make holding down a key actually increase
the character’s velocity.
This program merely sets the appropriate X or Y velocity when it encounters a key press. Holding down the key has no effect, and releasing the key has no effect. You can only move in the opposite direction or stop.