Creating Games with Pygame
Python is an excellent language for rapid application development and
prototyping. With Pygame, a wrapper built around SDL, the same can be
true for games. In addition, because its built on top of Python and SDL, Pygame
is highly portable. The only downside is it can be too slow for some
computationally intensive types of games. If your game is too
slow, the particular sub-routine(s) bringing down your execution
speed can be rewritten in C/C++.
This article is intended to be a short introduction and by no means covers
all there is to know about Pygame. Using a simple Space Invaders-type game,
I present what I feel are the fundamentals of Pygame.
The first step in creating our game is to import Pygame and the other
modules we need:
import random, os import pygame from pygame.locals import *
We need the os module for tasks related to opening files; the random
module is needed for the AI of our enemy sprites, as we will see later. We
then run import pygame, which imports all available Pygame modules from the
Pygame package. The final line is optional and puts a subset of the most
frequently used Pygame functions and constants into the global name space.
It may seem like we are skipping ahead here, but the last line of our program calls
our main function, so we might as well get it out of the way now.
if __name__ == '__main__': main()
Next, we create our main function. The basic steps we are going to follow for our main function
are initializing modules, loading game resources, preparing game objects and entering the game loop.
def main(): random.seed() pygame.init()
random.seed() is called to initialize the random number
generator. Optionally, one parameter can be given to random.seed(), and this
parameter can be any hashable object. If the parameter is omitted or is listed as
None, the system time is used.
pygame.init() attempts to initialize all the Pygame modules for
you. All the Pygame modules do not need to be initialized, but this command
automatically initializes the ones that do. Alternatively, you can
initialize each Pygame module by hand.
Most modules have a quit() function that cleans up the module, but
there is no need to call it manually, as there is with SDL. Pygame calls these
functions automatically and exits cleanly when Python finishes.
The next part of our main function is creating the display surface. The
surface is one of, if not the most, important elements of Pygame. A surface
is like a canvas on which you can draw lines, images and so on. Surfaces can be
any size, and you can have as many of them as you want. The first of the
following three code lines creates the display surface. I have found that running
the game in a window rather than in full screen mode is a good
idea during development. This way, if your game crashes for some reason,
you don't have to worry about the screen resolution being incorrect,
among other possible problems. In the case that your game is played in a
window rather than full-screen mode, the second line sets the title bar
caption. The third line makes the mouse cursor invisible over the game window.
screen = pygame.display.set_mode((640, 480), FULLSCREEN)
pygame.display.set_caption('Space Game')
pygame.mouse.set_visible(False)
To play the game in a window, remove the FULLSCREEN option as follows:
screen = pygame.display.set_mode((640, 480))
Before we can create our background, we need to create a function to load
images for us. For those new to programming, the load_image and
the load_sound functions should be placed above the main function. Our
function load_image takes the name of the image file as a parameter and, optionally,
a color key. The color key tells Pygame that all pixels
of a particular color should be transparent. In our game, we keep
all of our images and sounds in a directory called data. The command
os.path.join creates the complete path to our file, in a
platform-independent manner. Then, we try to open the file.
From here, we convert our images to
SDL's internal format, which greatly increases the speed at which images are blitted. This is so
because SDL does not have to do the conversion on the fly every time the image is blitted. We then set the
color key of the image, if we have one. If the color key is -1, the color
of the pixel in the top left corner is used. Finally, we return the image
and the rectangular object containing it.
def load_image(file_name, colorkey=None):
full_name = os.path.join('data', file_name)
try:
image = pygame.image.load(full_name)
except pygame.error, message:
print 'Cannot load image:', full_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()
Next, we load the background image using the function we just
defined. We then blit the image to our screen surface.
background_image, background_rect = load_image('stars.bmp')
screen.blit(background, (0,0))
We now create our function for loading the required sounds. Our
function load sound takes the name of the file we want to load as the
only parameter. The first thing we do is create a dummy class called
'No_Sound' that has a play method that does nothing. We then check if the
mixer was imported and initialized correctly. If not, we return our dummy
class. Then, we form our path to the file in the same way as was done for the load_image
function. If the file exists, we load it; otherwise, we print a message to the
terminal and return our dummy class. If we have made it to the end of
the function without any problems, we return our sound object.
def load_sound(name):
class No_Sound:
def play(self): pass
if not pygame.mixer or not pygame.mixer.get_init():
return No_Sound()
fullname = os.path.join('data', name)
if os.path.exists(full_name) == False:
sound = pygame.mixer.Sound(fullname)
else:
print 'File does not exist:', fullname
return No_Sound
return sound
We now can load our four sound effects using the function we just
defined. We make shot 1 global, because it can be called from within
our enemy sprite class.
explode1 = load_sound("explode1.wav")
explode2 = load_sound("explode2.wav")
global shot1
shot1 = load_sound("silent.wav")
shot2 = load_sound("fx.wav")
Next, we create and initialize three different counters. The first
one is required so as to know when the player has been killed, but the last
two are needed only for our end-of-game stats.
numberof_hits = 0 numberof_shots = 0 enemy_killed = 0
Now we create the player's ship sprite, all of the sprite groups
and the enemy sprites present at the beginning of the
game.
Pygame groups have containers used to hold sprites. There are
several different classes of groups, each having different methods and
properties. The group type that I use most is RenderClear, because it
allows for the rendering of all sprites in the group as well as the clearing of
them.
First, we create an instance for our Ship class, which we will define
momentarily. Then, we create a group called playership_sprite that contains
our ship object. Next, we create a group that holds all of our
bomb sprites. The bomb_sprite group is empty, as there is no bomb
sprite instances created until we actually fire. We then create our
enemyship_sprites group and add three instances of class Enemy to it. The
Enemy class takes the x-coordinate of the sprite as an argument to the
init function; we will define this class momentarily as well. We then
create our ebomb_sprite group, which stands for "enemy bomb". We also
want the group to be accessible globally so we can access it from
our enemy ship class.
ship = Ship() playership_sprite = pygame.sprite.RenderClear(ship) bomb_sprites = pygame.sprite.RenderClear() enemyship_sprites = pygame.sprite.RenderClear() enemyship_sprites.add(Enemy(212)) enemyship_sprites.add(Enemy(320)) enemyship_sprites.add(Enemy(428)) global ebomb_sprites ebomb_sprites = pygame.sprite.RenderClear()
We now define our sprite classes. As a reminder for those
new to programming, the class definitions should be placed above the
main function. All of our sprite classes consist of only to methods. The
__init__ method is called when an instance of the class is created, and
the update method is called every time we iterate over the game loop.
The first class we our going to define is the Ship class. In all of our
sprite classes we want to inherit the Pygame sprite class, and in our
init function we call the Pygame sprite __init__ function. We then load
our image and set the center of our sprite to (320,450), which is the
start location of the player's ship. Next, we set the number of pixels
the image should move with each turn to zero.
class Ship(pygame.sprite.Sprite):
"""This class is for the players ship"""
def __init__(self):
pygame.sprite.Sprite.__init__(self) #call Sprite initalizer
self.image, self.rect = load_image('ship.bmp', -1)
self.rect.center = (320,450)
self.x_velocity = 0
self.y_velocity = 0
Now we move on to our update function. All we have to do in our update
function is move the position of the sprite and then check to see if it
has gone out of bounds. The move_ip method moves the rect object by a
given offset. If the sprite has moved our of bounds, it is placed
back inside the boundaries. The boundaries are approximately the
bottom half of the screen.
def update(self):
self.rect.move_ip((self.x_velocity, self.y_velocity))
if self.rect.left < 0:
self.rect.left = 0
elif self.rect.right > 640:
self.rect.right = 640
if self.rect.top <= 260:
self.rect.top = 260
elif self.rect.bottom >= 480:
self.rect.bottom = 480
The enemy ship class is a little more complicated, as the update method
contains the AI component of the enemy sprite. The class inherits the
Pygame sprite class and calls the sprite init function as per usual. The
init function takes the starting x position of the sprite. We then
load the image of our enemy ship. The center of the rect object is set
to the x value given and the y value to 120. The distance variable is
used in the AI to know how many turns the ship should move in the same
direction. The x and y velocities are initialized to zero, as they are in
the player's ship class.
class Enemy(pygame.sprite.Sprite):
"""This class is for the enemy ships"""
def __init__(self, startx):
pygame.sprite.Sprite.__init__(self) #call Sprite intializer
self.image, self.rect = load_image('eship.bmp', -1)
self.rect.centerx = startx
self.rect.centery = 120
self.distance = 0
self.x_velocity = 0
self.y_velocity = 0
As I mentioned earlier, the update function contains the AI for our enemy
sprites. This particular AI is based heavily on random numbers. The AI
is quite simple but works well enough for our purposes in such a simple
game. The AI works as follows. If the remaining distance our sprite has to
move is zero, it selects a new random distance. The distance is how many
iterations through the main game loop the sprite moves in the same
direction. Then, a random x and y velocity is selected. It is possible
that both the x and y velocities would both be zero, in which case
the sprite would stand still for the number of turns indicated by the
distance variable. We then move the rect object containing the sprite
and decrease the distance remaining by one.
From here, we check to see if the sprite has moved out of bounds; if it has,
it is the placed back in bounds. The boundaries basically are the top
half of the screen. This ensures that enemy ships stay on the top
half and the player says on the bottom, so we don't have to worry
about collision detection between ships.
The last part of our AI component is firing. A random number is
selected between 1 and 60; if the number 1 is selected the enemy ship
fires. Firing is done by adding another enemy bomb sprite to the
ebomb group. The new bomb is passed its starting location, which is
the horizontal center and the bottom of the enemy ship. Last but
certainly not least, the shot sound effect is played.
def update(self):
#movement
if self.distance == 0:
#random distance from 3 to 15 turns
self.distance=random.randint(3,15)
#random x,y velocity form -2 to 2
self.x_velocity=random.randint(-2,2)
self.y_velocity=random.randint(-2,2)
self.rect.move_ip((self.x_velocity, self.y_velocity))
self.distance -= 1
if self.rect.left < 0:
self.rect.left = 0
elif self.rect.right > 640:
self.rect.right = 640
if self.rect.top <= 0:
self.rect.top = 0
elif self.rect.bottom >= 220:
self.rect.bottom = 220
#random 1 - 60 determines if firing
fire=random.randint(1,60)
if fire == 1:
ebomb_sprites.add(Ebomb(self.rect.midbottom))
shot1.play()
Seeing as we are in the process of defining our sprites, we might as well
create the last two. The bomb and enemy bomb classes are similar
and probably could have been combined into one class. I decided to keep
them separate for the sake of simplicity and clarity, which I felt to be more
important aspects of this tutorial. In both classes the sprite initalizer is
called, the appropriate image is loaded and the start position set. In
the update method, we check if the player's bomb has gone off the top
of the screen, and in the case of the enemy bomb, we see if it has gone off the bottom. If the
bomb has gone off screen, the sprite deletes itself with the kill
method. If the bomb still is on screen, it is moved four pixels up or
down, respectively.
class bomb(pygame.sprite.Sprite):
"""This class is for the players weapons"""
def __init__(self, startpos):
pygame.sprite.Sprite.__init__(self) #call Sprite intializer
self.image, self.rect = load_image('fire.bmp', -1)
self.rect.center = startpos
def update(self):
if self.rect.bottom <= 0:
self.kill()
else:
self.rect.move_ip((0, -4))
class Ebomb(pygame.sprite.Sprite):
"""This class is for the players weapons"""
def __init__(self, startpos):
pygame.sprite.Sprite.__init__(self) #call Sprite intializer
self.image, self.rect = load_image('efire.bmp', -1)
self.rect.midtop = startpos
def update(self):
if self.rect.bottom >= 480:
self.kill()
else:
self.rect.move_ip((0, 4))
Let's get back to our main function. We now can create our game loop, but
before we do, we must create two variables. The first one, running, is
used as the condition for the while loop. To end the loop and thus exit
the game, all we have to do is set the running value to zero. The other
variable, counter, is used to see if it is time to add another enemy ship
to the game. We then start the loop and add a delay of ten milliseconds to
make sure that the game doesn't run too quickly. If you are using an older
computer, the delay might be too long and should be lowered accordingly.
running = 1
counter = 0
while running:
pygame.time.delay(10)
The first thing we want to do in our loop is check for and cycle
through any user events. The QUIT can be created many different
ways, such as clicking the close button on the window frame if you're not
playing in full-screen mode. All the other events come from the keyboard
and are pretty self explanatory. In the case of the user pressing the f key,
a new bomb sprite is added to the player's bomb group. The number
of shots is increased by one, and this information is part of the game
stats printed to the terminal window at the end of the game. We then
play the shot sound affect.
for event in pygame.event.get():
if event.type == QUIT:
running = 0
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
running = 0
elif event.key == K_LEFT:
ship.x_velocity = -2
elif event.key == K_RIGHT:
ship.x_velocity = 2
elif event.key == K_UP:
ship.y_velocity = -2
elif event.key == K_DOWN:
ship.y_velocity = 2
elif event.key == K_f:
bomb_sprites.add(bomb(ship.rect.midtop))
numberof_shots += 1
shot2.play()
elif event.type == KEYUP:
if event.key == K_LEFT:
ship.x_velocity = 0
elif event.key == K_RIGHT:
ship.x_velocity = 0
elif event.key == K_UP:
ship.y_velocity = 0
elif event.key == K_DOWN:
ship.y_velocity = 0
To causes the game to last longer and encourage the player to work
quickly, every 200 times through the loop we add another enemy ship at
the center of the top half of the screen as shown below.
counter += 1
if counter >= 200:
enemyship_sprites.add(Enemy(320))
counter = 0
Clearing all the sprites from the screen is easy with the clear method,
because our sprite groups are of the type RenderClear. The clear method
takes two arguments. The first is the surface from which you want to clear the
sprites, and the second argument is the surface that should be used
as the background.
ebomb_sprites.clear(screen, background_image)
enemyship_sprites.clear(screen, background_image)
bomb_sprites.clear(screen, background_image)
playership_sprite.clear(screen, background_image)
We call the update method on all of our sprite groups, which in turn calls
the update method of each sprite in that group. As we recall, the update
method moves all of our sprites and makes any AI decisions required.
bomb_sprites.update()
playership_sprite.update()
ebomb_sprites.update()
enemyship_sprites.update()
Now that all of our sprites have been moved to their new positions, we
can check for collisions between the player's bombs and enemy sprites,
as well as for collisions between enemy bombs and the player's ship.
The group collidemethod returns a dictionary of all sprites in the
first group that collide with sprites from the second group. The indices
of the dictionary are the sprites in the first group that collide,
and the value is a list of sprites which which they collides. The group collidemethod
take four arguments. The first two are the groups you want to
check to see if they have colliding sprites, and the last two arguments are used if
you want the colliding sprites to be deleted. For our example, we want both
the player's bomb and the enemy it collided with to be destroyed, so the
last two values are 1 and 1. If you didn't want either of the sprites
to be deleted, set their corresponding values to 0.
For every collision between the players bombs and an enemy ship, we play
the explode sound effect and increase the enemy-killed stat by one. Next,
we check if the enemy sprite group is empty. If it is, we print a message
to the terminal that includes the game stats and sets the running variable
to zero so that the game loop exits on the next iteration through
the game loop.
#See if players bombs hit any enemy ships
for hit in pygame.sprite.groupcollide(enemyship_sprites, bomb_sprites, 1, 1):
explode1.play()
enemy_killed += 1
if enemyship_sprites.sprites() == []:
print "You Win!!!!"
print "Shot fired:",numberof_shots
print "Hits taken:",numberof_hits
print "Enemy killed", enemy_killed
running = 0;
Detecting collisions between enemy bombs is accomplished through a similar process. One
important thing to note here is we automatically delete enemy bombs
that collide, but we do not delete the player's ship; hence, the last
parameter in the group collide method is a 0. For every hit we increase
the number of hits by one and play our explode sound effect. If the
number of hits is three or more, we print a message to the terminal,
and we set the running variable to zero.
for hit in pygame.sprite.groupcollide(ebomb_sprites, playership_sprite, 1, 0).keys():
numberof_hits += 1
explode2.play()
if numberof_hits >= 3:
print "You lose :("
print "Shot fired:",numberof_shots
print "Hits taken:",numberof_hits
print "Enemy killed", enemy_killed
running = 0;
Now that we have gotten rid of all the sprites that should be removed from
game play, we can redraw the screen. Calling the draw method on each of
the sprite groups draws the sprites in that group to the specified
surface, which in our case is the screen. The display.flip method is
used with double buffing. Double buffering is where all drawing is done
to a hidden buffer and then the hidden buffer is swapped with the draw
buffer. This increases drawing speeds, as unseen layers need not be drawn
to the screen. If double buffering is not supported by your hardware,
drivers and so on, it can be simulated by Pygame.
ebomb_sprites.draw(screen)
bomb_sprites.draw(screen)
enemyship_sprites.draw(screen)
playership_sprite.draw(screen)
pygame.display.flip()
The last part of our main function, which is outside of our loop, is
to pause three seconds before exiting so that the player can see the
final screen. I also reset the screen so it is in a window. Doing so is
not necessary, but I have had some problems with certain development
environments, most notably IDLE, and resetting the screen helps in avoiding
some small problems.
pygame.time.delay(3000) screen = pygame.display.set_mode((640, 480))
As you can see, Pygame makes game development fun and easy. For more
information, check out the
Pygame Web.
The full source code and image files used in this article can be found
here.










