Python unter Linux: PyGame

PyGame ist eine Gruppe von Modulen, die einen Programmierer bei der Erstellung von Spielen unterstützt. Diese Module beinhalten Zugriff auf genau ein Grafikfenster. PyGame unterstützt den Entwickler mit Grafikprimitiven, einfachem Soundzugriff sowie Sprites und vielem mehr. Typische GUI-Elemente gibt es in diesen Modulen jedoch nicht. PyGame beinhaltet eine Grafikbibliothek, zu der Wikibooks auch ein Buch hat, nämlich SDL. Einige der hier angeführten Beispiele sind diesem Buch entlehnt. Hintergrundinformationen zu den Modulen findet sich auf der PyGame-Webseite.

Ein Grafikfenster

Bearbeiten
#!/usr/bin/python
import pygame
import sys

def init():
    WINWIDTH = 640
    WINHEIGHT = 480
    pygame.init()
    screen = pygame.display.set_mode((WINWIDTH, WINHEIGHT))
    screen.fill((200, 200, 200))
    pygame.display.update()

def event_loop():
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                sys.exit()

if __name__ == '__main__':
    init()
    event_loop()
 
Bild der Anwendung

PyGame muss initialisiert werden, das übernimmt die Funktion pygame.init(), welche vor allen anderen PyGame-Funktionen aufgerufen werden muss. Ein neues Fenster erhalten wir mit der Methode pygame.display.set_mode(), der wir neben der Fenstergröße als Tupel auch noch weitere Flags mitgeben könnten. Weitere Angaben wären zum Beispiel, dass wir den Vollbildmodus (pygame.FULLSCREEN) oder OpenGL-Unterstützung (pygame.OPENGL) wünschen. pygame.display.set_mode() übergibt eine so genannte Surface, eine Struktur, die das Grafikfenster repräsentiert.

Den Bildschirm füllen wir mit einer Farbe screen.fill((200, 200, 200)) (grau), die wir als Tupel übergeben. Damit diese Färbung wirksam wird, frischt pygame.display.update() den gesamten Bildschirm auf.

PyGame speichert alle Ereignisse wie Tastendrücke, Mausbewegungen und Joystick-Kommandos in einer Folge von Events. Mit pygame.event.get() holen wir uns das nächste anstehende Ereignis. Im Attribut event.type ist die Art von Ereignis gespeichert, die anliegt. In unserem einfachen Beispiel wird nur nach pygame.QUIT und pygame.KEYDOWN verzweigt, in diesen Fällen beendet sich das Programm. Die nachstehende Tabelle enthält einen Ausschnitt der möglichen Ereignisse, für eine vollständige Liste ziehen Sie bitte die Online-Dokumentation zu Rate.

Ereignis Bedeutung
QUIT Anwender wünscht das Programm zu beenden, beispielsweise durch Drücken des Schließen-Knopfes
KEYDOWN Taste wurde heruntergedrückt
KEYUP heruntergedrückte Taste wurde wieder losgelassen
MOUSEMOTION Die Maus wurde bewegt
MOUSEBUTTONDOWN Ein Knopf an der Maus wurde gedrückt
USEREVENT Ein frei definierbares Ereignis passierte. Genau genommen gibt es hier viele weitere Events, die frei definierbar sind, nämlich alle zwischen USEREVENT und NUMEVENTS-1.

Malprogramm

Bearbeiten

Um die Events einmal real zu benutzen, haben wir ein Malprogramm als Beispiel geschrieben. Mit den Tasten 0 bis 3 steuert man die Farbwahl, drückt man einen der Mausknöpfe im Fenster, so wird dort ein Klecks mit der aktuell gewählten Farbe gezeichnet. Die Taste Escape bricht das Programm ab.

#!/usr/bin/python
import pygame
import sys

