Creating 2D Video Games with Pygame


There's strictly no warranty for the correctness of this text. You use any of the information provided here at your own risk.


Contents:

  1. Introduction
  2. Overview of a Video Games Program
  3. Sprites and Groups
  4. Transparent Pixels
  5. How to Rebuild the Screen Each Frame to Create Animation
  6. Python's "*args" mechanism
  7. Keyboard and Joystick Input (Events)
  8. Controlling the Speed
  9. Collision Checks
  10. The Game's Main Loop
  11. Screen Coordinates Stored in Arrays
  12. Attribute "self.active" and Method "start"
  13. A "Scene" (or "Level") Class
  14. Sprite Class, Gamestate Class, Sprite-Initialization and an Example of it All
  15. Plotting Huge Numbers of Pixels Using PixelArrays
  16. Drawing Simple Shapes
  17. Playing Effect Sounds (".wav")
  18. Scrolling the Background
  19. Physics Simulation with Pymunk
  20. Further Pygame Tutorials
  21. Skeleton Code


1. Introduction

Of all applications, video games have the highest requirements of the computer hardware.
It is also more difficult to write games than most other computer programs.

If you want to write a game in Python/Pygame, you should already know, how to write Python programs in general. You should know, how data processing with lists, dictionaries and functions work, and how to set up classes and objects. You should have a general idea of how input (from keyboard, joysticks or files) and output (to the screen, the soundcard or again to files) is done on a computer and how to make programs run as fast as possible.
You will have to set up a main loop, so it won't hurt, if you were already familiar with writing GUI-applications, because unlike console programs GUI-applications are also driven by a main loop.
And you should be prepared, that it's not so easy, to make funny looking objects on the screen behave exactly the way you want.


Pygame is a library (a set of modules), to write 2D video games in Python. Actually, 3D is also possible to a certain extent.

Pygame calls functions of the underlying C-library "SDL" (Simple Direct Layer).
For Perl, the modules "SDL", "SDL::Surface", "SDL::Events" and so on do something similar.

There is also another library called "SFML", which does similar things like "SDL". The documentation of SFML says, "You can think of it as an object oriented SDL". SFML can also be programmed in Python, using the bindings called "PySFML". To my surprise, PySFML is sometimes faster than Pygame, especially when it comes to rescaling images in real-time.

One advantage of Pygame over PySFML is, that it is easier to install and therefore more common. So it is easier to share a Python-script, that uses Pygame with others, than one, that uses PySFML. Not many people will know what SFML and PySFML is, so they probably haven't installed it.

In general, Python is not the fastest language. So speed may be an issue in Pygame-games sometimes. Pygame is probably not the choice of professional game programmers, they probably would write in C/C++ or even Assembly. Nevertheless often you can get decent results with Pygame too.

Also look for the directories "docs" and "examples" in the pygame-distribution. On my Linux-box, these directories were installed to:

/usr/lib/python/site-packages/pygame/docs
/usr/lib/python/site-packages/pygame/examples

This official documentation can be found online too, but it's good to have the examples directly available as scripts.


2. Overview of a Video Games Program

What is needed in the program of a video game?

That's it, basically. There's a game logic, events from input devices, output onto the screen and to the soundcard.

Some more details about sprite animation:
Pygame uses socalled "surfaces". A surface is a rectangular area, onto which images are drawn. The size of the surface can be defined by the programmer. After the image is drawn, the surface is put over the background as a second layer. This is called "blitting", the term is

to blit.

Which is an abbreviation of "Block Image Transfer".

It can be defined, where on the background the sprite surface shall be blitted.
As it's 2D, in the end the player doesn't notice, that there are several layers. To him, it looks like a single screen, where some action takes place.
The primary screen of the output window is represented by the "display"-object. It is also a surface. The game background can be drawn onto this screen, or a second surface can be layered above it (to separate the screen into areas for example). The surfaces of the sprites are then blitted onto the surface of the background.


3. Sprites and Groups

For a sprite, you need a surface with the image, you want to show.
Pygame's Surface-class takes a tuple with the surface's size as its argument.
It is also useful to define a rectangle, the size of the surface, using the Rectangle-class. Arguments to this class are a tuple with the position coordinates and again a tuple with the surface's size. You can also use:

rect = surface.get_rect()

But then, you have to define the position of the rectangle later (I think).

The rectangle then has attributes such as

rect.size
rect.topleft

for size and position of the rectangle (and therefore the sprite). "rect.size" is the same as "(rect.x, rect.y)".
You can draw the image you want to show onto the surface. It is also possible to load in jpg- or png-files for this purpose.

Up to now, some things were defined, but nothing is shown yet. That's why one tutorial said, sprites can be "spooky things that move but are not really there".
To draw the sprite, you have to blit them onto a visible surface, such as the screen:

screen.blit(spritesurface, rect)

It would be possible to pass a tuple with the coordinates for the position, but it's better to use the rect for that.

