pygame

Eine kleine Einführung

Da der Einstieg in pygame manchmal nicht so leicht ist, hier eine kleine Einführung.

Anmerkung: Dies ist kein Pythontutorial! Grundwissen über Python wird vorausgesetzt.

Das Ziel

Es ist immer gut, ein Ziel zu haben. Am Ende dieser Einführung werden wir ein einfaches Spiel geschrieben haben, bei dem man eine Spielfigur über ein Spielfeld bewegen kann, Items einsammeln und gleichzeitig den in immer größerer Anzahl erscheinenden Gegnern ausweichen muss. Einige Spiel-Informationen werden dabei auf einer Statusleiste dargestellt.

Das Grundgerüst

Um überhaupt etwas mit pygame machen zu können, gibt es einige Dinge, die man immer braucht. Zunächst muss man natürlich pygame mit import pygame importieren. Dann muss man jedes Modul im pygame-Paket auch noch initialisieren, damit man es benutzen kann. Dies geht über die init()-Funktion, die jedes pygame-Modul besitzt. Um das ganze etwas zu erleichtern, kann man aber auch über pygame.init() alle Module auf einmal initialisieren.

Als nächstes brauchen wir ein Fenster, indem unser Spiel angezeigt wird. Die Darstellung geschieht in pygame.display und mit der methode set_mode((*width*, *height*)) wird ein Fenster in der angegebenden Größe erzeugt. Zurückgegeben wird ein Surface-Objekt, über das man nun Dinge darstellen kann. Eine andere, nicht ganz so wichtige Methode von pygame.display ist set_caption(*title*), mit der der Text in der Fensterleiste geändert werden kann.

So sieht unser Programm nun aus:

1
2
3
4
import pygame
pygame.init()
screen = pygame.display.set_mode((500,300))
pygame.display.set_caption("Pygame ist toll")

Wenn wir das nun ausführen, sieht man noch nicht viel. Nur ein kurzes Aufblitzen eines Fensters. Das ist auch nicht weiter verwunderlich, da das Programm sofort beendet wird, wenn die vier Zeilen ausgeführt wurden. Wir brauchen also eine Schleife, einen mainloop, die das Programm erst verlässt, wenn es wirklich beendet werden soll. Für diesen Fall müssen die Events ausgewertet werden, die in Form einer Liste von Event-Objekten über pygame.event.get() verfügbar sind. In einer Schleife kann man nun prüfen, ob es Events gibt, bei denen das Programm beendet werden soll. Dazu prüft man erst den Typ eines Events und dann bei Bedarf noch weitere Attribute. Zur Erleichterung kann man mit from pygame.locals import * viele Konstanten importieren, unter anderem auch die Eventtypen. Um die Rechenleistung nicht komplett auszureitzen, brauchen wir noch eine Bremse für den mainloop. Dazu gibt es das Modul pygame.time, das eine Klasse Clock bereitstellt, mit deren tick(*ms*)-Methode man timeouts setzen kann. Das ganze sieht dann so aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import pygame
from pygame.locals import *

def main():
    pygame.init()
    pygame.display.set_mode((500,300))
    pygame.display.set_caption("Pygame ist toll")
    clock = pygame.time.Clock()

    while True:
        clock.tick(40)
        for event in pygame.event.get():
            if event.type == QUIT:
                return
            elif event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    return

if __name__ == '__main__': main()

Dies erzeugt ein 500*300 Pixel großes Fenster, das sich über das Fensterkreuz oder Esc schließen lässt. Wie man sieht, habe ich das ganze in eine Funktion gepackt, wie es sich für ein ordentliches Pythonprogramm gehört.

Basiswissen 1

Blitten

Blitten ist das Übertragen von Informationen eines Bildes auf ein anderes. Dieser Vorgang ist der Kern von pygame, denn alles was man am Ende sieht, wurde mindestens einmal geblitet, und zwar auf den Bildschirm, bzw. das den Bildschirm repräsentierende Surface-Objekt. Dieses wird von pygame.display.set_mode zurückgegeben. Mit dem Befehl pygame.display.flip() wird der Inhalt dieser Surface auf das Fenster übertragen und so für uns sichtbar.

