The Pygame Library

[ LiB ]

The Pygame Library

Pygame is a Python wrapper for the Simple DirectMedia Layer (SDL). Pygame focuses on bringing the world of graphics and game programming to programmers in an easy and efficient way.

Typically, Pygame projects are small, simple, two-dimensional or strategy games . In Chapter 5, I'll give you a close look at a few existing Pygame-based game engines, including Pyzzle, a Myst -like engine; PyPlace, a two-dimensional isometric engine; and AutoManga, a cell -based anime-style animation engine.

Installing Pygame

This book's CD comes with a copy of Pygame in the \PYTHON\PYGAME folder. The most recent versions can be found online at http://www.pygame.org/download.shtml.

The Windows binary installer on the CD has versions for Python 2.3 and 2.2, there is a Mac .sit for older Mac versions and a version for the Mac OSX, and the RPM binary has been included for the Red Hat operating system. Pygame actually comes with the most recent and standard UNIX distributions, and can be automatically built and installed by the ports manager.

On Windows, the binary installer will automatically install Pygame and all the necessary dependencies. A large Windows documentation package, along with sample games and sample code, is available at the Pygame homepage at http://www.pygame.org.

Pygame also requires an additional package, called the Numeric Python package, in order to use a few of its sleeker and quicker array tools. This package can be found in the Python section of the accompanying CD. At this time, the creators of Numeric Python are working

NOTE

SDL

SDL is considered an alternative to Direct X especially on Linux machines. As a multimedia and graphics library, SDL provides low-level access to a computer's video, sound, keyboard, mouse, and joystick.

SDL is similar in structure to a very rudimentary version of Microsoft's Direct X API, the big difference being that SDL is open source, supports multiple operating systems (including Linux, Mac, Solaris, FreeBSD, and Windows), and has an API binding to other languages, including Python.

SDL is written in C and available under the GNU Lesser General Public License. Sam Lantinga, who worked for both Loki Software and Blizzard entertainment, is the genius behind SDL. He got his start with game programming in college by porting a Macintosh game called Maelstrom to the Linux platform.

Sam was working on a Windows port of a Macintosh emulator program called Executor and figured that the code he was building to extract the emulator's graphics, sound, and controller interface could be used on other platforms. Late in 1997 he went public with SDL as an open-source project, and since then SDL has been a contender.

on an even faster version called Numeric. If you need to install the Numeric package, use the .exe for Windows or the tar.gx for Posix environments. Numeric is distributed under an OSI license just like Python itself, and the latest development can be found at http:// sourceforge .net/projects/numpy.

The Mac OS X tar includes Python 2.2, Pygame1.3 (hacked for Macs), PyOpenGL, and Numeric. There are still some bugs and issues with SLD compatibility on pre-OS X and post-OS X, and a simple installer for the Mac that should fix most of these issues is planned for when Python 2.3 is released.

NOTE

CAUTION

Do not use stuffit to untar the pack age on Mac OS X. Stuffit will trun cate some of the larger filenames.

Pygame is distributed under the GNU LGPL, license Version 2.1. See Figure 4.1 for a shot of Pygame installation.

Figure 4.1. Installing Pygame

graphic/04fig01.gif


Using Pygame

Pygame itself is fairly easy to learn, but the world of computer games and graphics is often unforgiving to beginners . Pygame has also suffered criticism for its lack of documentation. This lack of documentation leads many new developers to browse through the Pygame package, looking for information. However, if you browse through the package, you will find an overwhelming number of classes at the top of the index, making the package seem confusing. The key to starting with Pygame is to realize that you can do a great deal with just a few functions, and that you may never need to use many of the classes.

Importing Pygame

The first step towards using Pygame after it has been installed is to import the Pygame and other modules needed for development into your code. Do the following:

 import os, sys import pygame from pygame.locals import * 

Keep in mind that Python code is case-sensitive, so for Python, Pygame and pygame are totally different creatures . Although I capitalize Pygame in this book's text, when importing the module, pygame needs to be in all lowercase letters .

First import a few non-Pygame modules. You'll use the os and sys libraries in the next few examples for creating platform independent files and paths. Then import Pygame itself. When Pygame is imported, it doesn't actually import all of the Pygame modules, as some are optional. One of these modules, called locals , contains a subset of Pygame with commonly used functions like rect and quit in the easy-to-access global namespace. For the upcoming examples the locals module will be included so that these functions will be available as well.

NOTE

TIP

The Pygame code repository is a community-supported library of tools and code that utilizes Pygame. The source code is managed by Pygame, but submissions are from users of the library. The repository holds a number of useful code snip petseverything from visual effects to common game algorithmsand can be found at http://www.pygame.org/pcr/ .

The Pygame Surface

The most important element in Pygame is the surface. The surface is a blank slate, and is the space on which you place lines, images, color , and so on. A surface can be any size, and you can have any number of them. The display surface of the screen is set with:

 Pygame.display.set_mode() 

You can create surfaces that have images with image.load() , surfaces that contain text with font.render() , and blank surfaces with Surface() . There are also many surface functions, the most important being blit() , fill() , set_at() , and get_at() .

The surface.convert() command is used to convert file formats into pixel format; it sets a JPEG, GIF, or PNG graphic to individual colors at individual pixel locations.

NOTE

TIP

Using surface.convert is impor tant so that SDL doesn't need to convert pixel formats on-the-fly . Converting all of the graphic images into an SDL format on-the-fly will cause a big hit to speed and performance.

Loading a surface image is fairly simple:

 My_Surface = pygame.image.load('image.jpeg') 

as is converting an image:

 My_Surface = pygame.image.load('image.jpeg').convert() 

