Writing 2D Video Games in Python 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
  4. How to Rebuild the Screen Each Frame to Create Animation
  5. Python's "*args" mechanism
  6. Events
  7. Controlling the Speed
  8. Collision Checks
  9. Joystick Input
  10. The Game's Main Loop
  11. Attribute "self.active" and Method "start"
  12. A "Scene" (or "Level") Class
  13. Plotting Single Pixels
  14. Plotting Transparent Pixels


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.

The first time with Pygame will be tough, and your first programs won't be good. It will take time, before you can write programs the way they are intended to be written. You will have to study the Pygame examples, but also the code of many games of other people.
But if you don't give up and keep learning, maybe some day you can master writing games with Pygame.


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.

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.

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

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 < 800:
        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 you 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)

def drawImage(width, height, colour):
    surface = pygame.Surface((width, height))
    pygame.draw.circle(surface, colour, (width // 2, height // 2), height // 2)
    return surface

class RoundShip(pygame.sprite.Sprite):

    def __init__(self, width, height, x, y, colour):
        pygame.sprite.Sprite.__init__(self)
        self.image = drawImage(width, height, colour)
        self.rect  = pygame.Rect((x, y), (width, height))
        self.collided = False

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

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

class RoundAsteroid(pygame.sprite.Sprite):

    def __init__(self, width, height, x, y, colour):
        pygame.sprite.Sprite.__init__(self)
        self.colour = colour
        self.image = drawImage(width, height, colour)
        self.rect  = pygame.Rect((x, y), (width, height))

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

class RoundAsteroidsGroup(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 AllSpritesGroup(pygame.sprite.Group):

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

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

if __name__ == '__main__':
    Main()

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. 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.


5. 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.


6. Events

Pygame uses a socalled event queue to handle events. To process keyboard events, the following construction is useful:

pygame.event.pump()
pressed = pygame.key.get_pressed()
if pressed[K_q]:
    pygame.quit()
    return

This tells you, which keys are pressed at the moment. The other construction (described below) just tells you, which keys have been pressed in just that loop. So it doesn't recognize, if a key was held down for a longer period.
The other, more detailed construction, looks like this:

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:
            pygame.quit()
            return

As mentioned, it can handle keyboard events too, but then the problem described above occurs. It is possible to use both constructions together. Then, you can skip the line "pygame.event.pump()", because the other construction includes this. Otherwise the line should be called each loop to clear the event queue, so that it doesn't overflow.

Actually, you can also use just the second construction to check, if keys were pressed for a longer period, too. You then have to set up a variable and process both, the "pygame.KEYDOWN" and the "pygame.KEYUP" event. Joystick input is processed in a similar way. A working example:

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

import pygame

class Main:

    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((800, 600))
        self.keystate = {}
        d = ("left", "right", "up", "down")
        for i in d:
            self.keystate[i] = False
        self.running = True
        while self.running:
            if self.processEvents() == "quit":
                pygame.quit()
                self.running = False

    def processEvents(self):
        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_q:
                    return "quit"
                if event.key == pygame.K_LEFT:
                    self.keystate["left"] = True
                if event.key == pygame.K_RIGHT:
                    self.keystate["right"] = True
                if event.key == pygame.K_UP:
                    self.keystate["up"] = True
                if event.key == pygame.K_DOWN:
                    self.keystate["down"] = True

            if event.type == pygame.KEYUP:
                if event.key == pygame.K_LEFT:
                    self.keystate["left"] = False
                if event.key == pygame.K_RIGHT:
                    self.keystate["right"] = False
                if event.key == pygame.K_UP:
                    self.keystate["up"] = False
                if event.key == pygame.K_DOWN:
                    self.keystate["down"] = False

        if self.keystate["left"]:
            print "Moving left."
        if self.keystate["right"]:
            print "Moving right."
        if self.keystate["up"]:
            print "Moving up."
        if self.keystate["down"]:
            print "Moving down."

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.


7. Controlling the Speed

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

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

This limits the game to 60 frames per second (fps).
That's good, but it's not enough. Because some objects on the screen are supposed to move faster (or slower) than others.
The function "pygame.time.get_ticks()" returns the number of milliseconds that have passed, since the application started. With this, you can set up a delay time in a certain function like this:

...
self.enemies = EnemiesGroup(...)

# Main loop:
while True:
    self.timer = pygame.time.get_ticks()
    self.enemies.update(self.timer)
    self.enemies.draw(...)
    pygame.display.flip()

class EnemiesGroup(pygame.sprite.Group):

    def __init__(self, *args):
        pygame.sprite.Group.__init__(self, *args)
        self.timer = pygame.time.get_ticks()
        self.delaytime = 200

    def update(self, currenttime):
        if currenttime - self.timer > self.delaytime:
            print "Doing something slowed down."
            ...
            self.timer += self.delaytime

When you call "get_ticks()" at "init" of the object, but then a lot of time passes in the game until the timed update function is called, it may happen, that the code is executed very fast for some seconds, and then slows down to the wanted frequency. In this case "self.timer += self.delaytime" isn't sufficient to reach the current time, because so much time has passed in the meantime. The solution for this is to do "self.timer = pygame.time.get_ticks()" again inside the class, just before the main loop calls "update". A function "setActive" is a good place to do that.

You can print the "fps-rate" ("frames per second") with:

print self.clock.get_fps()

Sometimes, you can also just count down a variable, and when it's zero, take the sprite from a group using "group.remove(sprite)". When all sprites in the group are drawn in the main loop, but this sprite has been removed, it is not drawn anymore.
Well, if you remove the sprite from the group of all sprites, and it respawns later, it's kind of difficult to get it back into that group (as the sprite itself doesn't know groups, it's not member of any more).


8. 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, like they used to be long ago (on the Sinclair ZX Spectrum).
    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.


9. Joystick Input

I can get simple input of my vintage Atari joystick (2 axes, one button) (connected to PC with a special DB9-parallel port interface) in Pygame like this:

pygame.init()
joystick = pygame.joystick.Joystick(0)
joystick.init()
joystickstate = {"left" : False, "right" : False, "up" : False, "down" : False}
while True:
    for event in pygame.event.get():
        if event.type == pygame.JOYBUTTONDOWN:
            print "Bang!"
        if event.type == pygame.JOYAXISMOTION:

            # Joystick pushed:
            if event.axis == 0 and int(event.value) == -1:
                joystickstate["left"] = True
            if event.axis == 0 and int(event.value) == 1:
                joystickstate["right"] = True
            if event.axis == 0 and int(event.value) == -1:
                joystickstate["up"] = True
            if event.axis == 0 and int(event.value) == 1:
                joystickstate["down"] = True

            # Joystick released:
            if event.axis == 0 and int(event.value) == 0:
                joystickstate["left"] = False
                joystickstate["right"] = False

            if event.axis == 1 and int(event.value) == 0:
                joystickstate["up"] = False
                joystickstate["down"] = False

    if joystickstate["left"]:
        print "Going left."

    if joystickstate["right"]:
        print "Going right."

    if joystickstate["up"]:
        print "Going up."

    if joystickstate["down"]:
        print "Going down."

For auto-fire, write it similar to the joystick-directions:

joystickstate = {"fire" : False}
for event in pygame.event.get():
    if event.type == pygame.JOYBUTTONDOWN:
        self.joystickstate["fire"] = True
    if event.type == pygame.JOYBUTTONUP:
        self.joystickstate["fire"] = False
if self.joystickstate["fire"]:
    print "Bang, bang, bang!"

This is similar to processing "pygame.KEYDOWN" and "pygame.KEYUP" as shown above.


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. 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.


12. 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:
....

In a real game, you won't get far with this construction. Think of a game like "Jumpman" (Epyx, 1983). It has 30 levels. Do you want to put everything (including 30 different screen settings) directly into that loop?

Well, some smaller games get along 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.

Pygame itself doesn't provide a class to separate levels, but it seems an obvious idea, and the classes of Python (or any other object oriented language) are absolutely capable of doing that.
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.
It can happen, that a loop in one scene runs much faster than "another" loop in another scene, because in the scene, there's less to do. This can confuse the clock and timer for a while. I don't know yet, what to do about that.


13. Plotting Single Pixels

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.


14. Plotting Transparent Pixels

For games, you most likely import png-images and use them on surfaces.
Today, besides RGB ("red", "green", blue") for the colour, the pixels of images can also have a fourth information channel "alpha", that represents the pixel's opacity, that is, how opaque or on the other hand how transparent a pixel is.
Images with transparent backgrounds may be what you want in games, because then you don't have to worry about what colour is below the image's borders.
Let's say, you create an image for the sprites yourself, by plotting onto a surface (as described above). You could then plot tranparent pixels like this:

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

It seems, the jpg-format doesn't support an alpha-channel. There's a second method in Pygame to get a colour to be drawn as transparent. That is by defining such a colour as a "colorkey", using "surface.set_colorkey()":

image = pygame.image.load("myimage.jpg")
transparent = (255, 255, 255) # Use the colour 'white' as 'transparent'.
image.set_colorkey(transparent)
screen.blit(image, (100, 100))


Back to the computing-page


Author: hlubenow2 {at-symbol} gmx.net