At the beginning of the main loop (that is repeated many times each second), the main screen is either filled with a solid colour or with a background image. Then, you alter the position of the sprite's rect with "rect.x += 1", for example. Then you blit the sprite to the screen there. Then the main loop starts again. This is, how animation is done. Here's a minimal example (use Ctrl+c to break, there isn't event handling yet):

#!/usr/bin/python
# coding: utf-8

import pygame

BLACK = (0, 0, 0)
RED   = (255, 0, 0)

pygame.init()
screen = pygame.display.set_mode((800, 600))
surface = pygame.Surface((20, 20))
surface.fill(RED)
rect = surface.get_rect()
rect.topleft = (0, 300)
while True:

    # Clearing all sprites and rebuilding the screen:
    screen.fill(BLACK)

    if rect.x < 600:
        rect.x += 1
    screen.blit(surface, rect)
    pygame.display.flip()

It is also useful to put all of this into a Sprite object. The reason is, these Sprite objects can be put into groups (that is, added to Group objects). Often you want to check, if a sprite collides with one sprite of a group of other sprites (like a starship colliding with one of several asteroids or a ball colliding with one of several walls or bricks). Pygame comes with functions to do these kind of collision-checks for you, if you use classes, that inherit from Pygame's Sprite and Group classes.
To make this work, each Sprite object needs a variable "self.image", holding the sprite's surface (with its image drawn on top of it), and a variable "self.rect" to hold the rectangle with the sprite's coordinates.
Notice, that the sprite also exists at its position, when it's not drawn.
Usually, you just don't do a collision check on a sprite, that isn't visible at the moment.

It's intended, to inherit from the classes "Sprite" and "Group". You shouldn't just extend these classes a bit in classes like "MySprite" or "MyGroup". Instead you should create an extended Sprite and Group-class for every type of object on the screen. So there should be:

class Alien(pygame.sprite.Sprite):
...
class AliensGroup(pygame.sprite.Group):
...
class Player(pygame.sprite.Sprite):
...
class PlayerGroup(pygame.sprite.Group):
...
class PlayerShot(pygame.sprite.Sprite):
...
class PlayerShotsGroup(pygame.sprite.Group):
...

and so on. The "update"-methods can be used for the sprite movement. This already defines the structure of the game program.

Here's an example about it all. Unfortunately, a working example isn't that small (press 'q' to quit). It also requires some knowledge about inheritance in object orientated programming:

#!/usr/bin/python
# coding: utf-8

import pygame
from pygame.locals import *

import os

BLACK = (0, 0, 0)
RED   = (255, 0, 0)
GREY  = (127, 127, 127)

class Main:

    def __init__(self):

        os.environ['SDL_VIDEO_WINDOW_POS'] = "245, 40"
        self.screen = pygame.display.set_mode((800, 600))
        pygame.display.set_caption('Sprite Example')
        pygame.init()
        self.initSprites()
        self.clock = pygame.time.Clock()
        while True:
            self.clock.tick(60)

            # Clearing all sprites and rebuilding the screen:
            self.screen.fill(BLACK)

            if self.processEvents() == "quit":
                return
            self.player.checkCollisionWith(self.asteroids)
            self.allSprites.update(self.screen, 1)
            pygame.display.flip()

    def processEvents(self):
        pygame.event.pump()
        pressed = pygame.key.get_pressed()
        if pressed[K_q]:
            quit()
            return "quit"
        return 0

    def initSprites(self):
        self.player = RoundShip(20, 20, 100, 306, RED)
        self.asteroids = RoundAsteroidsGroup(RoundAsteroid(30, 30, 600, 100, GREY),
                                             RoundAsteroid(30, 30, 500, 300, GREY))
        self.allSprites = AllSpritesGroup(self.asteroids, self.player)

class RoundShip(pygame.sprite.Sprite):

    def __init__(self, width, height, x, y, colour):
        pygame.sprite.Sprite.__init__(self)
        self.surface = pygame.Surface((width, height))
        pygame.draw.circle(self.surface, colour, (width // 2, height // 2), height // 2)
        self.rect         = self.surface.get_rect()
        self.rect.topleft = (x, y)
        self.collided     = False

    def checkCollisionWith(self, spritegroup):
        if not self.collided:
            if pygame.sprite.spritecollide(self, spritegroup, False):
                print
                print "Crash!"
                print
                self.collided = True

    def update(self, screen, *args):
        if not self.collided:
            self.rect.x += args[0]
        screen.blit(self.surface, self.rect)

class RoundAsteroid(pygame.sprite.Sprite):

    def __init__(self, width, height, x, y, colour):
        pygame.sprite.Sprite.__init__(self)
        self.surface = pygame.Surface((width, height))
        pygame.draw.circle(self.surface, colour, (width // 2, height // 2), height // 2)
        self.rect         = self.surface.get_rect()
        self.rect.topleft = (x, y)

    def update(self, screen, *args):
        screen.blit(self.surface, self.rect)

class RoundAsteroidsGroup(pygame.sprite.Group):

    def __init__(self, *args):
        pygame.sprite.Group.__init__(self, *args)

    def update(self, screen):
        for s in self.sprites():
            s.update(screen)

class AllSpritesGroup(pygame.sprite.Group):

    def __init__(self, *args):
        pygame.sprite.Group.__init__(self, *args)

    def update(self, screen, *args):
        for s in self.sprites():
            s.update(screen, 1)

if __name__ == '__main__':
    Main()

It is also possible to add a whole group of sprites to another group:

self.asteroids = RoundAsteroidsGroup(...)
self.allSprites = AllSpritesGroup()
self.allSprites.add(self.asteroids)

Like Python dictionaries, Pygame Groups are not sorted. If you need a certain order of the sprites, define an attribute for it in the member's class. Then write a function using "group.sprites()" to get a list of the members. Then you can sort that list by the defined attribute. You know, like this:

class MyObject:

    def __init__(self, num):
        self.num = num

a = [MyObject(4), MyObject(1), MyObject(3), MyObject(2)]
for i in a:
    print i.num

print

# Sort list (in place) by object attribute:
a.sort(key=lambda x: x.num, reverse=False)

for i in a:
    print i.num

If you tried to use ordinary lists instead of Pygame Groups, you'd have to write "for i in mygroup: ... " over and over again. Or you'd have to write your own "Groups" class, but that would be like reinventing the wheel.
It's a useful feature of Pygame Groups, that sprites can be automatically removed from them at collision. For example, in a Shoot'em up-game, when colliding with the player's shot, sprites can be removed from a group named "enemies_alive" and added to a group "enemies_explosions", that has different tasks (like showing an explosion).


4. Transparent Pixels

Surfaces are in the shape of a rectangle. If they were fully drawn on top of each other, (usually black) borders would be visible. There are several methods to avoid that.

The first one is setting a "colorkey", like this:

        LEFT_OUT_COLOR = (100, 100, 100)
        self.surface = pygame.Surface((10, 10))
        self.surface = self.surface.convert()
        self.surface.set_colorkey(LEFT_OUT_COLOR)

After that, all pixels of that color are simply not blitted, when the surface is blitted onto the screen or onto another surface.

There's also the method "surface.convert_alpha()" instead of ".convert(), but I'm not so sure any more, what it does. I'll get back to that topic, when I have more information.


5. How to Rebuild the Screen Each Frame to Create Animation

This is probably the most important chapter. Please read it carefully.

The main loop should rebuild the screen every frame.

So at the beginning of the main loop a background should be blitted onto the screen. The background is a Surface object. The surface can hold an image (loaded as jpg or png), or can be filled with a solid colour. The main class should hold the (pure) background surface in memory, to quickly blit it onto the screen at the beginning of the main loop.

pygame.init()
self.screen = pygame.display.set_mode((800, 600))
self.background = pygame.Surface((800, 600))
self.background.fill((200, 200, 200)) # white
while True:
    self.screen.blit(self.background, (0, 0))
    ...
    pygame.display.flip()

Now: The blit-method lets you define, on which surface to blit. You want your background to stay clean. So you should go on blitting the sprites onto the screen.
This is different from real life: When a painter puts a canvas in front of the wooden frame of an easel, he will paint onto the canvas. If he tried to paint on the wooden frame behind the canvas somehow, the paint wouldn't be visible on the canvas.
In Pygame on the other hand, when you blit a clean background onto the screen, and keep on blitting onto the screen afterwards, the blitted objects will be visible in front of the background. And won't smudge it.

So that's how you build up your screen layer by layer in the main loop. And at the beginning of the next loop, it gets cleaned again by blitting the background on top again. And so on.

This should be the only blitting in the program. Don't try to blit or clean anything deep down in the subclasses.

Instead, this is what you should do:

  1. Blit the background onto the screen, wiping its content (see above).

  2. Manage your sprites in groups, using two classes for each sprite, derived from the classes "pygame.sprite.Sprite" and "pygame.sprite.Group".

  3. Inside your sprite classes, use a Surface for the image called "self.image" and a Rect called "self.rect" for the sprite's position.
    Use "self.rect.topleft = (100, 100)" to set the position of the sprite.

  4. From the main loop, call the group's "update" function, that calls the "update" functions of the sprites. The content of these functions has to be written by you. Use Python's "*args"-mechanism (described below) to manage different types of arguments, when the group calls the sprites' "update" functions.
    Use the sprites' "update" functions to move "self.rect" to the positions, where you want the sprites to be shown.
    (This is a bit like a painter, who stands before his canvas and has the image, he wants to paint, already in mind, but hasn't painted anything yet.)

  5. After "update" has brought the sprites into position, call the group's "draw" function from the main loop. "draw" then blits the sprites to the screen. You can use a special group, that holds all the sprites. Ideally, the call
    self.allSprites.draw(self.screen)

    should blit all the sprites of the program to the screen ("self.allSprites" being a group you defined, holding all your sprites). If "draw" doesn't do, what you wanted, rewrite it in your group class.

  6. Finally, call "pygame.display.flip()".

This should rebuild your screen each frame centralized from the main loop.


6. Python's "*args" mechanism

In step 3 above, you should call the sprite group's "update" function. It then calls different "update" functions inside the sprite classes of its members. But what, if the sprites need different arguments to their "update" functions?
Then, "*args" can help. When you use "*args" as a function parameter, it takes, whatever arguments are passed to it, no matter, what type they are or how many of them there are. Inside the functions, the parameters can be accessed through "args", which is an ordinary Python list.
This example should make it clear:

#!/usr/bin/python
# coding: utf-8

def takeDifferentArguments(*args):
    print args[0]

takeDifferentArguments(1, 2, 3)
takeDifferentArguments(("x", 3), "Hello")

So you can pass a lot of different arguments with your call to the group's "update" function, and the sprites' "update" functions then can sort out, which of them they need.


7. Keyboard and Joystick Input (Events)

What kind of keyboard I/O routine needs to be used in Pygame, depends on what kind of keyboard input you want to achieve:

  1. Getting just a single key press. If you hold down the key, multiple key-presses aren't detected. The rest of the application isn't blocked:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            return
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_q or if event.key == pygame.K_ESCAPE:
                pygame.quit()
                return
            else:
                return event.key
    

    Note: The pygame.KEYDOWN-event is triggered only once, in the moment the key is pressed down. When the user holds down the key for a longer time, and the function is called again by the main-loop, the pygame.KEYDOWN-event isn't detected any more. So the pygame.KEYDOWN-condition isn't true all the time, when a key is held down, it's only true in the very moment, the key is pressed.

  2. Another method to achieve this:
    pygame.event.pump()
    pressed = pygame.key.get_pressed()
    if pressed[K_q]:
        pygame.quit()
        return

  3. Non-blocking keyboard input with more control and repeated detection of held down keys. A variable is set up, and it has to be checked each loop for "pygame.KEYDOWN" and "pygame.KEYUP" events:
    #!/usr/bin/python
    # coding: utf-8
    
    import pygame
    
    class Main:
    
        def __init__(self):
            pygame.init()
            self.screen = pygame.display.set_mode((800, 600))
            self.keystates = {}
            for i in (pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN, pygame.K_q):
                self.keystates[i] = False
            self.running = True
            while self.running:
                if self.processEvents() == "quit":
                    self.running = False
            pygame.quit()
    
        def processEvents(self):
    
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    for i in self.keystates:
                        if event.key == i:
                            self.keystates[i] = True
                if self.keystates[pygame.K_q]:
                    return "quit"
    
                if event.type == pygame.KEYUP:
                    for i in self.keystates:
                        if event.key == i:
                            self.keystates[i] = False
    
            # Same indentation level as "for event in ...":
            for i in self.keystates:
                if self.keystates[i]:
                    print i
            return 0
    
    Main()

  4. Getting text input from the keyboard in Pygame. Getting the ASCII code of a single letter at a time, and also recognizing 'shift' being pressed. In the calling function, the returned keycode can then be turned into a character using the "chr()" function:

    #!/usr/bin/python
    # coding: utf-8
    
    import pygame
    
    class Main:
    
        def __init__(self):
            pygame.init()
            self.screen = pygame.display.set_mode((800, 600))
            self.running = True
            while self.running:
    
                result = self.getASCIICodeFromKeyboard()
                if result["quit"]:
                    self.running = False
    
                if result["keycode"] == pygame.K_LEFT:
                    print "Moved left."
                elif result["keycode"] == pygame.K_RIGHT:
                    print "Moved right."
                elif result["keycode"] == pygame.K_BACKSPACE:
                    print "'Backspace' pressed."
                elif result["keycode"] == pygame.K_DELETE:
                    print "'Delete' key pressed."
                elif result["keycode"] == pygame.K_HOME:
                    print "'Home' key pressed."
                elif result["keycode"] == pygame.K_END:
                    print "'End' key pressed."
                elif result["keycode"] == pygame.K_RETURN:
                    print "'Return' key pressed."
                elif result["keycode"] == pygame.K_ESCAPE:
                    print "'Escape' key pressed."
    
                elif result["keycode"] > 31 and result["keycode"] < 127:
                    print "'" + chr(result["keycode"]) + "' pressed."
    
    
        def getASCIICodeFromKeyboard(self):
    
            result = {"keycode" : 0, "quit": False}
            shift_pressed  = False
    
            if pygame.key.get_mods() & (pygame.KMOD_SHIFT | pygame.KMOD_LSHIFT):
                shift_pressed = True
    
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    result["quit"] = True
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        result["quit"] = True
    
                    a = event.key
                    # a-z to A-Z:
                    if event.key >= 97 and event.key <= 122 and shift_pressed:
                        a -= 32
                    result["keycode"] = a
    
            return result
    
    Main()

It is also possible to put events into the event queue yourself.
But when you want to do that, it may be a good idea to think of other ways to achieve what you want with just variables inside the main loop.
Anyway, "event.type" is just an integer. So you can get a suitable integer from pygame like this:

my_etype = pygame.USEREVENT + 1

Then, you can get yourself an Event object

my_event = pygame.event.Event(my_etype)

and throw it into the event queue:

pygame.event.post(my_event)

When the loop reaches the construction above, the event is fetched from the queue, deleted from it and processed:

for event in pygame.event.get():
    if event.type == my_etype:
        print "Event found."

The problem is, when you posted the event inside the loop, it will be posted each loop again. You have to set up a variable to take care of that, if you want the event to be processed only once. After you've set up that variable, you may realize, you don't need the event queue at all for your task.

If you want an event to be posted periodically into the queue - let's say once a second - there's another method:

pygame.time.set_timer(my_etype, 1000)

You don't have to define an Event object and post it yourself then, the timer does it for you. The for-loop works just like above.


I can get input of my vintage Atari joystick (2 axes, one button, connected to the PC with a special DB9 to USB interface) in Pygame.

The real-life code I'm using to check the first joystick and the keyboard (cursor keys for movement, and one of the Control keys for "fire") can be found here.
I've also posted it at the end of this page.


8. Controlling the Speed

23.11.2023: The content of this chapter had to be completely rewritten (it's done now) ! Sorry for that.

The general speed of the application is controlled by the clock like this:

self.clock = pygame.time.Clock()
while True:
    self.clocktick = self.clock.tick(60)
    ...

This limits the game to 60 frames per second (fps), which should be enough. To make this work, "self.clock.tick(FPS)" (where "FPS" is 50 or 60 for example) has to be called exactly once each loop.

The class of each object, that's supposed to be moved, then has to have its own "speed"-attribute.
When calling the object's "move"-method, the "clocktick" of the mainloop is then passed to it as an argument.
The amount, the object is moved each call by the "move"-method is then:

speed * clocktick

That way,

The "fps-rate" ("frames per second") can be printed with:

print(self.clock.get_fps())


9. Collision Checks

  1. To check, if two sprites collide, you can use:

    if pygame.sprite.collide_rect(sprite1, sprite2):
        ...

    Remember, that the forms of the sprites are not limited to an 8x8 matrix.
    Instead, if you have a sprite of let's say a "lasershot" with a size of just 2x8 pixels, the rectangle collision check in Pygame is rather precise.

    That's all fine and useful, but often, you still want more.

  2. To check, if a sprites collides with any member of a sprite group, you can do:
    hit_list = pygame.sprite.spritecollide(sprite, group, False)

    "hit_list" is a list of the sprites in the group, that collided with the given sprite. You can then check, if it's empty and so on.
    If you set the last argument to the function to "True", the collided sprites in the groups are removed from all their groups. Probably, they're then also not drawn any more, and that may be what you want. You can add them then to a special sprite group of "bodies" or "explosions" to show an explosion for some time. Set a counter, and if it's at zero, remove the sprites from the "explosions" group too.

  3. To check, if any member of a sprite group collides with any member of a second sprite group, you can do:
    hit_dictionary = pygame.sprite.groupcollide(group1, group2, False, False)

    "hit_dictionary" is a dictionary with the collided sprites of group1 as keys, and the collided sprites of group2 as values.
    The bool arguments to the function again define, if collided sprites are automatically removed from their groups.

It is a good idea, to do collision checks in a function in the main class. It has access to all required sprites and groups.


10. The Game's Main Loop

Probably the most difficult part of writing games is keeping in mind, what the game loop will do, when it reaches the code.

When writing simple procedural code like

a = 1
print a

each line is processed one by one, then the program ends. This doesn't work in (window-) programs with GUIs ("Graphical User Interfaces") though, because several things have to be taken care of at once: User-input has to be checked, buttons need to wait for user-action, the windows and their contents have to be displayed and so on.
It's just the same with games:

Everything has to be processed simultaneously.
Therefore, a main loop is used. But in games, the loop is even more difficult to handle than in GUI-programs. The later provide socalled "widgets" (like a button), that wait for the user, and when they are activated (clicked, for example), functions defined by the programmer are called.
But games just use an ordinary loop like

while True:
    ...

that runs through every function automatically all the time. If it's one time around, it is called a "frame", I guess. There can be 40 or 50 frames per second (fps), for example.
So when you write a function of a game, you have to keep in mind, that it won't just be called when you want it, but also thousands of times per seconds, when you don't want it.
So instead of explicitely calling a function once like in procedural or in GUI-programs, the loop of a game enters the functions automatically, and you have to prevent it from executing their code, if you don't want that.
This is be done by setting up a lot of variables.

It all becomes easier, when you write one class for every object on the screen (inherited by "pygame.sprite.Sprite"), and set up the main loop as described above. Then you know, that the loop enters the functions "update" and "draw" once each frame. And the conditions, when the loop shall not enter the functions but move on, are all handled inside the class.
Preventing the main loop from executing certain functions can then also be achieved, by simply removing a sprite from a certain group.


11. Screen Coordinates Stored in Arrays

A screen position is usually defined by x and y coordinates. In Pygame, "0, 0" is at the top left position of the screen. "x" defines the horizontal position, "y" the vertical one.

At some point, you may want to store the data of a screen in an array. The array has to hold several rows of values, so it is a two-dimensional array.

Now I'd like to point out, that the first square brackets of such an array have to hold the "y" coordinate, and the second ones the "x" coordinate. So, in the array, this is just the other way round than on the screen.
Such an array (of for example a grid of 10x5 positions) has to be built like this:

screenarray = []
value = 0
for y in range(5):
    row = []
    for x in range(10):
        row.append(value)
    screenarray.append(row)

And the values in the array have to be accessed like this:

print screenarray[y][x]

Not the other way round, as one might think. This has to be kept in mind, otherwise it will easily lead to confusion and to bugs in the program, that may be difficult to find.


12. Attribute "self.active" and Method "start"

It seems, I often use the attribute "self.active" and a method "start" in my classes for sprites.

This situation occurs, when a sprite isn't shown right from the start of the level, but lateron during the game. For example, when the player shoots.
The method "start" is useful, to give the sprite its initial position. Also the attribute "self.active" can be set to "True" there.

The method "start" can be called from the main class, but only once at certain events, that is, when the sprite is to appear.
Afterwards, the main loop calls the function "update" continously, that is once per frame. You can then block it from the function by checking the "self.active" variable. Further down in the function, there can be the condition to set it inactive:

def update(self):
    if not self.active:
        return
    ...
    if ... :
        self.active = False

But "self.active" can also be set inactive by other events like a collision.

This is not part of the Pygame documentation, this is just how I do it. But it seems to me, it could be some kind of a more general concept.


13. A "Scene" (or "Level") Class

In the short examples of the pygame documentation, at some time there's just the main loop like:

while True:
....

But what, if you have more than one level or game screens (which is, what most games have)? Then still the functions for the levels still have to be called somehow from that main loop.

In smaller games this can be done like this:

mainScreen = True
startGame = False
gameOver = False

while True:

    if mainScreen:
        ...
        if ... :
            startGame = True
            mainScreen = False

    elif startGame:
        ...
        if ... :
            startGame = False
            gameOver = True

    elif gameOver:
        ...

I have to admit, that looks quite clean.
But maybe you could create a class for each level. So the main loop remains like above, but at this highest level it just calls "run"-methods of several classes (one for each scenery or game level) and processes a return value of each class. So I suggest something like this:

class Main:

    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((800, 600))
        self.scenes = []
            self.append(Scene1(self.screen, ...))
            self.append(Scene2(self.screen, ...))
            self.append(Scene3(self.screen, ...))
        self.running = True

        # Main loop:
        while self.running:
            res = self.scenes[0].run()
            if res == "quit":
                self.running = False
            res = self.scenes[1].run()
            ...

class Scene1:

    def __init__(self, screen):
        self.screen = screen
        self.scene_finished = False
        while not self.scene_finished:
            res = self.processEvents()
            if res in ("quit", "go_on"):
                return res
            ...
            pygame.display.flip()
        return "go_on"

    def processEvents(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
            pygame.quit()
            return "quit"
            if event.type == pygame.KEYDOWN:
                ...
        return 0

class Scene2:

    def __init__(self, screen):
        self.screen = screen
        self.scene_finished = False
        while not self.scene_finished:
            res = self.processEvents()
            if res in ("quit", "go_on"):
                return res
            ...
            pygame.display.flip()
        if ... :
            return "game_won"
        else:
            return "game_over"

    def processEvents(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
            pygame.quit()
            return "quit"
            if event.type == pygame.KEYDOWN:
                ...
        return 0
...

That way, each scene is completely separated and can have completely different settings. The main loop always stays inside the current scene object. The programmer can concentrate on that scene and doesn't have to worry about the main loop wandering around somewhere else.

On the other hand, some things have to be repeated in the Scene-classes, "initSprites" for example. Also make sure, you pass the clock object of the Main class to the Scene classes and call "self.clock.tick(FPS)" in the loop of the scene classes again.


14. Sprite Class, Gamestate Class, Sprite-Initialization and an Example of it All

When you create sprites in Pygame, you're supposed to inherit from Pygame's "Sprite" class. But you still have to write a lot of functionality yourself. So it's probably a good idea to write a general Sprite class of your own, from which your other, more specific sprite classes inherit.
Such a general class should have the following features:

An example of such a class and how it may be used in the larger context of a program is shown below.

In the example, there will also be a class "Gamestate". It is useful to store game related variables, like how many quests the player has accomplished and how many lives are left. It can also hold a variable "self.state", to store, in which state the game is in. That is, which level the game is running, or if it already shows the "Game Over" screen. This variable can be used in the main loop to control, which game screen is shown.
Once per frame, early in the main loop, a function "self.checkGamestate()" should be called to check conditions, if this variable has to be changed, so that the game displays a different game screen.

The function "self.initSprites()" should be called only once, before the main loop. It should generate all sprites used in the game before the game starts. So all sprites exist from the beginning, but only the ones used in the current level (or game screen) are blitted to the screen. To restart a level - for example, when the player has died, but has another life - all sprites of the level are reset to their initial positions and states. It's better to reuse the same sprites, that were created in the beginning, than to recreate the sprites, each time the level is restarted. Because that would cost more time and memory, so it wouldn't be good programming practice.
Reusing the same sprites also means, that you must not remove them from all groups, when they are not needed any more in the level. Because when the level is restarted, they are needed again. So you have to collect the unused sprites in special groups, that can be called "DestroyedStarshipsGroup" for example. When the level is restarted, they can then be reactivated, by removing them from these groups and adding them to the ordinary groups (like "StarshipsGroup") again.

So here's an example:

#!/usr/bin/python
# coding: utf-8

import pygame
from pygame.locals import *

FPS          = 60

class MySprite(pygame.sprite.Sprite):

    def __init__(self, name, spritenr, x, y, speed):
        pygame.sprite.Sprite.__init__(self)
        self.name           = name
        self.spritenr       = spritenr
        self.speed          = speed
        self.surfaces       = self.createSurfaces()
        self.surfaceind     = 0
        self.init_position  = (x, y)
        self.moveTo(x, y, 0)

    def reset(self):
        self.moveTo(self.init_position[0], self.init_position[1], 0)

    def changeSurface(self, surfaceind):
        self.moveTo(self.rect.left, self.rect.top, surfaceind)

    def moveTo(self, x, y, surfaceind):
        self.surfaceind   = surfaceind
        self.surface      = self.surfaces[self.surfaceind]
        self.rect         = self.surface.get_rect()
        self.rect.topleft = (x, y)

    def update(self, surface):
        surface.blit(self.surface, self.rect)

    def createSurfaces(self):
        # Load images and put them on surfaces.
        # Then put these into a list:
        surfaces = []
        surface = pygame.image.load(...)
        # Don't forget this important step:
        surface = surface.convert_alpha()
        surfaces.append(surface)
        return surfaces


class Starship(MySprite):

    def __init__(self, name, spritenr, x, y, speed):
        MySprite.__init__(self, name, spritenr, x, y, speed)
        self.x = x
        self.y = y

    def move(self, keystates, clocktick):

        if keystates[K_LEFT]:
            self.move_left(clocktick)

        if keystates[K_RIGHT]:
            self.move_right(clocktick)

    def move_left(self, clocktick):
        self.x -= self.speed * clocktick
        self.moveTo(self.x, self.y, self.surfaceind)

    def move_right(self):
        ....


class StarshipsGroup(pygame.sprite.Group):

    def __init__(self, *args):
        pygame.sprite.Group.__init__(self, *args)

    def update(self, surface):
        for s in self.sprites():
            s.update(surface)


class Gamestate:

    def __init__(self):
        self.state = "level_1"

class Main:

    def __init__(self):
        pygame.init()
        self.screen    = pygame.display.set_mode( ... )
        self.clock     = pygame.time.Clock()
        self.running   = True
        self.gamestate = Gamestate()
        self.keystates = {K_LEFT : False, K_RIGHT: False}
        self.initSprites()

        while self.running:
            self.clocktick = self.clock.tick(FPS)

            self.checkGamestate()

            # Changes "self.keystates":
            self.processEvents()

            if self.gamestate.state == "intro":
                ....

            if self.gamestate.state == "level_1":
                self.starship.move(self.keystates, self.timer)
                self.starships.update(self.screen)
                pygame.display.flip()

            if self.gamestate.state == "game_over":
                ....

        # Main loop over:
        pygame.quit()

    def initSprites(self):

        self.starship = Starship("apollo", 1, 100, 100)
        self.starships = StarshipsGroup()
        self.starships.add(self.starship)

    def checkGamestate(self):
        # Checks conditions for changing "self.gamestate.state".
        ....

    def processEvents(self):
        for event in pygame.event.get():
            ....

If this makes sense to you, you already have an idea, how to program in Pygame.


15. Plotting Huge Numbers of Pixels Using PixelArrays

With screen resolutions like "1920x1080", today's pixels are extremely small.
Of course you can calculate and plot little squares of n x n pixels, that you can see as one point then.
It probably wouldn't be fast enough to plot whole sceneries for a game. But you can plot on pygame Surfaces at initializiation and use these surfaces in sprites. This can be done like this:

...
surface = pygame.Surface((20, 30))
pxarray = pygame.PixelArray(surface)
black = (0, 0, 0)
for i in range(0, 20):
    pxarray[i][i] = black
del pxarray
...

The PixelArray-object is designed to quickly plot a huge number of pixels onto the surface (for rendering mathematical graphs and such). Therefore it locks the surface while plotting. To unlock the surface (for blitting and so on), you have to delete the PixelArray-object (with "del pxarray"), when plotting is finished. In recent Pygame you call "pxarray.close()" instead.

If you want to plot in larger pixels, in squares of 3x3 for example to get an Amiga-type resolution, you should consider (instead of using a PixelArray) making a 3x3-surface, filling it in a color with ".fill()" and moving it around with its according Rect-object.


16. Drawing Simple Shapes

If you just want to draw simple shapes like lines, circles, polygons and such, you don't have to create a PixelArray. For example, if you want to simulate lower resolutions and want to use small rectangles as low-res pixels, you can just use the routines to draw rectangles.
Here's an example what can be done by drawing lines:

#!/usr/bin/python
# coding: utf-8

import pygame
from pygame.locals import *
import os

BLACK = (0, 0, 0)
WHITE = (200, 200, 200)

pygame.init()
os.environ['SDL_VIDEO_WINDOW_POS'] = "240, 40"
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Some Lines")
clock = pygame.time.Clock()

surface      = pygame.Surface((800, 600))
rect         = surface.get_rect()
rect.topleft = (0, 0)
linesize     = 1

for i in range(100, 550, 10):
    pygame.draw.line(surface, WHITE, (160, i + 50), (475 - i + 100, 0), linesize)
    pygame.draw.line(surface, WHITE, (475 - i + 100, 0), (640, 600 - i), linesize)
    pygame.draw.line(surface, WHITE, (640, 600 - i), (i + 160, 600), linesize)
    pygame.draw.line(surface, WHITE, (i + 160, 600), (160, i + 50), linesize)

while True:
    clock.tick(60)
    pygame.event.pump()
    pressed = pygame.key.get_pressed()
    if pressed[K_q]:
        pygame.quit()
        break
    screen.fill(BLACK)
    screen.blit(surface, rect)
    pygame.display.flip()

Sometimes it's also possible to use the "fill"-method combined with a "Rect"-object for drawing rectangles onto a surface. That may even be faster than "pygame.draw.rect()".


17. Playing Effect Sounds (".wav")

Playing ".wav"-files as effect sounds in Pygame, is relatively straightforward.
When initializing the game, create a hash "self.sounds" once, and fill it with pygame.mixer.Sound objects:

self.sounds = {}
self.sounds["crash"] = pygame.mixer.Sound("filename_for_crash.wav")

Remember, not to do the initialization repeatedly, especially not in the main loop (which would be executed like 50 times per second, which would be really bad).

When an event occurs in the main loop, so that the sound should be heard, just use the Sound objects ".play()" method:

if event:
    self.sounds["crash"].play()

Easy. In earlier versions of Pygame, you maybe must have initialized the mixer object first, but that does "pygame.init()" for you now.
What you should do just before "pygame.init()", is to set a reasonable sound buffer size. If the sound buffer is too small, you'll get unpleasant crackling noises. If it's too big, you'll get "latency", that means, all sounds are played with an audible delay. The following command would set the sound buffer to 512K (or is it bytes?), which is quite small. But it's ok, if your sound files are rather small (like mono beeping noises of 8-bit computers for example):

pygame.mixer.pre_init(44100, -16, 1, 512)
pygame.init()


18. Scrolling the Background

In 2D games, often the background is scrolled, while the player avatar is animated on a spot, giving the illusion of movement.
That is quite a nice effect, that is found in many classic games such as "River Raid" or "Dropzone" on the Atari 800 XL. Or "Giana Sisters", "Turrican", "Hybris", "Silkworm" or "Katakis" on the Amiga 500.

To make the background scroll, there has to be a scenery, that is larger than the screen. And a smaller, visible part of the scenery. This part is usually called a "viewport".

In Pygame, the key to achieve smooth scrolling of the background is in my opinion the possibility to pass a third argument to the ".blit()" function of surfaces. That third argument defines, what area of a surface is blitted. So the corresponding code would look something like this:

    self.screen         = ...
    self.scenerysurface = pygame.Surface((8192, 1024))
    self.arearect       = pygame.Rect((1024, 0), (640, 480))
    self.screen.blit(self.scenerysurface, (0, 0), self.arearect)

That would blit just a part of 640x480 pixels of the whole scenery surface to the main screen at position (0, 0). The blitted pixels are located in the scenery surface at position (1024, 0).
By moving the area rectangle:

    self.arearect.topleft = (1664, 0)

and blitting just the selected part of the scenery surface to the main screen, the scrolling of the background is created.

The scenery surface, that is much larger than the visible screen, can be prepared once before the main loop starts. After that, it mustn't be changed. Just parts of it have to be extracted like shown above and blitted to the main screen. Other objects, like for example the player avatar, have to be blitted onto the main screen afterwards. Not onto the scenery surface.

To show in practice, how this can be done, I wrote an example script. But as it's a bit too large for this site, I put it onto my Github page. I put several smaller Pygame scripts into one Github project called "pygame_experiments", so to get the example script, you'd have to download the whole package (as a .zip-file) on the main project page. Click on the green "Code" button and select "Download ZIP". Don't worry, the file's not big, it just contains a few small Python scripts.


19. Physics Simulation with Pymunk

There's a library which makes the simulation of physics (like for example gravity) in a video game easier. It is called "Pymunk" (as it's based on the library "Chipmunk"). Here's an example from a tutorial:

#!/usr/bin/python
# coding: utf-8

import pygame
from pygame.locals import *
import pymunk
import pymunk.pygame_util
import os

GRAY = (220, 220, 220)

os.environ['SDL_VIDEO_WINDOW_POS'] = "300, 200"
pygame.init()
screen = pygame.display.set_mode((640, 240))
pygame.display.set_caption("Bouncing Ball")
clock = pygame.time.Clock()
draw_options = pymunk.pygame_util.DrawOptions(screen)

space = pymunk.Space()
space.gravity = (0, -900)

b0 = space.static_body
segment = pymunk.Segment(b0, (0, 0), (640, 0), 4)
segment.elasticity = 1

body = pymunk.Body(mass = 1, moment = 10)
body.position = (300, 200)

circle = pymunk.Circle(body, radius = 30)
circle.elasticity = 0.95
space.add(body, circle, segment)
clock = pygame.time.Clock()

while True:
    clock.tick(60)
    pygame.event.pump()
    pressed = pygame.key.get_pressed()
    if pressed[K_q]:
        pygame.quit()
        break
    screen.fill(GRAY)
    space.debug_draw(draw_options)
    pygame.display.flip()
    space.step(0.01)


20. Further Pygame Tutorials


21. Skeleton Code

Here I'm posting the skeleton-code I'm using for starting a Pygame project. The scripts are called

They can also be found on my GitHub-page.

"pygame_code-skeleton.py":

#!/usr/bin/python3
# coding: utf-8

import pygame
import os
from inputhandler import InputHandler

"""
    pygame_code-skeleton.py 1.0 - Some Pygame code to build from there.

    Copyright (C) 2023 Hauke Lubenow

    This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see .
"""

SCALEFACTOR  = 3
FPS          = 60

COLORS = {"black"   : (0, 0, 0),
          "blue"    : (0, 0, 197),
          "magenta" : (192, 0, 192),
          "red"     : (192, 0, 0),
          "green"   : (0, 192, 0),
          "cyan"    : (0, 192, 192),
          "yellow"  : (189, 190, 0),
          "white"   : (189, 190, 197),
          "gray"   :  (127, 127, 127),
          "transparent" : (0, 0, 0, 0) }

c = ("black", "blue", "red", "magenta", "green", "cyan", "yellow", "white")
COLORNRS = {}
for i in c:
    COLORNRS[i] = c.index(i)


class MySprite(pygame.sprite.Sprite):

    def __init__(self, name, id, speed):
        pygame.sprite.Sprite.__init__(self)
        self.name           = name
        self.id             = id
        self.speed          = speed
        self.image          = None
        self.rect           = None

    def setPosition(self, spos_x, spos_y):
        self.spos_x = spos_x
        self.spos_y = spos_y

    def setPCPosition(self):
        self.pcpos_x = self.spos_x * SCALEFACTOR
        self.pcpos_y = self.spos_y * SCALEFACTOR

    def getPosition(self):
        return (self.spos_x, self.spos_y)

    def draw(self, screen):
        self.setPCPosition()
        self.rect.topleft = (self.pcpos_x, self.pcpos_y)
        screen.blit(self.image, self.rect)


class Ball(MySprite):

    def __init__(self, name, id, width, height, x, y, colorname, speed):
        MySprite.__init__(self, name, id, speed)
        self.createImage(width, height, colorname)
        self.setPosition(x, y)

    def move(self, direction, ssize_screen, clocktick):
        if direction == "left":
            self.spos_x -= self.speed * clocktick
            if self.spos_x < 0:
                self.setPosition(ssize_screen[0], self.spos_y)
        if direction == "right":
            self.spos_x += self.speed * clocktick
            if self.spos_x > ssize_screen[0]:
                self.setPosition(0, self.spos_y)
        if direction == "up":
            self.spos_y -= self.speed * clocktick
            if self.spos_y < 0:
                self.setPosition(self.spos_x, ssize_screen[1])
        if direction == "down":
            self.spos_y += self.speed * clocktick
            if self.spos_y > ssize_screen[1]:
                self.setPosition(self.spos_x, 0 )

    def createImage(self, ssize_x, ssize_y, colorname):
        self.image     = pygame.Surface((ssize_x * SCALEFACTOR, ssize_y * SCALEFACTOR))
        self.image     = self.image.convert_alpha()
        self.rect      = self.image.get_rect()
        self.image.fill(COLORS["transparent"])
        pygame.draw.circle(self.image, COLORS[colorname], (self.rect.width // 2, self.rect.height // 2), self.rect.height // 2)


class BallGroup(pygame.sprite.Group):

    def __init__(self, *args):
        pygame.sprite.Group.__init__(self, *args)

    def draw(self, surface):
        for s in self.sprites():
            s.draw(surface) 

class Main:

    def __init__(self):
        os.environ['SDL_VIDEO_WINDOW_POS'] = "185, 30"
        pygame.init()
        self.ssize_screen = (256, 192) 
        self.screen = pygame.display.set_mode((self.ssize_screen[0] * SCALEFACTOR, self.ssize_screen[1] * SCALEFACTOR))
        pygame.display.set_caption("Hello Ball")
        self.ih = InputHandler(True)
        self.clock = pygame.time.Clock()
        self.initSprites()
        self.running = True
        while self.running:
            self.clocktick = self.clock.tick(FPS)
            self.screen.fill((0, 0, 0))

            self.ballgroup.draw(self.screen)

            if self.checkInput() == "quit":
                print("Bye.")
                self.running = False
                continue
            pygame.display.flip()
        pygame.quit()

    def initSprites(self):
        self.ball = Ball("ball", 1, 10, 10, 130, 100, "red", speed = 0.15)
        self.ballgroup = BallGroup()
        self.ballgroup.add(self.ball)

    def checkInput(self):
        # "action" is a dictionary with the keys "left right up down fire quit":
        action = self.ih.getKeyboardAndJoystickAction()
        for i in action.keys():
            if i == "quit" and action[i]:
                return "quit"
            if action[i]:
                self.ball.move(i, self.ssize_screen, self.clocktick)
        return 0

Main()



Back to the main page


Author: hlubenow2 {at-symbol} gmx.net