Surface

Die Klasse pygame.Surface ist genau so wichtig, wie das Blitten, da nur Surfaces geblittet werden können. Eine Surface repräsentiert ein Bild einer bestimmten Größe und weiteren festgelegten Parametern. Eine Surface kann man über die Methode fill(*color*) mit Farbe füllen (color ist ein 3er Tupel mit rgb-Werten) oder halt auf andere Surfaces blitten. Wenn man Bilder aus Dateien lädt, wird auch automatisch eine Surface erzeugt, auf die dann die Bildinformationen geschrieben werden.

Jetzt kommt Farbe ins Spiel!

Schauen wir und die Theorie mal in der Praxis an. Doch dazu brauchen wir erst einmal etwas, das wir darstellen wollen. Ich hab da mal was vorbereitet...

http://media.bytemuehle.de/downloads/pygame-tutorial/player.png

Nun müssen wir von diesem Bild aber noch zu unserer Surface kommen. Dazu gibt es pygame.image.load(*path*). Die von dieser Funktion zurückgegebene Surface sollte nun noch über die convert() in ein günstigeres Pixelformat umgerechnet werden, sodass das Blitten später, das ja in jedem Schleifendurchlauf, also etliche Male pro Sekunde, möglichst schnell geht. Ein anderer Punkt ist noch die Transparenz, bei der es beim Laden teilweise Probleme geben kann, je nach genutztem Dateiformat (zB. .gif). So kann man mit Surface.set_colorkey(*color*, *flag*) eine Farbe angeben, die als transparent dargestellt wird. All diese Funktionen, die beim Laden eines Bildes aus einer Datei aufgerufen werden müssen, sollten oder könnten, lassen sich in einer Funktion zusammenfassen (aus dem offiziellen pygame-Tutorial):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def load_image(name, colorkey=None):
    try:
        image = pygame.image.load(name)
    except pygame.error, message:
        print 'Cannot load image:', 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

Wenn colorkey gleich -1 ist, wird der Farbwert des ersten Pixels genommen (linke, obere Ecke).

Jetzt können wir unser Bildchen auf den Hintergrund blitten. Die main-Funktion sieht dann wie folgt aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def main():
    pygame.init()
    screen = pygame.display.set_mode((500,300))
    pygame.display.set_caption("Pygame ist toll")
    clock = pygame.time.Clock()

    player_img = load_image("player.png", -1)

    while True:
        # maximal 40 fps
        clock.tick(40)

        # events bearbeiten
        for event in pygame.event.get():
            if event.type == QUIT:
                return
            elif event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    return

        # den Bildschirm mit einer Hintergrundfarbe füllen und so
        # gleichzeitig das alte Bild löschen
        screen.fill((120,30,66))

        # das Bild in die obere linke Ecke blitten
        screen.blit(player_img, (10, 10))

        # alles aufs Fenster flippen
        pygame.display.flip()

Aufgabe

Erweitere das Programm so, dass sich das Bild gleichmäßig in eine Richtung bewegt, bis es an den Rand des Fensters kommt, wo es wieder umkehrt!

(Dazu sind keine neuen pygame-Funktionen nötig!)

Basiswissen 2

Alles schön und gut, das mit dem Bildchen Laden und Blitten, aber so ist es nicht wirklich elegant und würde schon bei einem etwas größeren Programm mit etwas mehr Objekten unübersichtlich und chaotisch. Kurz: Wer hoch hinaus will, braucht mehr! Und zwar die folgenden drei Klassen:

Rect

Die Klasse pygame.Rect repräsentiert - wie der Name schon sagt - ein Rechteck. Das klingt jetzt erstmal nicht so innovativ, aber es erleichtert die Arbeit ungemein, denn hat man einmal ein Rect erzeugt, hat man 20 Attribute, die Verschiedene Punkte oder Werte des Rechtecks beschreiben:

top, left, bottom, right topleft, bottomleft, topright, bottomright midtop, midleft, midbottom, midright center, centerx, centery size, width, height w,h