class Malprogramm:
    def __init__(self, width, height):
        self._width = width
        self._height = height
        pygame.init()
        self._screen = pygame.display.set_mode((self._width, self._height))
        self._screen.fill((200, 200, 200))
        pygame.display.update()
        self._drawColor = (200, 0, 0)

    def malen(self, (x, y)):
        pygame.draw.circle(self._screen, self._drawColor, (x, y), 10)
        pygame.display.update((x-10, y-10, 20, 20))
        
    def event_loop(self):
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    sys.exit()
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        sys.exit()
                    elif event.key == pygame.K_0:
                        self._drawColor = (255, 255, 255)
                    elif event.key == pygame.K_1:
                        self._drawColor = (0, 0, 255)
                    elif event.key == pygame.K_2:
                        self._drawColor = (0, 255, 0)
                    elif event.key == pygame.K_3:
                        self._drawColor = (255, 0, 0)
                        
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    self.malen(event.pos)

if __name__ == '__main__':
    m = Malprogramm(640, 480)
    m.event_loop()
 
Bild der Anwendung

Die __init__()-Methode der Klasse verhält sich, wie die Funktion init() aus dem ersten Beispiel, bis auf dass sie eine Startfarbe belegt: self._drawColor = (200, 0, 0) (rot). Diese Farbe wird in der Methode event_loop() bei entsprechenden Tastendrücken verändert.

In der Methode malen() wird eine bestimmte angegeben Stelle mit einem ausgefüllten Kreis bemalt. pygame.draw.circle() benötigt dazu die Surface, die Farbe als Tupel, den Mittelpunkt als Tupel und den Radius. pygame.display.update() frischt den Bildschirm wieder auf, damit wir diesen Kreis sehen können. In diesem Fall benutzen wir eine Variante der Methode, da wir nicht den gesamten Bildschirm bemalt haben, sondern nur ein Rechteck. Dieses den Kreisfleck umgebene Rechteck übergeben wir der Methode pygame.display.update(), die dadurch wesentlich schneller arbeiten kann, als müsste sie den gesamten Bildschirm auffrischen.

Bei gedrückter Taste ist in event.key die Konstante der gedrückten Taste gespeichert. Einen unvollständigen Überblick über die Informationen, die Sie aus der event-Variablen herauslösen können in Abhängigkeit vom gewählten Ereignistyp gibt die folgende Tabelle:

Ereignistyp Event-Felder Bedeutung
KEYDOWN unicode Das Unicode-Zeichen dieses Tastendruckes
key K_0..K_9 - Zifferntaste
K_a..K_z Buchstabentaste und viele mehr
mod KMOD_LSHIFT - linke Schift-Taste,
KMOD_LCTRL linke STRG-Taste
KMOD_RALT recht ALT-Taste und viele mehr
KEYUP key, mod wie oben
MOUSEMOTION pos Absolute Position innerhalb des Fensters als Tupel
rel Veränderung zur letzten Position
buttons gedrückte Mausknöpfe
MOUSEBUTTONDOWN, MOUSEBUTTONUP pos, button wie oben

Animation

Bearbeiten

Zu folgendem Beispiel passt der Ausschnitt aus Heinrich Heines Gedicht: Ein Jüngling liebt ein Mädchen:

Ein Jüngling liebt ein Mädchen,
die hat einen andern erwählt;
der andre liebt eine andre,
und hat sich mit dieser vermählt.

Das Mädchen heiratet aus Ärger
den ersten besten Mann,
der ihr in den Weg gelaufen;
der Jüngling ist übel dran.

Wir versuchen einmal eine ähnliche Situation zu programmieren, wobei der Jüngling dem Mädchen hinterherläuft, diese wiederum ihrem Erwählten, der seinerseits einer anderen hinterherläuft. Diese wiederum versucht den Jüngling zu erhaschen. Technisch gesehen lassen wir schlicht einige Punkte sich aufeinander zu bewegen.

Das folgende Programm generiert ein Ereignis (USEREVENT) auf der Basis eines Timers. Der Timer löst das Ereignis nach 200 Millisekunden aus, in der Event-Loop wird entschieden, ob nach weiteren 0,2 Sekunden erneut das gleiche Ereignis erfolgen soll. Nach jedem Zeitintervall werden Linien zwschen Punkten, die sich bewegen, neu gezeichnet:

#!/usr/bin/python
import pygame
import math
import sys