This week 5 lucky Members will receive a copy of The Official Ubuntu Server Book by Benjamin Mako Hill and Linux Journal's very own Kyle Rankin. No entry necessary. Check back here early next week to find out who the lucky Online Members are.




Comments
NameError: name 'main' is not defined
Traceback (most recent call last):
File "Pygame_LinuxJournalExample.py", line 23, in
if __name__ == '__main__': main()
NameError: name 'main' is not defined
this appeared after trying to fix lots of indentation errors...
complex, difficult and confusing
For newbies, this article is complex, difficult and confusing - and plenty of redundancies...
Would be much more easier to learn from minimal examples.
I have some similar minimal examples at:
http://nitrofurano.linuxkafe.com/python
http://nitrofurano.linuxkafe.com/sdlbasic
http://nitrofurano.linuxkafe.com/wxbasic
FTP Link
Hi,
The file is here:
ftp://ftp.ssc.com/pub/lj/listings/Web/7694.tar.gz
("Web", not "WEB")
You could have found it yourself on the ftp server... ;-)
Dead Link
Hi people,
Looks like the link is dead. Can someone else give it a try?
ftp://ftp.ssc.com/pub/lj/listings/Web/7694.tar.gz
Thanks,
Jim
The link
The link still doesn't work with that 'Web'.
Re: Creating Games with Pygame
Actually the link to the tarball is STILL not working- can anyone get a hold of it and mail it to mr981505@netscape.net
ta.
Re: Creating Games with Pygame
Excellent.
Now, if we could have a similar article detailing a simple database app (PostgreSQL, MySQL or similar as the back end) with a gui front end I would seriously consider spending time on Python.
Re: Creating Games with Pygame
Database connection is really easy in python. For Dbase connectivity you can find information on http://www.python.org/topics/database/. With the DB API 2.0, there is actually not much difference in connecting to a mysql, or a postgresql database.
For gui, you have a number of choices - wxpython, tk, gtk just to name a few. You can find info on this on www.wxpython.org . If you prefer tk, you can find info on that on http://www.python.org/topics/tkinter/.
Re: Creating Games with Pygame
Instead of waiting for next article look at http://www.pygtk.org/pygtk2tutorial/index.html
Greetings
kamyl
Re: Creating Games with Pygame
Very nice tutorial, i've never programmed in Python and I was able to understand a lot and even find some problems
This is mostly logic but...
in the load_sound method there's a mistake
fullname = os.path.join('data', name)
if os.path.exists(full_name) == False: # Wrong
sound = pygame.mixer.Sound(fullname)
else:
print 'File does not exist:', fullname
return No_Sound
Change the mistake for
if os.path.exists(full_name) == True:
and then it'll work :) if it exists it should be played and not the oposite :)
Congratulations for this nice tutorial
Re: Creating Games with Pygame
Also the author confuses 'fullname' and 'full_name'.
This article is very poorly written.
Re: Creating Games with Pygame
There is no error in the actual source file
Re: Creating Games with Pygame
That the tutorial deviates from the source it's explaining is another serious problem.
program coding ex. color
how to create it using the ,
Re: Creating Games with Pygame
The programming in the tutorial certainly had some problems, but the introduction to the topic was top notch. I was introduced to an aspect of python programming (well, programming in general) I had never been exposed to in such a way that I felt I had a handle on the basics. Sure, the sample code could have been editted better, but the tutorial served it's purpose in introducing a new package and showing off the capabilities.
Re: Creating Games with Pygame
Even better would be just "if os.path.exists(fullname):"; usually in Python there's no guarantee that the result will be True or False, just that the result will evaluate to true or false in a boolean context (which True and False happen to do, but so do 1 and 0, or 18 and None...)
Re: Creating Games with Pygame
Just a few nitpicks from a Pygame hacker...
RenderClear is "going away" in Pygame 1.7, coming in a few days. Group, RenderPlain, and RenderClear are now all the same group. The fourth one, RenderUpdates (not mentioned in the article), is still separate and usually gives better performance when used properly (and doesn't require double buffering).
You mention that pygame.time.delay with a constant value might be too long for slower machines; that's what pygame.time.Clock is for, you can specify an FPS to try and maintain, and it will calculate the correct time to sleep.
The call to random.seed() is not necessary; Python automatically seeds the RNG when it is not done explicitly.
Resetting the screen mode can be avoided by calling pygame.quit() at the end of the program. This shuts down all the SDL systems, including the display.
"if enemyship_sprites.sprites() == []:" is unsafe Python code in general. You don't know if sprites() is going to return a list, tuple, iterator, generator... Properly it should be written as "if len(enemyship_sprites.sprites()) == 0:", but you can also write it as "if not enemyship_sprites:" (I don't like that version). Pygame 1.7 you can just write "if len(enemyship_sprites) == 0:" which is probably the best way in the future.
-- Joe Wreschnig
not bad
Not bad but I've seen better tutorials on pygame. Anyway, good python coder can do all of available tutorials in a week... Not much of it so good work!
--
Regards
Nazgob
www.nazgob.com
Re: Creating Games with Pygame
The link to the tar ball on the LJ FTP site has been fixed.--LJ Editorial
Uh... no. The link is still b
Uh... no. The link is still broken. However, the user comment above with the correct URL does work.
Re: Creating Games with Pygame
The link to the source code and image files gives an ftp error:
"550 Can't chagne director to /pub/lj/listings/WEB/7694.tar.gz: No such file or directory"
Otherwise looks like it could be fun to play around with.
Post new comment