Ich denke mal, die Bezeichnungen müssen nicht weiter erklärt werden. Es handelt sich immer um einen oder zwei Pixelangaben. Das tolle ist nun, dass man über jedes dieser Attribute, das gesamte Rechteck verschieben kann, ohne sich um die anderen Punkte oder irgendwelche Umrechnungen kümmern muss. Man kann einfach

mein_rect.center = 180, 230

schreiben, und schon wird das Rect so verschoben, dass es genau über dem Punkt (180, 230) liegt. Das kann einem schon einiges abnehmen, doch wirklich komfortabel wird es erst zusammen mit Sprites...

Ach so, vielleicht sollte ich noch sagen, wie man überhaupt zu einem Rect kommt. Nun, es gibt zwei Möglichkeiten:

  1. Man erzeugt es ganz normal, wobei es auch verschiedene Varianten gibt:
    1. pygame.Rect(left, top, width, height)
    2. pygame.Rect((left, top), (width, height))
    3. pygame.Rect(object)

    Zu c) ist anzumerken, das object ein Objekt sein muss, das entweder selbst ein Rect ist, oder ein rect-Attribut haben muss.

  2. Über Surface.get_rect bekommt man ein Rect-Objekt, das die gleiche Größe wie Surface hat und im Punkt (0, 0) startet. Man kann auch Keyword-Argumente übergeben, die dann auf das Rect angewandt werden, zB. surf.get_rect(center=(100,100)).

Sprite