class Animation:

    def __init__(self, width, height):
        self._width = width
        self._height = height
        pygame.init()
        self._screen = pygame.display.set_mode((self._width, self._height))
        self._screen.fill((200, 200, 200))
        pygame.display.update()
        self._punkte = [(0.0, 0.0), (width - 1.0, 0.0), (width - 1.0, height - 1.0), (0.0, height - 1.0)]
        pygame.time.set_timer(pygame.USEREVENT, 200)
        
    def malen(self):
        pygame.draw.line(self._screen, (0,0,0), self._punkte[0], self._punkte[1], 1)
        pygame.draw.line(self._screen, (0,0,0), self._punkte[1], self._punkte[2], 1)
        pygame.draw.line(self._screen, (0,0,0), self._punkte[2], self._punkte[3], 1)
        pygame.draw.line(self._screen, (0,0,0), self._punkte[3], self._punkte[0], 1)
        pygame.display.update()

    def berechnen(self):
        punkte_tmp = []
        anz_punkte = len(self._punkte)
        dx = 0.0 # vorbelegt, da wir den Wert zurueckgeben
        for i in xrange(4):
            x1, y1 = self._punkte[i]
            x2, y2 = self._punkte[(i+1) % anz_punkte]
            dx = x2 - x1
            dy = y2 - y1
            # Einheitsvektor
            laenge = math.sqrt(dx * dx + dy * dy)
            ex = dx / laenge
            ey = dy / laenge
            x_neu = x1 + 5.0 * ex
            y_neu = y1 + 5.0 * ey
            punkte_tmp.append((x_neu, y_neu))
        self._punkte = punkte_tmp
        return dx

    def event_loop(self):
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    sys.exit()
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        sys.exit()
                elif event.type == pygame.USEREVENT:
                    self.malen()
                    dist = self.berechnen()
                    if dist > 20.0:
                        pygame.time.set_timer(pygame.USEREVENT, 200)

if __name__ == '__main__':
    a = Animation(800, 800)
    a.event_loop()
 
Bild der Animation

self._punkte ist eine Liste, die Punkte als Tupel enthält. Diese Punkte werden in der Methode berechnen() neu berechnet und es werden Linien zwischen den Punkten neu gezeichnet. Die Punkte sollen sich dabei aufeinander zubewegen. Der Timer wird in __init__() mit dem Aufruf pygame.time.set_timer() gestartet. Die Argumente sind der Ereignistyp und die Zeitspanne in Millisekunden. Die Methode malen() sorgt dafür, daß zwischen allen vier Punkten Linien in schwarz gemalt werden. Tritt ein USEREVENT ein, so werden die Linien zwischen den Punkten gemalt, es werden neue Punkte berechnet und entschieden, ob weitere Punkte berechnet werden müssen. Gegebenenfalls wird der Timer erneut gestartet.

 Details

Die Berechnung erfolgt nach folgendem Schema:
Zuerst werden alle Punkte in den Ecken verteilt. Anschließend bewegt sich jeder Punkt ein Stück auf den anderen zu. Der Punkt self._punkte[0] bewegt sich in die Richtung des Punktes self._punkte[1] und so fort, der letzte bewegt sich wieder auf den ersten Punkt zu. Mathematisch bedeutet das,  zu berechnen, wobei   der normierte Richtungsvektor zwischen zwei benachbarten Punkten ist. und   den nächsten Zeitschritt bedeutet.

Bilder und Fonts

Bearbeiten

Das folgende Beispiel demonstriert, wie man Bilder in PyGame lädt und mit Hilfe von Fonts einen Text in ihnen aufbringt. Damit das Programm korrekt funktioniert, muss ein Bild mit dem Namen beispiel.png im aktuellen Verzeichnis liegen.

#!/usr/bin/python
import pygame
import sys

WINWIDTH = 640
WINHEIGHT = 480

def init(width, height):
    pygame.init()
    screen = pygame.display.set_mode((width, height))
    screen.fill((250, 250, 250))
    pygame.display.update()
    return screen

def load_pic(filename, width, height):
    surface = pygame.image.load(filename)
    picW, picH = surface.get_size()
    transform = False
    if picW > width:
        picH = 1.0 * width / picW * picH
        picW = width
        transform = True
    if picH > height:
        picW = 1.0 * height / picH * picW
        picH = height
        transform = True
    if transform:
        w = int(round(picW))
        h = int(round(picH))
        tmp = pygame.transform.scale(surface, (w, h))
        surface = tmp
    return surface 