A conversion only needs to be done once per surface, and should increase the display speed dramatically.

Drawing on the display surface doesn't actually cause an image to appear on the screen. For displaying, the pygame.display.update() command is used. This command can update a window, the full screen, or certain areas of the screen. It has a counterpart command, pygame.display.flip() , which is used when using double-buffered hardware acceleration.

NOTE

CAUTION

The convert() command will actu ally rewrite an image's internal format. This is good for a game engine and displaying graphics, but not good if you are writing an image-conversion program or a program where you need to keep the original format of the image.

Creating a Pygame Window

Creating a window in which Pygame can run an application is fairly easy. First you need to start up Pygame with an initialize command:

 pygame.init() 

Then you can set up a window with a caption by using Pygame's display command:

 My_Window = pygame.display.set_mode((640, 480)) 

This code run by itself (the code is included as the My_Window.py example in this chapter's code section on the CD) creates a 640x480-pixel window labeled Pygame Window, just like in Figure 4.2. Of course, the window accomplishes nothing, so it immediately disappears after showing up on the screen.

Figure 4.2. A simple Pygame window

graphic/04fig02.gif


The Ever-Important rect()

The most used class in Pygame probably the rect() class, and it is the second most important concept in Pygame. rect() is a class that renders a rectangle:

 My_Rectangle  = pygame.rect() 

rect() comes with utility functions to move, shrink, and inflate itself; find a union between itself and other rects; and detect collisions. This makes rect() an ideal class for a game object. The position of a rect() is defined by its upper-left corner. The code that rect s use to detect overlapping pixels is very optimized, so you will see rect s used in sprite and other sorts of collision detection. For each object, there will often be a small rect() underneath to detect collisions.

The Event System

In order for Pygame to respond to a player or user event, you normally set up a loop or event queue to handle incoming requests (mouse clicks or key presses). This loop is a main while loop that checks and makes sure that the player is still playing the game:

 still_playing = 1 while (still_playing==1):         for event in pygame.event.get():                 if event.type is QUIT:                         still_playing = 0 

The for event line uses pygame.event.get() to get input from the user. Pygame understands basic Windows commands, and knows that QUIT is equivalent to pressing the X at the top right corner of a created window. The pygame.event function is used to handle anything that needs to go into the event queuewhich is basically input from any sort of device, be it keyboard, mouse, or joystick. This function basically creates a new event object that goes into the queue. The pygame.event.get function gets events from the queue. The event members for pygame.event are

  • QUIT. Quit or Close button.

  • ACTIVEEVENT. Contains state or gain.

  • KEYDOWN. Unicode key when pressed.

  • KEYUP. Uncode key when released.

  • MOUSEMOTION. Mouse position.

  • MOUSEBUTTONUP. Position mouse button releases.

  • MOUSEBUTTONDOWN. Position mouse button pressed.

  • JOYAXISMOTION. Joystick axis motion.

  • JOYBALLMOTION. Trackball motion.

  • JOYHATMOTION. Joystick motion.

  • JOYBUTTONUP. Joystick button release.

  • JOYBUTTONDOWN. Joystick button press.

  • VIDEORESIZE. Window or video resize.

  • VIDEOEXPOSE. Window or video expose.

  • USEREVENT. Coded user event.

These are normally used to track keyboard, mouse, and joystick actions. Let's say you wanted to build in mouse input handling. All mouse input is retrieved through the pygame.event module.

 if event.type is MOUSEBUTTONDOWN:         # do something 

Pygame also has a number of methods to help it deal with actual mouse position and use; these are listed in Table 4.1.

Table 4.1. Pygame Mouse Event Methods

Method

Purpose

get_cursor

Gets the mouse cursor data

get_focused

Gets the state of the mouse input focus

get_pos

Gets the cursor position

get_pressed

Gets the state of the mouse buttons

get_rel

Grabbing mouse movement

set_cursor

Sets the state of the shape of the mouse cursor

set_pos

Moves the cursor

set_visible

Displays or hides the mouse cursor


You can check the state of a mouse or keyboard event by using pygame.mouse.get_pos() or pygame.key.get_pressed() , respectively.

Drawing with Pygame

Pygame has great built-in functions for graphics. These functions revolve around the idea of the surface, which is basically an area that can be drawn upon. Let's say you wanted to fill the background in My_Window.py with a color. First grab the size of the window:

 My_Background = pygame.Surface(My_Window.get_size()) 

This creates a surface called My_Background that's the exact size of My_Window. Next convert the surface to a pixel format that Pygame can play with:

 My_Background = My_Background.convert() 

And finally, fill the background with a color (set with three RGB values):

 My_Background.fill((220,220,80)) 

Now let's do some drawing over the background surface. Pygame comes with a draw function and a line method, so if you wanted to draw a few lines, you could do this:

 pygame.draw.line (My_Background, (0,0,0,),(0,240),(640,240), 5) pygame.draw.line (My_Background, (0,0,0), (320,0), (320,480), 5) 

Pygame's draw.line takes five parameters. The first is the surface to draw on, the second is what color to draw (again in RGB values), and the last is the pixel width of the line. The middle parameters are the start and end points of the line in x and y pixel coordinates. In this case, you draw the thick lines crossing in the exact center of the window, as shown in Figure 4.3.

Figure 4.3. Pygame's draw.line is used to split My_Window into four sections

graphic/04fig03.gif


The easiest way to display the background and lines is to put them into a draw function:

 def draw_stuff(My_Window): My_Background = pygame.Surface(My_Window.get_size()) My_Background = My_Background.convert() My_Background.fill((220,220,80)) pygame.draw.line (My_Background, (0,0,0,),(0,240),(640,240), 5)         pygame.draw.line (My_Background, (0,0,0), (320,0), (320,480), 5) return My_Background 

Then call the function within the loop that exists (and illustrated as code sample My_Window_3.py on the CD):

 My_Display = draw_stuff(My_Window) My_Window.blit(My_Display, (0,0)) pygame.display.flip() 

Blitting

Blitting ( Block Image Transfering ) is practically synonymous with rendering, and specifically means redrawing an object by copying the pixels of said object onto a screen or background. If you didn't run the blit() method, nothing would ever get redrawn and the screen would just remain blank. For those of you who must know, blit isn't a made-up wordit's short for "bit block transfer."

In any game, blitting is often a process that slows things down, and paying attention to what you are blitting, when you are blitting, and how often you are blitting will have a major impact on your game's performance. The key to a speedy graphics engine is blitting only when necessary.

The blit method is very important in Pygame graphics. It is used to copy pixels from a source to a display. In this case, blit takes the pixels plotted in My_Display (which took the commands from draw_stuff ) and copies them to My_Window . The blit method understands special modes like colorkeys and alpha, it can use hardware support if available, and it can also carry three-dimensional objects in the form of an array (using blit_array() ). In this example, blit is taking My_Display as the input and rendering it to My_Window , and it uses the upper-left corner (pixel 0,0) to key up the surface.

The pygame.display.flip() command is Pygame's built-in function for updating the entire display (in this case, the entirety of My_Window ) once any graphic changes are made to it.

NOTE

TIP

In Windows, you can add a single "w" to the end of a Python file (so that instead of ending it in py, it ends in pyw) to make the pro gram open up a window without opening up the interpret console, that funny -looking DOS box.

Loading an Image with Pygame

Image loading is an oft-needed function in games; in this section I'll show you the steps for loading an image in Pygame.

After importing the necessary modules, you need to define a function for loading an image that will take an argument. The argument will be used to set the colorkey (the transparency color) of the image; it looks like this:

 def load_image(name, colorkey=None): 

Colorkey blitting involves telling Pygame that all pixels of a specific color in an image should be transparent. This way, the image square doesn't block the background. Colorkey blitting is one way to make non-rectangular , two-dimensional shapes in Pygame. The other common trick is to set alpha values using a graphics program like Adobe Photoshop, as illustrated in Figure 4.4 and explained in the following sidebar.

Figure 4.4. Setting alpha values using Adobe Photoshop

graphic/04fig04.gif


To turn colorkey blitting on, you simply use surface.set_colorkey(color) . The color fed to surface.set_colorkey is three-digit tuple (0,0,0) with the first number being the red value, the second green, and the third blue (that is, rgb).

NOTE

Colorkey versus Alpha

Both colorkey and alpha are techniques for making parts of a graphic transparent when traveling across the screen. In Pygame, most 2D game objects and sprites are rect s, and are rectangular in shape. This means you need a way to make part of the rectangle transparent, so that you can have circular, triangular , or monkey -shaped game pieces. Otherwise you would only be capable of displaying square pieces over a background.

Alpha is one technique for making parts of an image transparent. An alpha setting causes the source image to be translucent or partially opaque . Alpha is normally measured from 0 to 255, and the higher the number is the more transparent the pixel or image is. Alpha is very easy to set in a graphic editor (like Adobe Photoshop), and Pygame has a built-in get_alpha() command. There is also per-pixel alpha where you can assign alpha values to each individual pixel in a given image.

When using a colorkey technique (sometimes called colorkey blitting ) you let the image renderer know that all pixels of one certain color are to be set as transparent. Pygame has a built-in colorkey(color) function that takes in a tuple in the form of RGB. For instance, set_colorkey(0,0,0) would make every black pixel in a given image transparent.

You'll use both techniques in this chapter. The load_image function in this section uses set_colorkey() , while the load_image command in the Monkey_Toss.py graphics example later on in the chapter uses get_alpha .

The module needs to know where to grab the image, and this is where the os module comes into play. You'll use the os path function to create a full pathname to the image that needs to be loaded. For this example, say that the image is located in a "data" subdirectory, and then use the os.path.join function to create a pathname on whatever system (Mac, Windows, UNIX) that Python is running on.

 fullname = os.path.join('data', name) 

Try/except Code Blocks

Being able to fail gracefully is important in programming. Basically, you always need to leave a back door, or way out of a program, for if an error occurs. You'll find that try/except or try/finally constructs are very common.

Python offers a try/except/else construct that allows developers to trap different types of errors and then execute appropriate exception-handling code. try/except actually looks just like a series of if/elif/else program flow commands:

 try:         execute this block except error1:         execute this block if "error1" is generated except error2:         execute this block if "error2" is generated else:         execute this block 

This structure basically allows for the execution of different code blocks depending on the type of error that is generated. When Python encounters code wrapped within a try-except - else block, it first attempts to execute the code within the try block. If this code works without any exceptions being generated, Python then checks to see if an else block is present. If it is, that code is executed.

If a problem is encountered while running the code within the try block, Python stops execution of the try block at that point and begins checking each except block to see if there is a handler for the problem. If a handler is found, the code within the appropriate except block is executed. Otherwise, Python jumps to the parent try block, if one exists, or to the default handler (which terminates the program).

A try/except structure is used to load the actual image using Pygame's image.load. Do this through a try/except block of code in case there is an error when loading the image:

 try:         image=pygame.image.load(fullname) except pygame.error, message:                 print 'Cannot load image:', name                 raise SystemExit, message 

Once the image is loaded, it should be converted. This means that the image is copied to a Pygame surface and its color and depth are altered to match the display. This is done so that loading the image to the screen will happen as quickly as possible:

 image=image.convert() 

The next step is to set the colorkey for the image. This can be the colorkey provided when the function was called, or a -1 . If the -1 is called, the value of colorkey is set to the top-left (0,0) pixel. Pygame's colorkey expects an RGBA value, and RLEACCEL is a flag used to designate an image that will not change over time. You use it in this case because it will help the speed of the image being displayed, particularly if the image must move quickly.

 if colorkey is not None:         if colorkey is -1:                 colorkey = image.get_at((0,0))         image.set_colorkey(colorkey, RLEACCEL) 

The final step is to return the image object as a rect (Like I've said, Pygame is based on rect s and surfaces) for the program to use:

 return image, image.get_rect() 

The full code snip for load_image is listed here and also on the CD, as Load_Image.py :

 def load_image(name, colorkey=None): fullname = os.path.join('data', name)         try:                 image=pygame.image.load(fullname)         except pygame.error, message:                 print 'Cannot load:', name                 raise SystemExit, message         image=image.convert()         if colorkey is not None:                  if colorkey is -1:                         colorkey = image.get_at((0,0))                 image.set_colorkey(colorkey, RLEACCEL)         return image, image.get_rect() 

Displaying Text

Pygame has, of course, methods for dealing with text. The pygame.font method allows you to set various font information attributes:

 My_Font = pygame.font.Font(None, 36) 

In this case, you set up a My_Font variable to hold Font(None, 36) , which establishes no particular font type ( None , which will cause a default font to be displayed) and a 36 font size ( 36 ). Step 2 is to choose what font to display using font.render :

 My_Text = font.render("Font Sample", 1, (20, 20, 220)) 

The arguments passed to font.render include the text to be displayed, whether the text should be anti-aliased (1 for yes, 0 for no), and the RGB values to determine the text's color. The third step is to place the text in Pygame's most useful rect() :

 My_Rect = My_Text.get_rect() 

Finally, you get the center of both rect() s you created and the background with Python's super-special centerx method (which is simply a method for determining the exact center of something), and then call the blit() method to update:

 My_Rect.centerx = My_Background.get_rect().centerx background.blit(My_Text, My_Rect) 

A Pygame Game Loop

A Pygame game loop is usually very straightforward. After loading modules and defining variables and functions, you just need a loop that looks at user input and then updates graphics. This can be done with only a few lines of code. A typical event loop in a game would look something like this:

 while 1:         for event in pygame.event.get():                 if event.type == QUIT:                         #exit or quit function goes here                         return                 screen.blit(MY_Window, (0, 0))                 pygame.display.flip() 

The pygame.event module looks for user input, and pygame.blit and pygame.display keep the graphics going. Let's say, for example, that you wanted to look specifically for up or down arrow keys for player control. To do so, you could simply add elif statements to the event loop:

 while 1:         for event in pygame.event.get():                 if event.type == QUIT:                         #exit or quit function goes here                         return                 # Add to listening for arrow keys In the event queue                 elif event.type == KEYDOWN:                         If event.key == K_UP                                 # do something                         If event.key == K_DOWN                                 # do something                 screen.blit(MY_Window, (0, 0))                 pygame.display.flip() 

Pygame Sprites

Originally computers were simply incapable of drawing and erasing normal graphics fast enough to display in real-time for purpose of a video game. In order for games to work, special hardware was developed to quickly update small graphical objects, using a variety of special techniques and video buffers. These objects were dubbed sprites . Today sprite usually refers to any animated two-dimensional game object.

Sprites were introduced into Pygame with Version 1.3, and the sprite module is designed to help programmers make and control high-level game objects. The sprite module has a base class Sprite , from which all sprites should be derived, and several different types of Group classes, which are used as Sprite containers.

When you create a sprite you assign it to a group or list of groups, and Pygame instantiates the sprite game object. The sprite can be moved, its methods can be called, and it can be added or removed from other groups. When the sprite no longer belongs to any groups, Pygame cleans up the sprite object for deletion (alternately, you can delete the sprite manually using the kill() method).

The Group class has a number of great built-in methods for dealing with any sprites it owns, the most important being update() , which will update all sprites within the group. Several other useful group methods are listed in Table 4.2.

Table 4.2. Useful Group Methods

Method

Use

add()

Adds a sprite to the group

copy()

Makes a copy of the group with all of its members

empty()

Removes all sprites within the group

len()

Returns how many sprites the group contains

remove()

Removes sprite from the group

truth()

Returns true if group has any sprites

update()

Calls an update method on each sprite within the group


Groups of sprites are very useful for tracking game objects. For instance, in an asteroid game, player ships could be one group of sprites, asteroids could be a second group, and enemy starships a third group. Grouping in this way can make it easy to manage, alter, and update the sprites in your game code.

Memory and speed are the main reasons for using sprites. Group and sprite code has been optimized to make using and updating sprites very fast and low-memory processes. Pygame also automatically handles cleanly removing and deleting any sprite objects that no longer belong to any groups.

Updating an entire screen each time something changes can cause the frames -per-second rate to dip pretty low. Instead of updating the entire screen and redrawing the entire screen normally, an engine should only change the graphics that have actually changed or moved. The engine does this by keeping track of which areas have changed in a list and then only updating those at the end of each frame or engine cycle. To help out in this process, Pygame has different types of groups for rendering. These methods may not work with a smooth-scrolling, three-dimensional, realtime engine, but then again, not every game requires a whopping frame-rate. Pygame's strength lies elsewhere.

Besides the standard Group class there is also a GroupSingle , a RenderPlain , a RenderClear , and a RenderUpdates class (see Figure 4.5). GroupSingle can only contain one sprite at any time. Whenever a sprite is added to GroupSingle , any existing sprite is forgotten and set for deletion. RenderPlain is used for drawing or blitting a large group of sprites to the screen. It has a specific draw() method that tracks sprites that have image and rect attributes. RenderPlain is a good choice as a display engine for a game that scrolls through many backgrounds but not any rects , like scrolling games where the player stays in a consistent area of the screen and the background scrolls by to simulate movement. RenderClear has all the functionality of RenderPlain but also has an added clear() method that uses a background to cover and erase the areas where sprites used to reside. RenderUpdates has all the functionality of RenderClear , and is also capable of tracking any rect (not just sprites with rect attributes) for rendering with draw() .

Figure 4.5. Sprite container classes

graphic/04fig05.gif


Sprites also have built-in collision detection. The spritecollide() method checks for collisions between a single sprite and sprites within a specific group, and will return a list of all objects that overlap with the sprite if asked to. It also comes with an optional dokill flag, which, if set to true, will call the kill() method on all the sprites.

A groupcollide() method checks the collision of all sprites between two groups and will return a dictionary of all colliding sprites if asked to. Finally, the spritecollideany() method returns any single sprite that collides with a given sprite. The structure of these collision methods is:

 pygame.sprite.spritecollide(sprite, group, kill?) ->list pygame.sprite.groupcollide(group1, group2, killgroup1?, killgroup2?) -> dictionary pygame.sprite.spritecollideany(sprite, group) -> sprite 

Here is an example of a collision that checks to see whether My_Sprite ever collides with My_Player , and removes the offending My_Sprite sprite:

 for My_Sprite in sprite.spritecollide(My_Player, My_Sprite, 1):         #What happens during the collision plays out here 

When using Pygame sprites, you need to keep a few things in mind. First, all sprites need to have a rect() attribute in order to use the collide() or most other built-in methods. Second, when you call the Sprite base class to derive your sprite, you must call the sprite_init_() method from your own class_init_() method.

Game Object Classes

Python being a pseudoobject-oriented language, normally game classes are created first, then specific instances of game objects are initiated from the created classes. Let's walk through creating an example class, a banana :

 class Banana:         # _init_ method # banana method         # banana method 2         # banana method 3 def main         My_Banana = Banana() 

This is roughly how a class works. The Banana class needs at least an _init_ method, and will likely contain many more. After the class is created, simply call the class to create an instance called My_Banana in the main loop.

Since an _init_ method is mandatory, let's take a look at what that method would look like first:

 class Banana(pygame.sprite.Sprite):         def _init_(self):                 pygame.sprite.Sprite._Init_(self) 

The Banana class is set up as a Pygame sprite. When you define the _init_ method, you must specify at least one parameter that represents the object of the class for which the method is called. By convention, this reference argument is called self .

You may want to add other specifications to the _init_ method. For instance, you may wish to specify an image / rect and load up a graphic. You may also want to tie the Banana class to the screen:

 class Banana(pygame.sprite.Sprite):         def _init_(self):                 pygame.sprite.Sprite._Init(self)                 self.Image, self.rect = load_png('banana.png')                 screen = pygame.display.get_surface() 

After defining _init_ , you may also want to add methods that define the object's position on the screen, and update the object when necessary:

 class Banana(pygame.sprite.Sprite):         def _init_(self):                 pygame.sprite.Sprite._Init(self)                 self.Image, self.rect = load_png('banana.png')                 screen = pygame.display.get_surface()         def Bannana_Position(self, rect)                 # Funky math here                 # that defines position on screen                 return position         def Banana_Update(self)                 # Code that updates the banana 

Pygame Drawbacks

Pygame is simply a wrapper around SDL, which is a wrapper around operating system graphic calls. Although programming is much easier when using Pygame than when using SDL, Pygame removes you pretty far from the code that actually does the work, and this can be limiting in a number of ways.

Probably the most significant drawback to Pygame, however, is the fact that the library needs so many dependencies in order to function. Obviously, Pygame needs Python and SDL to run, but it also needs several smaller libraries, including SDL_ttf, SDL_mixer , SDL_image , SDL_rotozoom , and the Python Numeric package for the surfarray module. Some of these libraries have their own dependencies.

UNIX packages generally come with package and dependency managers that make managing dependencies a controllable problem in UNIX. But on Windows systems, it can be difficult to distribute a game without creating a collection of all the needed files the game requires to run.

Luckily, there are Python tools to help build Windows executables. I mentioned a few of these in Chapter 3, in particular a tool called Py2exe. Pete Shinners, the Pygame author, actually wrote a tutorial on how to use Py2exe to package a Python Pygame for Windows. The tutorial comes with a sample distutils script and can be found at http://www.pygame.org/docs/tut/Executable.html.

Finally, although hardware acceleration is possible with Pygame and fairly reliable under Windows, it can be problematic because it only works on some platforms, only works full screen, and greatly complicates pixel surfaces. You also can't be absolutely sure that the engine will be faster with hardware accelerationat least not until you've run benchmark tests.

A Pygame Example

In this section you'll use the Pygame load_image function with game loops to create a simple two-dimensional graphics-engine game example. The steps you need to take are as follows :

  1. Import the necessary libraries.

  2. Define any necessary functions (such as load_image ).

  3. Define any game object classes (sprites, game characters ).

  4. Create a main event loop that listens for events.

  5. Set up Pygame, the window, and the background.

  6. Draw and update necessary graphics ( utilizing groups and sprites).

I envision a monkey-versus-snakes game, where the monkey/player throws bananas at snakes to keep them at bay. The steps for coding this example are explained in each of the following sections, the full source code can be found on the CD as Monkey_Toss.py , and Figure 4.6 gives you a preview of the game.

Figure 4.6. A preview of Monkey_Toss.py

graphic/04fig06.gif


Importing the Necessary Libraries

Importing has been covered ad nauseum already, so I will not bore you with the details. Simply start with this code:

 # Step 1 - importing the necessary libraries import pygame, os import random from pygame.locals import * 

These libraries should be familiar to you with the exception of the random module. Python comes equipped with random , and we will be using the random. randrange method to generate random numbers .

NOTE

Random Library

The random.randrange method generates a random number (an integer) within the range given. For instance, this snippet prints a number between 1 and 9:

 import random Print ( random.randrange(1, 10)) 

Simple enough. Note that random.randrange prints up to the highest number given, but not the actual highest digit. Random numbers are used so often in games that you will often encounter random number functions like this:

 Def  DiceRoll():       Dice1 = random.randrange( 1, 7)       Print "You rolled %d" % (dice1)       Return dice1 

You will be using random's randrange() and seed() methods to produce random numbers for the Monkey_Toss.py example.

Defining Necessary Functions

You will be using a version of load_image in this game example, but you will switch from using colorkey and look instead for alpha values in the graphics. You have the graphics already built with alpha channels and stored in a data directory next to the game code (and also on the CD). This means you need to alter a few lines of code from Load_Image.py :

 def load_image(name):     fullname = os.path.join('data', name)     try:         image = pygame.image.load(fullname)         # Here instead of the colorkey code we check for alpha values         if image.get_alpha is None:                 image = image.convert()         else:                 image = image.convert_alpha()     except pygame.error, message:         print 'Cannot load image:', fullname         raise SystemExit, message     return image, image.get_rect() 

You will also define a very short function to help handle keystroke events from the player. We will call this function AllKeysUp :

 def AllKeysUp(key): return key.type == KEYUP 

Defining Game Object Classes

First you will define a sprite class. The class needs, of course, an _init_ method:

 class SimpleSprite(pygame.sprite.Sprite):         def __init__(self, name=None):                 pygame.sprite.Sprite.__init__(self)                 if name:                         self.image, self.rect = load_image(name)                 else:                         pass 

When initiating, you set SimpleSprite to load the given image name and become a rect() . Normally, you would include error code in case the name isn't passed or something else goes wrong, but for now you will just use Python's pass command ( pass is an empty statement that can be used for just such a situation).

You will also give your SimpleSprite a method to set up its surface:

 def set_image(self, newSurface, newRect=None):                 self.image = newSurface                 if newRect:                         self.rect  = newRect                 else:                         pass 

Normally you would set up each pass default and also include at least a base method for updating the sprite, but for now let's keep it easy.

For this engine, as I said, I envisioned a monkey versus snakes game, and since you are writing in Python, start with the Snake_Grass class:

 class Snake_Grass:     def __init__(self, difficulty):         global snakesprites         global block         for i in range(10):             for j in range(random.randrange(0,difficulty*5)):                 block = SimpleSprite("snake.png")                 block.rect.move_ip(((i+1)*40),480-(j*40))                 snakesprites.add(block)     def clear(self):         global snakesprites         snakesprites.empty() 

There are two methods in this class, one to initiate the object and one to clear it. The clear() method simply uses empty() to clear out the global snakesprites when it is time. The _init_ method takes in the required self and also a measure of difficulty, ensures snakesprites and block are created, and then starts iterating through a for loop.

The outer for loop iterates through a second inner for loop that creates a random number of "blocks," each of which contains a square snakesprites loaded with the snake.png graphic. These sprites are created and moved into stacks on the game board using a bit of confusing math ( block.rect.move_ip(((i+1)*40),480-(j*40)) ). Don't worry too much about the math that places these sprites on your 480 pixel-wide surface; instead, realize that when initiated with an integer representing difficulty, a Snake_Grass object will create a playing board similar to that in Figure 4.7.

Figure . Figure 4.7 Snake_Grass object called with a difficulty of 2

graphic/04fig07.gif


The placement of the snakesprites and the height of the rows are random so that a differ ent game board surface is produced each time the game is run.

Define the player sprite next; this will be Monkey_Sprite . You want the Monkey_Sprite to possess the ability move in the game, so you need to define a number of methods to define and track movement:

 class Monkey_Sprite(pygame.sprite.Sprite):     def __init__(self, game):     # For creating an Instance of the sprite     def update(self):     # Update self when necessary     def check_crash(self):     # Check for collision with other sprites     def move(self):     # How to move     def signal_key( self, event, remainingEvents ):      # Respond to player If they me to do something     def check_land(self):     # See If reach the bottom of the screen 

That's a lot of methods, but in actuality, the Monkey_Sprite is fairly uncomplicated once you take the time to walk through each method. Lets start with _init_ :

 def __init__(self, game):          pygame.sprite.Sprite.__init__(self)          self.image, self.rect = load_image('monkey.png')         self.rightFacingImg = self.image         self.leftFacingImg = pygame.transform.flip( self.image, 1, 0)          self.direction = 0          self.increment = 25          self.oldPos = self.rect          self.game = game          self.listenKeys = {} 

First you load the image into a rect() that will represent the Monkey_Sprite game object, monkey.png, on the game board surface. Then you set a number of variables. The rightFacingImg is the normal state of the graphic, and the leftFacingImg is the graphic rotated 180 degrees using the Pygame's handy transform.flip() method.

The self.direction value is a Boolean value that will either have the Monkey_Sprite traveling left (represented by a 0) or right (represented by a 1). Set self.increment to 25, representing 25 pixels that the Monkey_Sprite will travel with each update. The next three settings are all set for the methods that follow and use them.

Update is the next method:

 def update(self):         self.check_land()         self.move()         if self.direction == 0:             self.image = self.rightFacingImg         else:             self.image = self.leftFacingImg         self.check_crash() 

Update first checks, using the check_land method, to see whether the Monkey_Sprite has reached the bottom of the screen. You haven't defined check_land yet, but you will momentarily. Then update moves the Monkey_Sprite with the move method, which you also have yet to define. It then checks which direction Monkey_Sprite is facing and makes sure the graphic being used is facing the correct way. Finally, update calls check_crash , which also needs to be defined, and checks to see whether there have been any sprite collisions.

The check_land method simply looks to see if the Monkey_ Sprite has crossed a particular pixel boundary on the game board surface, which is defined by the self.rect.top and self.rect.left variables. If it has, then we know that the Monkey_Sprite needs to start back over at the top of the screen.

 def check_land(self):         if (self.rect.top == 640) and (self.rect.left == 1):             self.game.land() 

The move method uses the defined increment value you set in _init_ to move the sprite across the screen in the direction you've set. If the sprite goes outside the game window (>640 or <0 pixels), you make the sprite switch and travel back across the screen in the opposite direction:

 def move(self):          self.oldPos = self.rect          self.rect = self.rect.move(self.increment, 0)          if self.rect.right > 640:              self.rect.top += 40              self.increment = -25              self.direction = 1          if self.rect.left < 0:              self.rect.top += 40              self.increment = 25              self.direction = 0 

The check_crash method uses Pygame's built-in group methods and pygame.sprite.spritecollide() to check if the Monkey_Sprite ever collides with anything in the crash list, which in this case includes any snakesprites . If there is a crash, Monkey_Sprite will call the game.crash() method, which we will define momentarily.

 def check_crash(self):          global snakesprites          crash_list = pygame.sprite.spritecollide(self, snakesprites, 0)          if len(crash_list) is not 0:              self.game.crash(self) 

Only one more method is associated with the Monkey_ Sprite , signal_key , which is simply a listener for keyboard events.

 def signal_key( self, event, remainingEvents ):                 if self.listenKeys.has_key( event.key ) \                   and event.type is KEYDOWN:                         self.listenKeys[event.key]( remainingEvents ) 

Once a MonkeySprite object is loaded, it will appear in the top-left corner of the game board surface and travel across the screen, as shown in Figure 4.8. When it hits the edge of the screen, it drops a little and then heads back in the opposite direction. If the Monkey_Sprite ever touches a snakesprite or the bottom of the screen, he will start back at the top again.

Figure 4.8. An instance of the Monkey_Sprite class travels across the screen

graphic/04fig08.gif


Now you have monkeys and snakes. You need one more actor, a banana, which the Monkey_Sprite objects will throw at and destroy the snake objects with. This means you need methods for the banana to update and move and check for collisions:

 class Banana(pygame.sprite.Sprite):     def __init__(self, rect, game):     def update(self):     def move(self):     def check_hit(self): 

Initializing the banana sprite works much like the other _init_ methods. There will be an incremental value that defines how many pixels the banana moves when updated, and the sprite that represents the banana will load up a rect() and fill it with the fruit.png file. Finally, you will need some code to check with the master game object for when the banana collides or moves off the screen:

 def __init__(self, rect, game):         pygame.sprite.Sprite.__init__(self)         self.increment =16         self.image, self.rect = load_image("fruit.png")         if rect is not None:             self.rect = rect         self.game = game 

Updating and moving are also set up like the other classes. The banana moves according to its increment value and checks are required to see if the banana collides with any sprites or moves off of the game board surface:

 def update(self):         self.move()     def move(self):         self.rect = self.rect.move(0, self.increment)          if self.rect.top==480:             self.game.miss()         else:             self.check_hit() 

Finally, the check_hit method looks for any collisions with snakesprites just like with the Monkey_Sprite :

 def check_hit(self):         global snakesprites         collide_list = pygame.sprite.spritecollide(self, snakesprites,0)         if len(collide_list) is not 0:             self.game.hit() 

There is still one more class to writethe most important and lengthy game object. You are actually going to put the game controls and variables into a game class called MonkeyToss . We need MonkeyToss to be able to handle a number of different things, but mostly keyboard events, collisions, and actions for when sprites move off the screen. This gives MonkeyToss several different methods:

 class MonkeyToss:     def __init__(self, charGroup):     def crash(self, oldPlane):     def land(self):     def drop_fruit(self):     def miss(self):     def signal_key( self, event, remainingEvents ):     def hit(self): 

The master game class initializes pretty much everything else you need as far as game mechanics. First, it takes in the game sprites and assigns them to the charGroup group. Then it defines the game difficulty that the rest of the classes use. The specifc keybard key the sprite needs to respond to is the spacebar, which when pressed will fire the drop_fruit method. Finally the snake, monkey, and banana ( fruit ) are all initialized :

 def __init__(self, charGroup):          self.charGroup = charGroup          self.difficulty = 2          self.listenKeys = {K_SPACE: self.drop_fruit}          self.snake = Snake_Grass(self.difficulty)          self.monkey = Monkey_Sprite(self)          self.charGroup.add( [self.plane] )          self.fruit = None 

The crash method is called by our Monkey_Sprite when it collides with a snakesprite . When the Monkey_Sprite collides with a snakesprite , it needs to be destroyed with the kill() method and then a new Monkey_Sprite should be instantiated to start over and be assigned to the sprite group:

 def crash(self, oldMonkey):          self.monkey.kill()          self.monkey = Monkey_Sprite(self)          self.charGroup.add ([self.monkey]) 

The land method is also called by the Monkey_Sprite when it reaches the bottom of the screen. For this sample the method is identical to the crash method, but in a real game, the landing might create a new field of snakes, or pop the player to a different area of the game entirely.

 def land(self):          self.monkey.kill()          self.monkey = Monkey_Sprite(self)          self.charGroup.add([self.monkey]) 

The drop_fruit method is called when the spacebar is pressed, and Monkey_Sprite attempts to drop fruit on a snake. Drop_fruit assigns self.fruit an instance of the Banana class and adds it to the active sprite group:

 def drop_fruit(self):          if self.fruit is None:              self.fruit = Banana(self.monkey.rect, self)              self.charGroup.add([self.fruit]) 

Code must be created for when the dropped fruit falls past the end of the screen; for our purposes the sprite can just call the kill() method on itself:

 def miss(self):          self.fruit.kill()          self.fruit = None 

For keyboard events, define a signal_key method:

 def signal_key( self, event, remainingEvents ):         if self.listenKeys.has_key( event.key ):             self.listenKeys[event.key]()         else:             self.monkey.signal_key( event, remainingEvents ) 

The last part is the code that handles sprite collision. This bit is fairly complex. First you need to keep track of all the snakesprites , and then all of the sprites in the group, by creating My_Group . Then you call colliderects[] , which returns true if any rect in the group collides:

 def hit(self):          global snakesprites          My_Group = pygame.sprite.Group()          colliderects = [] 

Following colliderects[] is a for loop that basically checks to see if the bottom of the fruit rect and the top of the monkey rect collide, and if so adds them to the collide list:

 for i in range(3):              for j in range((self.fruit.rect.bottom+16-self.monkey.rect.top)/16):                  rect = Rect((self.fruit.rect.left-32+i*32, self.fruit.rect. bottom-j*16),(25,16))                  colliderects.append(rect) 

Then, for each collision, you need to destroy the given fruit and make sure the sprite group is updated:

 for rect in colliderects:              sprite = SimpleSprite()              sprite.rect = rect              My_Group.add(sprite)          list = pygame.sprite.groupcollide(My_Group, snakesprites, 1,1)          self.fruit.kill()          self.fruit = None 

That's quite a lot of work, but, happily, defining the classes comprises the bulk of this sample's code, and you are past the halfway point of coding. Now onwards!

Creating a Main Event Loop that Listens for Events

To create a main loop, you normally define a main function containing a while loop:

 def main():          while 1:                 # do stuff if __name__ == "__main__":     main() 

This ensures that main() is called and your while loop keeps running during the course of the game. As good coding practice, initialize a few variables inside of main() :

 global screen     global background     global snakesprites     global block 

You are also going to take advantage of a Pygame clock feature and use random's seed method to set a random number seed. Since you are going to be experiencing movement and time, you'll be setting an oldfps variable to help keep track of time and loop iterations:

 clock = pygame.time.Clock()     random.seed(111111)     oldfps = 0 

Finally, the while loop. You want to make sure time is recorded by using clocktick() and updating with each iteration. Any keyboard events are queued, so that QUIT, the Escape key, or the KEYUP , which is set to be the Spacebar, can be responded to:

 while 1:         clock.tick(10)         newfps = int(clock.get_fps())         if newfps is not oldfps:             oldfps = newfps         oldEvents = []         remainingEvents = pygame.event.get()         for event in remainingEvents:             oldEvents.append( remainingEvents.pop(0) )             upKeys = filter( AllKeysUp, remainingEvents )             if event.type == QUIT:                 return             elif event.type == KEYDOWN and event.key == K_ESCAPE:                 return             elif event.type == KEYDOWN or event.type == KEYUP:                 game.signal_key( event, upKeys ) 

Setting Up Pygame, the Window, and the Background

You can initialize Pygame using the init() method within main() . Then you use display.set_mode() to configure the game surface to 640x480 pixels, and the game caption to be "Monkey Toss". You then use your load_image method to load up the surface background and initialize blitting and flipping:

 pygame.init()     screen = pygame.display.set_mode((640, 480))     pygame.display.set_caption('Monkey Toss')     background, tmp_rect = load_image('background.png')     screen.blit(background, (0, 0))     pygame.display.flip() 

Drawing and Updating Necessary Graphics

For drawing, you start by initializing all of your sprites and sprite groups in main() :

 allsprites = pygame.sprite.RenderUpdates()     snakesprites= pygame.sprite.RenderUpdates()     block = None     game = MonkeyToss(allsprites) 

The code that does all the work lies at the end of the while loop, which clears the sprite groups then updates and redraws each changed rect() :

 allsprites.clear( screen, background)         snakesprites.clear(screen, background)         allsprites.update()          changedRects2 = allsprites.draw(screen)         changedRects3 = snakesprites.draw(screen)         pygame.display.update(changedRects2+changedRects3) 

The finished product and the full source code and data files can be found in Chapter 4's file on the CD. Obviously, quite a bit could be added to this program. Check out the complete game sample at the end of this chapter for a few ideas!

[ LiB ]


Game Programming with Pyton, Lua and Ruby
Game Programming with Pyton, Lua and Ruby
ISBN: N/A
EAN: N/A
Year: 2005
Pages: 133

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net