Sprites sind eine tolle Sache. Mit Sprites wird der Code wesentlich strukturierter und meist auch kürzer. Aber von vorn: Die Klasse pygame.sprite.Sprite repräsentiert ein Spielelement oder sonst irgendetwas, was man blitten möchte. Oder sagen wir besser Objekte von abgeleiteten Klassen, denn Sprite ist nur eine Grundklasse, von der man eigene Klassen ableitet, von denen man dann seine Objekte erzeugt. Der Sinn des ganzen ist zum einen, alles, was ein zu blittendes Objekt betrifft, in einer Klasse zu haben, und zum anderen, beliebig viele Objekte eines Typs erzeugen zu können. Dazu stellt Sprite einige Methoden zur Verfügung und auch zwei Attribute, die man definieren muss. Zum einen image, eine Surface die geblittet wird, und zum anderen rect, ein Rect, das angibt, wo image geblittet wird (eigentlich wird nur rect.topleft fürs blitten benutzt, aber ein Rect kann man, wie schon beschrieben, wesentlich komfortabler verschieben und man kann auch sehr leicht prüfen, ob ein Punkt innerhalb oder außerhalb eines Rects liegt, aber dazu später mehr. Zudem ist eine update-Methode zu definieren (oder viel mehr zu überschreiben), die jeden Frame einmal aufgerufen wird und wo dann die eigentliche Action passiert. Weitere Methoden beziehen sich auf Groups, daher erstmal zu Groups im Allgemeinen.

Group

Die Klasse pygame.sprite.Group dient der Verwaltung von (vielen) Sprites. Man kann Sprites beliebig hinzufügen (Group.add), entfernen (Group.remove, oder direkt alle mit Group.empty), mit Group.has testen, ob ein Sprite in der Gruppe enthalten ist und über Group.sprites eine Liste aller enthaltenen Sprites bekommen. Die wichtigsten Methoden sind aber Group.update, mit der die update-Methoden aller Sprites auf einmal aufgerufen werden, und Group.blit, mit der alle Sprites auf eine angegebene Surface geblittet werden (dh. die images werden geblittet, wobei die Position aus rect entnommen wird). Es gibt auch noch einige Unterklassen von Group, die weitere Möglichkeiten bieten, doch die Grundklasse reicht fürs erste aus.

Nun noch ein Nachtrag zur Sprite-Klasse. Diese hat noch folgende, Gruppen betreffende Methoden: add und remove um ein Sprite-Objekt zu einer oder mehreren Gruppen hinzuzufügen oder aus ihnen zu entfernen, kill um es aus allen Gruppen zu entfernen, alive um zu prüfen, ob es in mindestens einer Gruppe enthalten ist, und schließlich groups um eine Liste aller Gruppen zu erhalten, in denen es eingetragen ist.

Und Action!

Wenden wir das nun an, und erstellen uns eine Klasse Player, die von Sprite erbt. Als Parameter übergeben wir die Position, auf die der Player zentriert werden soll.

1
2
3
4
5
6
class Player(pygame.sprite.Sprite):
    def __init__(self, pos):

            pygame.sprite.Sprite.__init__(self)
            self.image  = load_image("player.png", -1)
            self.rect = self.image.get_rect(center=pos)

Statt player_img erzeugen wir nun ein Player-Objekt und eine Gruppe, in die wir alle Sprites packen (zur Zeit nur Player).

1
2
3
all_sprites = pygame.sprite.Group()
player= Player((50,50))
all_sprites.add(player)

Und zuletzt ersetzen wir noch das alte, manuelle Blitten von player_img durch

1
all_sprites.draw(screen)

Und schon erhalten wir beim Ausführen genau das gleiche Ergebnis wie vorher, doch durch die Verwendung von Sprite und Group ist es sehr leich, unsere Spielerfigur zu bewegen und auch mehrere Player zu erzeugen.

Ersteres machen wir ganz einfach über die update-Methode, die wir bis jetzt noch nicht brauchten. Ein einfaches verschieben des Rects sorgt dafür, dass der Player sich bewegt. Geschwindigkeit und Richtung lassen sich mit zwei Werten festlegen, die angeben, wie viele Pixel das Rect pro Frame in X- und Y-Richtung verschoben wird. Um möglichst flexibel zu bleiben, übergeben wir diese Angabe auch direkt als Parameter. Jetzt noch eine kleine If-Konstruktion, um zu verhindern, dass der Player aus dem sichtbaren Bereich verschwindet und wir haben unseren Player animiert.

Weil es etwas praktischer und auch üblicher ist, habe ich die load_image-Funktion um das image-Rect als Rückgabewert ergänzt und die entsprechende Stelle unten entsprechend angepasst.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def load_image(name, colorkey=None):
       # ...
        return image, image.get_rect()


    class Player(pygame.sprite.Sprite):
        def __init__(self, pos, move):
            pygame.sprite.Sprite.__init__(self)
            self.image, self.rect  = load_image("player.png", -1)
            self.rect.center=pos
            self.move = move

        def update(self):
            x, y = self.rect.center
            move_x, move_y = self.move
            new_x, new_y = x+move_x, y+move_y
            if not 0 <= new_x <= 500:
                    move_x *= -1
            if not 0 <= new_y <= 300:
                    move_y *= -1
            self.rect.center = x+move_x, y+move_y
            self.move = move_x, move_y

Mehrere Player-Objekte zu erzeugen und vor allem zu Verwalten ist dank der Gruppe kein Problem. Wir müssen weitere Player einfach in die bereits existierende Gruppe eintragen.

1
2
3
4
5
6
all_sprites = pygame.sprite.Group()
player1= Player((50, 50), (5,10))
player2= Player((50, 50), (10,5))
player3= Player((250, 0), (0,1))
player4= Player((0, 150), (20,0))
all_sprites.add(player1, player2, player3, player4)

Und schon haben wir ein Fenster mit vier sich hin und her bewegenden Playern. Doch das ist alles noch etwas suboptimal.

  • Da man Player.move sowieso immer auseinandernimmt, kann man die Werte auch direkt einzeln speichern.
  • Mit ein paar weiteren pygame-Methoden, kann man die update-Methode wesentlich aufpolieren. Zum einen sollte man die Werte für Fensterhöhe und Breite nicht fix hinschreiben, sondern dynamisch ermitteln (damit man später die Fenstergröße leichter ändern kann), zum anderen kann man die Tatsache ausnutzen, dass es einmal pygame.Rect.move und einmal pygame.Rect.move_ip gibt. Die erste Methode gibt ein neues Rect-Objekt zurück und die zweite ändert das Rect in place. Man kann also self.rect(...) aufrufen und den Rückgabewert nutzen, ohne das self.rect geändert wird. Und schließlich ist es auch eleganter, das Rect der Fenstersurface zur Kollisionsüberprüfung zu nutzen, genauer gesagt, dessen contains-Methode, die prüft, ob ein als Argument übergebenes Rect-Objekt innerhalb der eigenen Ränder liegt.
  • Das Erzeugen der Player-Objekte schreit geradezu danach, in eine Schleife gepackt zu werden.

Natürlich kann man da immer noch einiges verbessern, da werden wir gegen Ende vielleicht auch noch drauf kommen, aber so ist es fürs erste schon ganz gut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!python
# coding: utf8

import pygame
from pygame.locals import *


def load_image(name, colorkey=None):
    try:
        image = pygame.image.load(name)
    except pygame.error, message:
        print 'Cannot load image:', 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()


class Player(pygame.sprite.Sprite):
    def __init__(self, pos, move):
        pygame.sprite.Sprite.__init__(self)
        self.image, self.rect  = load_image("player.png", -1)
        self.rect.center=pos
        self.x, self.y = move
        self.screenrect = pygame.display.get_surface().get_rect()

    def update(self):
        if not self.screenrect.contains(self.rect.move(self.x, 0)):
                self.x *= -1
        if not self.screenrect.contains(self.rect.move(0, self.y)):
                self.y *= -1
        self.rect.move_ip(self.x, self.y)



def main():
    pygame.init()
    screen = pygame.display.set_mode((500,300))
    pygame.display.set_caption("Pygame ist toll")
    clock = pygame.time.Clock()

    all_sprites = pygame.sprite.Group()
    for args in [((50, 50), (5,10)),
                 ((50, 50), (10,5)),
                 ((250, 25), (0,1)),
                 ((25, 150), (20,0))]:
            all_sprites.add(Player(*args))

    while True:
        # maximal 40 fps
        clock.tick(40)
        # events bearbeiten
        for event in pygame.event.get():
            if event.type == QUIT:
                return
            elif event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    return

        # den Bildschirm mit einer Hintergrundfarbe füllen und so
        # gleichzeitig das alte Bild löschen
        screen.fill((120,30,66))

        ## Über die Gruppe alle Sprites updaten und dann blitten
        all_sprites.update()
        all_sprites.draw(screen)

        # alles aufs Fenster flippen
        pygame.display.flip()

if __name__ == '__main__': main()

Eventverarbeitung

Nun wollen wir unsere Spielfigur so umprogrammieren, dass der Spieler sie selber steuern kann. Dazu müssen wir uns die Events etwas genauer ansehen. Um weiterhin klar zwischen den verschiedenen Aufgabenbereichen trennen zu können, kopieren wir die Events im mainloop in eine Liste, die wir dann auch über die update-Methode an all unsere Spielobjekt-Klassen übergeben. So müssen nicht sämtliche Events für alle Objekte bzw. Klassen im mainloop ausgewertet werden. Natürlich erzeugen wir jetzt auch nur ein Player-Objekt (alles andere wäre recht sinnlos).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ...
    player = Player((50, 50))
    all_sprites.add(player)

     while True:
        # maximal 40 fps
        clock.tick(40)
        # events bearbeiten
        events = pygame.event.get()
        for event in events:
            if event.type == QUIT:
                return
            elif event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    return

        # den Bildschirm mit einer Hintergrundfarbe füllen und so
        # gleichzeitig das alte Bild löschen
        screen.fill((120,30,66))

        ## Über die Gruppe alle Sprites updaten und dann blitten
        all_sprites.update(events)
        all_sprites.draw(screen)
    #...

Jetzt zur update-Methode der Player-Klasse. Das Ziel ist, die Spielfigur mittels der Pfeiltasten steuern zu können. Also ersetzen wir die automatische Bewegung durch eine eventabhängige. Die Events, die wir abfangen müssen, sind alle vom Typ KEYDOWN (genau wie im mainloop). Die Pfeiltasten haben folgende key-Parameter:

K_LEFT
K_UP
K_RIGHT
K_DOWN

Pro Tastendruck soll die Spielfigur um 50 Pixel in eine Richtung bewegt werden. So kommen wir zu den x- und y-Werten. Die update-Methode sieht dann so aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# in Player.__init__ wurde self.step_size mit 50 initiallisiert
    def update(self, events):
        x = y = 0
        for event in events:
                if event.type == KEYDOWN:
                        if event.key == K_LEFT:
                                x = -1 * self.step_size
                        elif event.key == K_RIGHT:
                                x = 1 * self.step_size
                        elif event.key == K_UP:
                                y = -1 * self.step_size
                        elif event.key == K_DOWN:
                                y = 1 * self.step_size
        if x and not self.screenrect.contains(self.rect.move(x, 0)):
                x = 0
        if y and not self.screenrect.contains(self.rect.move(0, y)):
                y = 0
        self.rect.move_ip(x, y)

So, das ist aber etwas mühsam, da die Spielfigur sich nur pro Tastendruck bewegt. Angenehmer wäre es, wenn sie sich solange bewegt, wie eine Taste gedrückt wird. Um das umzusetzen, gibt es zwei Möglichkeiten:

  1. Indem man die Werte speichert und bei KEYUP-Events auf 0 zurücksetzt. step_size muss dann angepasst werden:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Player(pygame.sprite.Sprite):
    def __init__(self, pos):
        pygame.sprite.Sprite.__init__(self)
        self.image, self.rect  = load_image("player.png", -1)
        self.rect.center=pos
        self.step_size = 10
        self.x = self.y = 0
        self.screenrect = pygame.display.get_surface().get_rect()

    def update(self, events):
        for event in events:
                if event.type == KEYDOWN:
                        if event.key == K_LEFT:
                                self.x += -1 * self.step_size
                        elif event.key == K_RIGHT:
                                self.x += 1 * self.step_size
                        elif event.key == K_UP:
                                self.y += -1 * self.step_size
                        elif event.key == K_DOWN:
                                self.y += 1 * self.step_size
                elif event.type == KEYUP:
                        if event.key in (K_LEFT, K_RIGHT):
                                self.x = 0
                        elif event.key in (K_UP, K_DOWN):
                                self.y = 0
        if self.x and not self.screenrect.contains(self.rect.move(self.x, 0)):
                self.x = 0
        if self.y and not self.screenrect.contains(self.rect.move(0, self.y)):
                self.y = 0
        self.rect.move_ip(self.x, self.y)
  1. Indem man über pygame prüft, ob die entsprechenden Tasten gerückt sind. Vorerst reicht aber die 1. Möglichkeit aus. Wer sich umbedingt mit pygame.key beschäfftigen möchte, kann dies an dieser Stelle gerne tun und das Problem auch auf diese Weise lösen. -> Key-Doku

Kollisionserkennung

Als nächstes brauchen wir Dinge, mit denen unsere Spielfigur interagieren kann. Nehmen wir kirsche (Kirschen) zum Einsammeln und bombe (Bomben) zum Ausweichen. Für die Klassen an sich, brauchen wir nichts Neues, daher hier einfach mal der Code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Cherry(pygame.sprite.Sprite):
    def __init__(self, pos):
        pygame.sprite.Sprite.__init__(self)
        self.image, self.rect  = load_image("kirsche.png", -1)
        self.rect.center=pos

class Bomb(pygame.sprite.Sprite):
    def __init__(self, pos):
        pygame.sprite.Sprite.__init__(self)
        self.image, self.rect  = load_image("bombe.png", -1)
        self.rect.center=pos

Hier können wir zur mehrfachen Positionierung wieder eine Schleife verwenden:

1
2
3
4
for pos in ((172, 133),(71, 259),(210, 207),(338, 48),(427, 196),(317, 278),(452, 23)):
    all_sprites.add(Cherry(pos))
for pos in ((61, 124),(124, 202),(315, 194),(266, 102),(152, 31),(461, 86)):
    all_sprites.add(Bomb(pos))

Für alle, die sich nun fragen, wie ich zu diesen Koordinaten komme: Durch einen Zweizeiler kann man sich die Position eines Mausklicks ausgeben lassen und kann sich so sehr leicht eine Liste mit passenden Punkten erstellen:

1
2
3
# in einem beliebigen event-loop (for event in events:)
    if event.type == MOUSEBUTTONDOWN:
        print event.pos

Das sieht schon ganz gut aus, doch bis jetzt war nichts Neues dabei. Wir wollen schließlich, das etwas passiert, wenn man mit dem Player die anderen Objekte berührt. Und dies ist wieder ein Punkt, an dem sich die Nutzung von Group, Sprite und Rect auszahlt. Doch wir benötigen noch 2 Gruppen, um klar zwischen Kirschen und Bomben unterscheiden zu können. Eine andere, aber wie ich finde nicht so elegante Möglichkeit wäre, mit all_sprites und isinstance zu arbeiten. Hier das angepasste Codestück:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
all_sprites = pygame.sprite.Group()
cherries = pygame.sprite.Group()
bombs = pygame.sprite.Group()
player = Player((50, 50))
all_sprites.add(player)

for pos in ((172, 133),(71, 259),(210, 207),(338, 48),(427, 196),(317, 278),(452, 23)):
    c = Cherry(pos)
    all_sprites.add(c)
    cherries.add(c)
for pos in ((61, 124),(124, 202),(315, 194),(266, 102),(152, 31),(461, 86)):
    b = Bomb(pos)
    all_sprites.add(b)
    bombs.add(b)

Jetzt können wir ganz einfach mit pygame.sprite.spritecollide prüfen, ob sich das Rect unseres Player-Objekts mit dem eines Objekts einer Gruppe (cherries oder bombs) überschneidet. Das dritte Argument, das die Funktion annimmt, ist ein boolscher Wert, der angibt, ob überschneidende Objekte aus der Gruppe entfernt werden sollen (es wird die kill-Methode aufgerufen). In unserem Fall sollen die Kirschen entfernt werden und eine Zählvariable im Player hochgezählt werden. Bei Kollision der Player auf die Startposition zurückgesetzt werden und eine Möhre abgezoben bekommen. Das ganze sieht dann so aus:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!python
# coding: utf8

import pygame
from pygame.locals import *


def load_image(name, colorkey=None):
    try:
        image = pygame.image.load(name)
    except pygame.error, message:
        print 'Cannot load image:', 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()


class Player(pygame.sprite.Sprite):
    def __init__(self, pos):
        pygame.sprite.Sprite.__init__(self)
        self.image, self.rect  = load_image("player.png", -1)
        self.rect.center = pos
        self.start_pos = pos
        self.step_size = 10
        self.x = self.y = 0
        self.cherries = 0
        self.screenrect = pygame.display.get_surface().get_rect()

    def update(self, events):

        for event in events:
                if event.type == KEYDOWN:
                        if event.key == K_LEFT:
                                self.x += -1 * self.step_size
                        elif event.key == K_RIGHT:
                                self.x += 1 * self.step_size
                        elif event.key == K_UP:
                                self.y += -1 * self.step_size
                        elif event.key == K_DOWN:
                                self.y += 1 * self.step_size
                elif event.type == KEYUP:
                        if event.key in (K_LEFT, K_RIGHT):
                                self.x = 0
                        elif event.key in (K_UP, K_DOWN):
                                self.y = 0
        if self.x and not self.screenrect.contains(self.rect.move(self.x, 0)):
                self.x = 0
        if self.y and not self.screenrect.contains(self.rect.move(0, self.y)):
                self.y = 0
        self.rect.move_ip(self.x, self.y)

    def reset_position(self):
            self.rect.center = self.start_pos

class Cherry(pygame.sprite.Sprite):
    def __init__(self, pos):
        pygame.sprite.Sprite.__init__(self)
        self.image, self.rect  = load_image("kirsche.png", -1)
        self.rect.center=pos

class Bomb(pygame.sprite.Sprite):
    def __init__(self, pos):
        pygame.sprite.Sprite.__init__(self)
        self.image, self.rect  = load_image("bombe.png", -1)
        self.rect.center=pos


def main():
    pygame.init()
    screen = pygame.display.set_mode((500,300))
    pygame.display.set_caption("Pygame ist toll")
    clock = pygame.time.Clock()

    all_sprites = pygame.sprite.Group()
    cherries = pygame.sprite.Group()
    bombs = pygame.sprite.Group()
    player = Player((50, 50))
    all_sprites.add(player)

    for pos in ((172, 133),(71, 259),(210, 207),(338, 48),(427, 196),(317, 278),(452, 23)):
        c = Cherry(pos)
        all_sprites.add(c)
        cherries.add(c)
    for pos in ((61, 124),(124, 202),(315, 194),(266, 102),(152, 31),(461, 86)):
        b = Bomb(pos)
        all_sprites.add(b)
        bombs.add(b)

    while True:
        # maximal 40 fps
        clock.tick(40)
        # events bearbeiten
        events = pygame.event.get()
        for event in events:
        # um möglichst einfach Positionen für die Spielobjekte zu sammeln:
            if event.type == MOUSEBUTTONDOWN:
                print event.pos
            if event.type == QUIT:
                return
            elif event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    return

        for cherry in pygame.sprite.spritecollide(player, cherries, True):
                player.cherries += 1
        for bomb in pygame.sprite.spritecollide(player, bombs, False):
                player.cherries -= 1
                player.reset_position()

        # den Bildschirm mit einer Hintergrundfarbe füllen und so
        # gleichzeitig das alte Bild löschen
        screen.fill((120,30,66))

        ## Über die Gruppe alle Sprites updaten und dann blitten
        all_sprites.update(events)
        all_sprites.draw(screen)

        # alles aufs Fenster flippen
        pygame.display.flip()

if __name__ == '__main__': main()

Text darstellen

Zuletzt wollen wir noch den Inhalt der Zählvariable des Players darstellen. Dazu benötigen wir das Modul pygame.font. Um Text erzeugen zu können, braucht man ein Font-Objekt. Dazu gibt es die Klasse Font, mit der man eigene Font-Dateien erzeugen kann ("*.ttf"), oder SysFont, mit der man Systemschriften laden kann. Nehmen wir letzteres:

1
font = pygame.font.SysFont("Times New Roman",25)

Dieses Font-Objekt kann man jetzt nutzten, um Image-Objekte aus Strings zu erzeugen, und zwar mit der render-Methode:

1
text_img = self.font.render(text, True, (0,0,0))

Die Parameter sind folgende: der darzustellende Text, Antialiasing (Kantenglättung) verwenden, Vordergrundfarbe, Hintergrundfarbe (optional).

Hier ist die Endversion des Spielchens: pygame ist toll!

Wie gehts weiter?

Jetzt bist Du dran! Hier folgen einige Aufgaben und Anregungen, wie Du das Spiel noch verbessern kannst. Nicht alles, was Du dazu brauchst, findest Du in diesem Turorial, daher such dir immer eine Aufgabe heraus und versuche eine nach der anderen einzubauen. Die Aufgaben sind nicht umbedingt nach dem Schwierigkeitslevel geordnet.

Wenn Du nicht weiter weißt, schau in die Doku

Aufgabe

Starte das Spiel von vorne, wenn das alte gewonnen oder verloren wurde, nachdem Du das Ergebnis angezeigt hast.

Aufgabe

Miss die vergangene Zeit seit Spielbegin und stelle sie neben der Kirschenanzahl dar.

Aufgabe

Baue eine Pausefunktion ein, die über eine Taste aufgerufen werden kann. Bedenke dabei, dass während der Pause die Zeit nicht weiterlaufen darf.

Aufgabe

Baue Sound ein.

Aufgabe

Schreibe das Programm so um, dass es zu Beginn nur eine Kirsche gibt. Wenn diese aufgesammelt wird, soll an anderer Stelle eine neue erscheinen und gleichzeitig eine Bombe am Bildrand auftauchen, die sich in steigendem Tempo (jede neue Bombe bewegt sich etw. schneller) in einer Richtung bewegt.

Hinweis: Um das Spiel nicht zu schnell unspielbar werden zu lassen, solltest Du alle Spielelemente etwas verkleinern und das Fenster eventuell auch noch etwas vergrößern

tags: Programmieren , PyGame & Python erstellt am 7.8.2008 12:29, zuletzt gendert am 2.1.2009 15:53