def blit_pic(surface, pic, width, height):
    picW, picH = pic.get_size()
    w = (width - picW) / 2
    h = (height - picH) / 2
    surface.blit(pic, (w, h))
    font = pygame.font.SysFont('curier', 50)
    text = font.render("Hallo, Welt", True, (0, 0, 200))
    picW, picH = text.get_size()
    w = (width - picW) / 2
    h = (height - picH) / 2
    surface.blit(text, (w, h))
    pygame.display.update()

def event_loop():
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                sys.exit()

if __name__ == '__main__':
    screen = init(WINWIDTH, WINHEIGHT)
    pic = load_pic('beispiel.png', WINWIDTH, WINHEIGHT)
    blit_pic(screen, pic, WINWIDTH, WINHEIGHT)
    event_loop()
 
Bilder anzeigen

Neu an diesem Programm sind die beiden Funktionen load_pic() und blit_pic(). In load_pic() wird ein Bild geladen. Dieses Bild wird repräsentiert durch eine Surface, die sich manipulieren lässt, also beispielsweise drehen, bemalen und skalieren. Die Methode surface.get_size() liefert uns die Größe des Bildes. Das Bild wird so skaliert, dass es auf das Fenster passt.

blit_pic() dient dazu, das Bild auf das Grafikfenster zu zeichnen. Damit es mittig erscheint, wird hier mit Hilfe der Surface-Größe der Rand ausgerechnet.surface.blit(pic, (w, h)) übernimmt das eigentliche Blitten, eine Operation, die eine Surface auf einen anderen kopiert. Der Parameter (w, h) ist hierbei der Ursprung des Bildes.

Ebenfalls mittig soll eine Schrift auf das Bild gebracht werden. Hierzu laden wir mit pygame.font.SysFont() eine Schriftart "Curier" in 50 Punkte Größe. Mit font.render() wird dann eine neue Surface erzeugt, die einen konkreten Text unter Anwendung von Anti-Alias mit der gewählten Farbe repräsentiert. Auch diese wird auf den Bildschirm gezeichnet. Nach beiden Blit-Operationen wird das Grafikfenster aufgefrischt.

Das folgende Programm spielt WAV und OGG/Vorbis-Dateien, die auf der Kommandozeile übergeben werden ab:

#!/usr/bin/python
import pygame
import sys
import os.path

def hinweis():
    print "./sound soundfile"
    sys.exit()

if len(sys.argv) != 2:
    hinweis()

if not os.path.exists(sys.argv[1]):
    hinweis()

pygame.init()
pygame.mixer.music.set_endevent(pygame.USEREVENT)
pygame.mixer.music.load(sys.argv[1])
pygame.mixer.music.play()

while True:
    for event in pygame.event.get():
        if event.type == pygame.USEREVENT:
            sys.exit()

Die Funktion pygame.init() initialisiert auch den Mixer. pygame.mixer.music.set_endevent() legt ein Ereignis fest, welches eintrifft, sobald eine Sound-Datei zu ende gespielt ist. Die auf der Kommandozeile übergebene Sounddatei wird mit pygame.mixer.music.load() geladen, anschließend mit pygame.mixer.music.play() abgespielt.

Mit Hilfe einer zusätzlichen Funktion pygame.mixer.set_volume(lautstärke) könnten wir noch die Lautstärke einstellen. Dieser Wert liegt zwischen 0.0 und 1.0. Da das Abspielen einer Sounddatei das Programm überdauern kann, müssen wir hier eine Event-Loop einsetzen. Das Programm würde sonst beendet werden, bevor die Datei abgespielt würde. Am Ende bekommen wir das angeforderte Ereignis, das Programm kann sich dann zum richtigen Zeitpunkt beenden.

Zusammenfassung

Bearbeiten

Dieses Kapitel hat einen Überblick über Möglichkeiten von PyGame geboten. Wir können genau ein Grafikfenster öffnen, zeichnen, auf Ereignisse reagieren und selber Ereignisse erzeugen. Darüber hinaus haben wir gezeigt, wie man Bilder lädt und Texte zeichnet. PyGame kann mehr, als wir hier ausgeführt haben... Experimentieren Sie selbst!