Pygame-Tutorial

Aus Spieleprogrammierer-Wiki
Wechseln zu: Navigation, Suche

Dieses Tutorial richtet sich an Anfänger, die bereits die Grundlagen von Python kennen und in die Spieleprogrammierung einsteigen wollen. Es wird eine praktische Einführung in die Pygame-Bibliothek gegeben.

Inhaltsverzeichnis

Werbeanzeige

Einleitung

Was ist Pygame?

Pygame ist eine Sammlung von Python-Modulen, die es einem ermöglichen, relativ einfach und schnell Spiele zu programmieren. Pygame basiert auf der SDL-Bibliothek (Simple DirectMedia Layer). Pygame ist kostenlos zu haben (Lizenz LGPL) und läuft auf jedem Betriebssystem, für das es einen Python-Interpreter gibt, wenn man plattformunabhängig programmiert (was in Python aber nicht schwer ist).

Wo bekomme ich Pygame?

Die offizielle Pygame-Webseite findet sich unter http://www.pygame.org/. Dort gibt es auch immer die jeweils neuste Version, die Dokumentation, Tutorials und vieles mehr. Unter http://www.python.org/ gibt es die Python-Interpreter, und alle, die Python noch nicht kennen, finden dort alles, um es zu lernen. In diesem Tutorial wird Pygame Version 1.9.2 und Python Version 2.7.9 verwendet.

Pygame einrichten

Die Entwicklungsumgebung (IDE)

Man kann Python-Programme ganz einfach mit einem simplen Texteditor (wie Notepad unter Windows) schreiben. Viele spezielle Editoren bieten aber einiges mehr, was einem beim Programmieren hilft, wie etwa Syntax-Highlighting, Code-Vervollständigung und mehr. Als Beispiel wird hier die Einrichtung mit Eclipse und dem Pydev-Plugin beschrieben. Das Installieren gestaltet sich ziemlich einfach. Wir fügen das Pydev-Repository unter Help > Install New Software... > Add... hinzu: Der Name ist egal, die Location ist http://pydev.org/updates. Dann wählen wir das soeben hinzugefügte Repository im Dropdown-Menü oben aus, haken PyDev an und folgen den Anweisungen. Jetzt muss unter Window > Preferences > Pydev > Interpreter - Python noch die EXE-Datei des Python-Interpreters angegeben werden. Danach können wir in die Pydev-Perspektive schalten und loslegen.

Pygame integrieren

Dies geschieht am einfachsten über das Installationsprogramm. Dieses installiert alle benötigten Dateien (Python-Module, SDL-DLLs usw.) in den Unterordner site-packages der Python-Installation. Eclipse sollte das automatisch erkennen.

Testen

Zuerst erstellen wir ein Python-Projekt: File > New > Other > Pydev > Pydev Project. Jetzt müssen wir einen Namen für das Projekt eingeben, Create 'src' folder... auswählen und auf Finish klicken. Danach fragt Eclipse möglicherweise noch, ob wir die Pydev Perspective öffnen möchten. Dies beantworten wir mit Ja, und schon haben wir ein Python-Projekt angelegt.

Fügen wir ein wenig Code hinzu. Dazu machen wir einen Rechtsklick auf den src-Ordner in unserem Projekt links in der Übersicht und klicken auf New > Pydev Package. Dann geben wir einen Namen ein und klicken auf Finish. Pydev hat jetzt automatisch eine __init__.py Datei erstellt. Wer möchte, kann diese umbenennen, zum Beispiel in Teil_1.py. Die Datei öffnen wir jetzt durch einen Doppelklick und schreiben dieses einfache Skript zum Testen der Installation hinein:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. # Pygame-Modul importieren.
  4. import pygame
  5.  
  6. # Überprüfen, ob die optionalen Text- und Sound-Module geladen werden konnten.
  7. if not pygame.font: print('Fehler pygame.font Modul konnte nicht geladen werden!')
  8. if not pygame.mixer: print('Fehler pygame.mixer Modul konnte nicht geladen werden!')
  9.  
  10. def main():
  11.     # Initialisieren aller Pygame-Module und    
  12.     # Fenster erstellen (wir bekommen eine Surface, die den Bildschirm repräsentiert).
  13.     pygame.init()
  14.     screen = pygame.display.set_mode((800, 600))
  15.  
  16.     # Titel des Fensters setzen, Mauszeiger nicht verstecken und Tastendrücke wiederholt senden.
  17.     pygame.display.set_caption("Pygame-Tutorial: Grundlagen")
  18.     pygame.mouse.set_visible(1)
  19.     pygame.key.set_repeat(1, 30)
  20.  
  21.     # Clock-Objekt erstellen, das wir benötigen, um die Framerate zu begrenzen.
  22.     clock = pygame.time.Clock()
  23.  
  24.     # Die Schleife, und damit unser Spiel, läuft solange running == True.
  25.     running = True
  26.     while running:
  27.         # Framerate auf 30 Frames pro Sekunde beschränken.
  28.         # Pygame wartet, falls das Programm schneller läuft.
  29.         clock.tick(30)
  30.  
  31.         # screen-Surface mit Schwarz (RGB = 0, 0, 0) füllen.
  32.         screen.fill((0, 0, 0))
  33.  
  34.         # Alle aufgelaufenen Events holen und abarbeiten.
  35.         for event in pygame.event.get():
  36.             # Spiel beenden, wenn wir ein QUIT-Event finden.
  37.             if event.type == pygame.QUIT:
  38.                 running = False
  39.  
  40.             # Wir interessieren uns auch für "Taste gedrückt"-Events.
  41.             if event.type == pygame.KEYDOWN:
  42.                 # Wenn Escape gedrückt wird, posten wir ein QUIT-Event in Pygames Event-Warteschlange.
  43.                 if event.key == pygame.K_ESCAPE:
  44.                     pygame.event.post(pygame.event.Event(pygame.QUIT))
  45.  
  46.         # Inhalt von screen anzeigen.
  47.         pygame.display.flip()
  48.  
  49.  
  50. # Überprüfen, ob dieses Modul als Programm läuft und nicht in einem anderen Modul importiert wird.
  51. if __name__ == '__main__':
  52.     # Unsere Main-Funktion aufrufen.
  53.     main()

Wer jetzt bemerkt, dass er sich noch nicht mit Python auskennt, der sollte diese Sprache zuerst einmal erlernen. Ansonsten sollte der größte Teil keine Probleme bereiten. Starten können wir mit Run > Run > Python > Run > OK, Beenden über Escape oder das X. Es sollte ein Fenster mit schwarzem Hintergrund zu sehen sein – nicht mehr, aber auch nicht weniger.

Ein paar Grundlagen

Schauen wir uns das erste Beispiel mal etwas genauer an ...

Das erste Beispiel

Zuerst importieren wir das benötigte Pygame-Modul. Wenig Aufregendes:

  1. import pygame

Über diese Zeile importieren wir alles, was wir für's Programmieren mit Pygame benötigen. Zusätzlich können wir noch testen, ob die Module für die Sound- und die Font-Bibliothek korrekt geladen wurden.

Pygame initialisieren

Als Nächstes initialisieren wir Pygame und erstellen eine Surface (was das ist, besprechen wir später noch), die wir als Bildschirm verwenden, mit einer festen Auflösung von 800x600 Pixeln.

  1.     pygame.init()
  2.     screen = pygame.display.set_mode((800, 600))

Der set_mode Funktion können wir noch zusätzliche Parameter übergeben, aber ohne sie sucht SDL die für den PC besten Einstellungen heraus, was in den meisten Fällen der bessere Weg ist. Wer ein bestimmtes Format benötigt, schaut hier in der Dokumentation nach. Das Festlegen des Fenstertitels und das Anzeigen der Maus ist selbsterklärend. Als Letztes schalten wir noch die Tastenwiederholung ein. Dadurch weisen wir Pygame an, wiederholt eine "Taste gedrückt"-Nachricht zu senden, obwohl die Taste noch nicht losgelassen wurde.

  1.     pygame.display.set_caption("Pygame-Tutorial: Grundlagen")
  2.     pygame.mouse.set_visible(1)
  3.     pygame.key.set_repeat(1, 30)

Die Nachrichtenschleife

Damit ist Pygame grundlegend initialisiert. Jetzt müssen wir nur noch dafür sorgen, dass das Fenster auch länger als ein Frame angezeigt wird. Dafür verwenden wir eine Nachrichtenschleife. Um Probleme mit der Geschwindigkeit der verschiedenen PCs zu vermeiden, legen wir fest, dass wir maximal 30 Frames pro Sekunde berechnen wollen:

  1.     clock = pygame.time.Clock()
  2.  
  3.     # Die Schleife, und damit unser Spiel, läuft solange running == True.
  4.     running = 1
  5.     while running:
  6.         # Framerate auf 30 Frames pro Sekunde beschränken.
  7.         # Pygame wartet, falls das Programm schneller läuft.
  8.         clock.tick(30)
  9.  
  10.         # screen-Surface mit Schwarz (RGB = 0, 0, 0) füllen.
  11.         screen.fill((0, 0, 0))

Zuerst erstellen wir ein time.Clock-Objekt, welches sich um die Framerate-Begrenzung kümmern soll. Um die Schleife bequem wieder verlassen zu können, verwenden wir die running-Variable. Solange sie 1 ist, bleiben wir in der Schleife. In der Schleife lassen wir das time.Clock-Objekt erstmal berechnen, wie lange wir warten müssen, um maximal 30 Frames pro Sekunde zu erhalten. Danach überschreiben wir den gesamten Bildschirm mit der Farbe Schwarz.

  1.         for event in pygame.event.get():
  2.             # Spiel beenden, wenn wir ein QUIT-Event finden.
  3.             if event.type == pygame.QUIT:
  4.                 running = False

Hier haben wir die erste Abbruchbedingung. Wir holen uns per pygame.event.get() sämtliche Events, die Pygame empfangen/generiert hat. Finden wir eines vom Typ QUIT, setzen wir die running-Variable auf 0, sodass wir die Schleife verlassen können. Als Nächstes interessiert uns, welche Tasten der Benutzer gedrückt hat. Diese Events werden durch den Typ KEYDOWN repräsentiert.

  1.             if event.type == pygame.KEYDOWN:
  2.                 # Wenn Escape gedrückt wird, posten wir ein QUIT-Event in Pygames Event-Warteschlange.
  3.                 if event.key == pygame.K_ESCAPE:
  4.                     pygame.event.post(pygame.event.Event(pygame.QUIT))

Wir testen hier nur auf die Escape-Taste. Wurde sie gedrückt, generieren wir ein Event vom Typ QUIT, was dafür sorgt, dass wir die Schleife verlassen (wir erinnern uns: auf dieses Event reagieren wir ja etwas weiter oben). Nachdem wir alle Events durchgesehen haben, können wir endlich den Bildschirminhalt anzeigen:

  1.         pygame.display.flip()

Intern arbeitet Pygame mit einem Puffer für den Bildschrim, der erst mit diesem Befehl angezeigt wird (Double Buffering). Würde man direkt auf den Bildschirm rendern, würde man den Bildaufbau sehen können. Damit sind wir auch fast schon am Ende des Beispiels. Das gerade Besprochene haben wir in eine Funktion main gesteckt, die wir jetzt einfach aufrufen, falls diese Datei nicht als Modul importiert wird:

  1. if __name__ == '__main__':
  2.     # Unsere Main-Funktion aufrufen.
  3.     main()

Damit sind wir mit den Grundlagen fertig.

Programmieren einer Tilemap

Jetzt werden wir, am praktischen Beispiel einer Tilemap für ein 2D-Spiel, die Pygame-Funktionen für das Laden und Anzeigen von Bildern kennen lernen.

Beschreibung der Tilemap

Was ist das?

Screenshot der Tilemap, die in diesem Tutorial programmiert wird.

Bevor wir uns an das Implementieren der Tilemap machen, klären wir noch, was genau das ist, wozu man sie benutzen kann und wie wir sie hier in dem Tutorial bauen werden. Tilemaps werden häufig in 2D-Spielen verwendet. Sie sind eine einfache und schnelle Lösung, eine Landschaft oder ähnliches in einem Spiel darzustellen. Die Landschaft ist dabei aus zumeist gleich großen, quadratischen Kacheln (Tiles) zusammengesetzt, daher der Name. Mehrere, verschiedene Tile-Grafiken fasst man zu sogenannten Tilesets zusammen, damit kann man zum Beispiel während des Spiels einfach und schnell den Grafikstil ändern (etwa eine normale Gras-Grafik in eine Gras-mit-Schnee-Grafik) ändern. Um Platz zu sparen und um die Übersicht zu erhöhen, werden Tiles aus einem Set oftmals in einer einzigen Bilddatei gespeichert. Eine Tilemap speichert nun einfach eine Liste von Informationen, an welcher Position welches Tile liegt (zum Beispiel als 2D-Array, bei dem in jedem Eintrag abgespeichert ist, welcher Tile-Typ dort zu finden ist).

Wie gehen wir vor?

Unsere Tilemap wird folgendermaßen arbeiten: Wir verwenden eine Klasse names TileType, welche exakt einen Tile-Typ repräsentiert. Sie speichert die Position dieses Typs auf der Tileset-Grafik und seinen Namen. Hier ist außerdem der passende Ort, um weitere Informationen, wie etwa Begehbarkeit, "Tödlichkeit", Ereignisse usw. zu speichern. Die Klasse Tileset speichert in einem Dictionary (assoziatives Array) alle Tile-Typen, sowie die einheitliche Größe der Tiles. Und dazu noch die Grafik, auf der sämtliche Tiles vorhanden sind. Außerdem ermöglichen wir es, dass man diese Grafik einfach gegen eine andere austauschen kann. Ein Tile-Typ muss per Hand hinzugefügt werden – eine gute Gelegenheit um den Umgang mit den Datei-Funktionen von Python zu lernen ... Zu guter Letzt haben wir noch eine Klasse namens Tilemap, die sich hauptsächlich um das Anzeigen der Tiles kümmert. Hier wird ein Tileset gespeichert, und natürlich eine Liste von Tiles. Die Liste implementieren wir als 2D-Array, in dem pro Eintrag die ID eines Tile-Types gespeichert ist.

Bilder in Pygame

Los geht's

Bevor wir uns an das Programmieren der Tilemap machen, schauen wir, wie Pygame uns mit den Bildern helfen kann. Mittels Pygame können wir sehr schnell und sehr einfach ein Bild laden:

image = pygame.image.load("tolles_bild.bmp")

Unterstützt werden dabei folgende Formate (Auszug aus der Dokumentation):

Da sollte jeder "sein" Format finden. Mit dem fertig geladenen Bild könnten wir jetzt bereits arbeiten, aber für den Einsatz in einem Spiel/Programm sollte das Bild noch optimiert werden. Vorher schauen wir uns aber noch an, wie das geladene Bild in Pygame repräsentiert wird.

Surfaces

Pygame speichert alles, was irgendwie angezeigt werden könnte, in so genannten Surfaces. Auch Bilder und der Bildschirm werden als Surface repräsentiert. Für jetzt ist erstmal wichtig, dass eine Surface oder Teile einer Surface auf andere Surfaces bzw. den Bildschirm kopiert werden können. Details dazu kommen später.

Formatkonvertierung

Beim Erstellen der Bildschirm-Surface können wir, falls gewünscht, die Farbtiefe des Fensters angeben, bzw. wird dies von Pygame automatisch erledigt. Wenn wir jetzt ein Bild anzeigen lassen wollen, das nicht in diesem Format gespeichert ist, muss Pygame das Bild bei jedem Anzeige-Vorgang konvertieren – das dauert unter Umständen recht lange. Je mehr Bilder, desto mehr leidet die Performance. Deshalb werden wir direkt nach dem Laden das Bild in das passende Format konvertieren, so dass diese langsame Umwandlung später nicht mehr nötig ist. Dazu bietet Pygame gleich zwei Funktionen: eine für Bilder mit einem Alpha-Kanal (Transparenzwert pro Pixel) und eine für Bilder ohne. Da es auch eine Funktion gibt, die prüft, ob das Bild einen Alpha-Kanal hat, können wir das ohne Probleme automatisieren:

if image.get_alpha() is None:
    image = image.convert()
else:
    image = image.convert_alpha()

Alles nur rechteckig?

Wer die bisherige Ladefunktion schon getestet hat, wird festgestellt haben, dass wir immer nur Rechtecke anzeigen können. Das ist für ein echtes Spiel natürlich zu wenig, da die wenigsten Figuren rechteckig aussehen. Um dieses Problem zu lösen, bietet Pygame natürlich die bekannte Colorkey-Methode, in der eine bestimmte Farbe beim Anzeigen eines Bildes einfach ausgelassen wird. Das festlegen des Colorkeys unter Pygame ist auch sehr einfach realisierbar:

image.set_colorkey((rot, grün, blau), pygame.RLEACCEL)

Sehr oft wird als Colorkey die Farbe Magenta (255, 0, 255) verwendet, da man sie selten in einem Bild benötigt.

Zusammengesetzt: ein Bild laden

Und hier nochmal die gesamte Funktion zum Laden eines Bilds mit Pygame:

  1. # Hilfsfunktion, um ein Bild zu laden:
  2. def loadImage(filename, colorkey=None):
  3.     # Pygame das Bild laden lassen.
  4.     image = pygame.image.load(filename)
  5.  
  6.     # Das Pixelformat der Surface an den Bildschirm (genauer: die screen-Surface) anpassen.
  7.     # Dabei die passende Funktion verwenden, je nach dem, ob wir ein Bild mit Alpha-Kanal haben oder nicht.
  8.     if image.get_alpha() is None:
  9.         image = image.convert()
  10.     else:
  11.         image = image.convert_alpha()
  12.  
  13.     # Colorkey des Bildes setzen, falls nicht None.
  14.     # Bei -1 den Pixel im Bild an Position (0, 0) als Colorkey verwenden.
  15.     if colorkey is not None:
  16.         if colorkey is -1:
  17.             colorkey = image.get_at((0,0))
  18.         image.set_colorkey(colorkey, pygame.RLEACCEL)
  19.  
  20.     return image

Wir haben noch einen kleinen Zusatz eingebaut: Wenn der Colorkey auf -1 gesetzt ist, verwenden wir automatisch die Farbe des Pixels an Position (0, 0) als Colorkey, und wenn der Colorkey auf None gesetzt ist (was standardmäßig der Fall ist), dann wird überhaupt kein Colorkey gesetzt.

Bild anzeigen

Ein Bild können wir, wie bereits erwähnt, entweder direkt auf den Bildschirm rendern oder auf ein andere Surface. Diese Methode nennt sich Bit Block Transfer. Jede Surface besitzt eine Funktion namens blit, mit der eine andere Surface auf sie kopiert werden kann. In der Standardvariante wird eine komplette Surface an eine bestimmte Position geblittet:

screen.blit(image, (0, 10))

Damit wird die Surface image an der Position (0, 10) auf die Surface screen geblittet (screen ist hier die Surface, die zum Anzeigen auf dem Bildschirm verwendet wird).

Mehr zu Surfaces

Jetzt können wir Bilder laden, optimieren und anzeigen. Wem das reicht, der kann gleich zum nächsten Abschnitt springen. Alle Anderen bekommen jetzt noch ein paar Details zu Surfaces (und noch viel mehr gibt's hier). In den meisten Fällen erstellt man eine Surface durch das Laden eines Bilds. Falls nötig, kann man aber auch eine "per Hand" erstellen (etwa um einen Puffer zu haben). Dafür stehen zwei Funktionen zur Verfügung:

pygame.Surface((width, height), flags=0, depth=0, masks=None): return Surface
pygame.Surface((width, height), flags=0, Surface): return Surface

Als Flags können wir "anfragen" (das heißt, unter Umständen ignoriert SDL diese Anfrage, wenn es Probleme geben könnte), dass die Surface im Grafikkartenspeicher abgelegt werden soll (Flag HWSURFACE) und/oder dass wir einen Alpha-Kanal haben möchten (Flag SRCALPHA). Letzteres könnte langsam sein. In den meinsten Fällen ist es aber zum Glück nicht nötig, da man Colorkeys und einen Transparenzwert für die komplette Surface angeben kann. Die wichtigste Funktion einer Surface ist wohl die blit-Funktion (Dokumentation), die wir schon kurz angeschaut haben. Sie ermöglicht es, andere Surfaces (bzw. Ausschnitte daraus) auf eine andere zu kopieren:

Surface.blit(source, dest, area=None, special_flags = 0): return Rect

Der Parameter source ist die Surface, die ganz oder teilweise kopiert werden soll. Über den zweiten Parameter, dest, kann der Zielbereich angegeben werden, in den kopiert wird. Entweder als ein Tupel von Zahlen (x, y) oder als eine Rect-Variable. Um nur Ausschnitte von einer Surface zu kopieren (unerlässlich für unsere Tilemap), benötigt man den dritten Parameter, area. Er erwartet eine Rect-Variable, die den kleineren Ausschnit definiert. Über den special_flags-Parameter kann man Überblendungseffekte beim Kopieren einbauen (BLEND_ADD, BLEND_SUB, BLEND_MULT, BLEND_MIN, BLEND_MAX). Weitere wichtige Funktionen sind get_width und get_height zum Ermitteln der Größe der Surface, sowie fill, um die Surface mit einer Farbe komplett zu füllen.

Das soll erstmal genug sein, jetzt kommen wir zum Programmieren der Tilemap.

Jetzt aber zur Tilemap

Hinweis: Die Zeilennummern stimmen mit den fertigen Dateien überein, den gesamten Code findest du am Ende dieses Teiles.

Die Klasse TileType

Dies ist die einfachste und kleinste Klasse. Deshalb zuerst der Code:

  1. # Speichert die Daten eines Tile-Typs:
  2. class TileType(object):
  3.     # Im Konstruktor speichern wir den Namen
  4.     # und erstellen das Rect (den Bereich) dieses Typs auf der Tileset-Grafik.
  5.     def __init__(self, name, start_x, start_y, width, height):
  6.         self.name = name
  7.         self.rect = pygame.rect.Rect(start_x, start_y, width, height)

Wir speichern als erstes den Namen bzw. die ID des Tile-Typs in der Variable name. In einem Rect speichern wir gleichzeitig die Position und die Größe dieses Types in der Tileset-Grafik. Wir gehen gleich zur nächsten Klasse weiter, der Tileset-Klasse, in der deutlich mehr passiert.

Die Klasse Tileset

Hier in der Tileset-Klasse müssen wir vor allem die Tile-Typen speichern. Der Konstruktor der Klasse erwartet einen Dateinamen für das Bild der Tiles, dazu einen Colorkey für das Bild sowie die Breite und Höhe eines Tiles (alle Tiles haben die gleiche Größe). Zuletzt wird noch ein Dictionary für die Tile-Typen angelegt, das aber erst einmal leer bleibt.

  1. # Verwaltet die Tileset Grafik und eine Liste mit Tile-Typen.
  2. class Tileset(object):
  3.     # Im Konstruktor laden wir die Grafik
  4.     # und erstellen ein leeres Dictionary für die Tile-Typen.
  5.     def __init__(self, image, colorkey, tile_width, tile_height):
  6.         self.image = Utils.load_image(image, colorkey)
  7.         self.tile_width = tile_width
  8.         self.tile_height = tile_height
  9.         self.tile_types = dict()

Die loadImage-Funktion haben wir in das Modul Utils ausgelagert. Also das import Utils am Anfang des Moduls nicht vergessen.

Kommen wir zur wichtigsten Funktion: Das Hinzufügen eines Tile-Typen. Das ist im Grunde auch nicht sonderlich kompliziert. Wir fügen einfach eine neue Instanz der TileType-Klasse in unser Dictionary ein, mit passenden Parametern:

  1.     # Neuen Tile-Typ hinzufügen.
  2.     def add_tile(self, name, start_x, start_y):
  3.         self.tile_types[name] = TileType.TileType(name, start_x, start_y, self.tile_width, self.tile_height)

Auch das Abfragen eines Types ist leicht, einfach über den Key den passenden Wert zurückliefern:

  1.     # Versuchen, einen Tile-Type über seinen Namen in der Liste zu finden.
  2.     # Falls der Name nicht existiert, geben wir None zurück.
  3.     def get_tile(self, name):
  4.         try:
  5.             return self.tile_types[name]
  6.         except KeyError:
  7.             return None

Möglicherweise existiert der angegebene Schlüssel nicht. Wir fangen die Exception ab und geben dann einfach None zurück. Damit kommen wir zu der eigentlichen Tilemap-Klasse.

Die Klasse Tilemap

Wir beginnen wieder mit dem Konstruktor:

Die verwendete Tileset-Grafik.
  1. # Die Tilemap Klasse verwaltet die Tile-Daten, die das Aussehen der Karte beschreiben.
  2. class Tilemap(object):
  3.     def __init__(self):
  4.         # Wir erstellen ein neues Tileset.
  5.         # Hier im Tutorial fügen wir manuell vier Tile-Typen hinzu.
  6.         self.tileset = Tileset.Tileset("tileset.png", (255, 0, 255), 32, 32)
  7.         self.tileset.add_tile("grass", 0, 0)
  8.         self.tileset.add_tile("mud", 32, 0)
  9.         self.tileset.add_tile("water", 64, 0)
  10.         self.tileset.add_tile("block", 0, 32)

Es fällt natürlich sofort auf, dass das ziemlich unflexibel ist. Ein deutlich besserer Weg wäre es, sämtliche Informationen aus einer Datei zu lesen. Aufgrund der Einfachheit dieses Tutorials werden wir aber davon absehen und überlassen dem Leser die Verbesserung! Was passiert: Wir erstellen eine neue Instanz der Tileset-Klasse, übergeben ein passendes Bild, den Colorkey und die Größe eines einzelne Tiles (32x32 Pixel). Danach fügen wir per Hand vier Tile-Typen hinzu.

  1.         # Festlegen der Startposition der Kamera. Hier (0, 0).
  2.         self.camera_x = 0
  3.         self.camera_y = 0
  4.  
  5.         # Die Größe der Maps in Tiles.
  6.         self.width = 30
  7.         self.height = 25
  8.  
  9.         # Erstellen einer leeren Liste für die Tile Daten.
  10.         self.tiles = list()

Wir legen Standardwerte für die Position der Kamera und die Größe der Karte fest. Die Größe der Karten wird in Tiles angegeben, nicht in Pixeln. Auch hier gilt wieder: Die Größe kann flexibler gestaltet werden, etwa durch einen Parameter in dem Konstruktor oder eine Datei. Zudem erstellen wir eine noch leere Liste, in der wir später die Tiles speichern wollen.

  1.         # Manuelles Befüllen der Tile-Liste:
  2.         # Jedes Feld bekommt ein zufälliges Tile zugewiesen.
  3.         for i in range(0, self.height):
  4.             self.tiles.append(list())
  5.             for j in range(0, self.width):
  6.                 x = random.randint(0, 4)
  7.                 if x == 0:
  8.                     self.tiles[i].append("grass")
  9.                 elif x == 1:
  10.                     self.tiles[i].append("water")
  11.                 elif x == 2:
  12.                     self.tiles[i].append("mud")
  13.                 else:
  14.                     self.tiles[i].append("block")

Mit diesem Code endet der Konstruktor. Wir befüllen hier die Liste der Tiles. Dazu verwenden wir zwei for-Schleifen, die die Höhe bzw. Breite der Karte durchlaufen. Für jedes i (eine Zeile) erstellen wir eine neue Liste, die wir mit Zufallswerten füllen (hier gibt es eine Auswahl zwischen den vier hinzugefügten Tiles). Standardmäßig sollte hier jedes Feld auf "" oder None gesetzt werden, oder man liest gleich alle Positionsdaten aus einer Datei. Schauen wir uns die komplizierteste, aber auch wichtigste Funktion an: Die Funktion render, die die Karte auf den Bildschirm (oder eine beliebige andere Surface rendert):

  1.     # Hier rendern wir den sichtbaren Teil der Karte.
  2.     def render(self, screen):
  3.         # Zeilenweise durch die Tiles durchgehen.
  4.         for y in range(0, int(screen.get_height() / self.tileset.tile_height) + 1):
  5.             # Die Kamera Position mit einbeziehen.
  6.             ty = y + self.camera_y
  7.             if ty >= self.height or ty < 0:
  8.                 continue
  9.             # Die aktuelle Zeile zum einfacheren Zugriff speichern.
  10.             line = self.tiles[ty]
  11.             # Und jetzt spaltenweise die Tiles rendern.
  12.             for x in range(0, int(screen.get_width() / self.tileset.tile_width) + 1):
  13.                 # Auch hier müssen wir die Kamera beachten.
  14.                 tx = x + self.camera_x
  15.                 if tx >= self.width or tx < 0:
  16.                     continue
  17.                 # Wir versuchen, die Daten des Tiles zu bekommen.
  18.                 tilename = line[tx]
  19.                 tile = self.tileset.get_tile(tilename)
  20.                 # Falls das nicht fehlschlägt können wir das Tile auf die screen-Surface blitten.
  21.                 if tile is not None:
  22.                     screen.blit(self.tileset.image, (x * self.tileset.tile_width, y * self.tileset.tile_height), tile.rect)

Wir werden die Funktion Zeile für Zeile durchgehen:

Damit sind wir fast fertig. Die Veränderung der Kameraposition ist im Gegensatz zur letzten Funktion sehr einfach:

  1.     # Tastendrücke verarbeiten:
  2.     def handle_input(self, key):
  3.         # Pfeiltaste links oder rechts erhöht bzw. verringert die x-Position der Kamera.
  4.         if key == pygame.K_LEFT:
  5.             self.camera_x += 1
  6.         if key == pygame.K_RIGHT:
  7.             self.camera_x -= 1
  8.  
  9.         # Und das gleiche nochmal für die y-Position.
  10.         if key == pygame.K_UP:
  11.             self.camera_y += 1
  12.         if key == pygame.K_DOWN:
  13.             self.camera_y -= 1

Tilemap anzeigen lassen

Für dieses Tutorial sind wir jetzt mit der Tilemap fertig. Für ein kleines 2D-Spiel kann man sie gut als Grundlage verwenden und noch etwas verbessern. Etwa durch mehrere Layer, weitere Informationen der einzelne Tiles (eine eigene Tile-Klasse zum Beispiel) und natürlich Auslesen der Map-Daten aus einer Datei. Zum Schluss bauen wir die Tilemap noch in das Skript aus dem letzten Tutorial ein. Zuerst erstellen wir eine Tilemap:

  1.     map = Tilemap.Tilemap()

In der Event-Schleife geben wir die gedrückten Tasten an die Map weiter:

  1.                 map.handle_input(event.key)

Und direkt vor dem Anzeigen des Bildpuffers rendern wir die Map:

  1.         map.render(screen)

Überblick

Und hier nochmal der gesamte Code im Überblick:

Utils.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4.  
  5. # Hilfsfunktion, um ein Bild zu laden:
  6. def load_image(filename, colorkey=None):
  7.     # Pygame das Bild laden lassen.
  8.     image = pygame.image.load(filename)
  9.  
  10.     # Das Pixelformat der Surface an den Bildschirm (genauer: die screen-Surface) anpassen.
  11.     # Dabei die passende Funktion verwenden, je nach dem, ob wir ein Bild mit Alpha-Kanal haben oder nicht.
  12.     if image.get_alpha() is None:
  13.         image = image.convert()
  14.     else:
  15.         image = image.convert_alpha()
  16.  
  17.     # Colorkey des Bildes setzen, falls nicht None.
  18.     # Bei -1 den Pixel im Bild an Position (0, 0) als Colorkey verwenden.
  19.     if colorkey is not None:
  20.         if colorkey is -1:
  21.             colorkey = image.get_at((0,0))
  22.         image.set_colorkey(colorkey, pygame.RLEACCEL)
  23.  
  24.     return image

TileType.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4.  
  5. # Speichert die Daten eines Tile-Typs:
  6. class TileType(object):
  7.     # Im Konstruktor speichern wir den Namen
  8.     # und erstellen das Rect (den Bereich) dieses Typs auf der Tileset-Grafik.
  9.     def __init__(self, name, start_x, start_y, width, height):
  10.         self.name = name
  11.         self.rect = pygame.rect.Rect(start_x, start_y, width, height)

Tileset.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4. import Utils
  5. import TileType
  6.  
  7. # Verwaltet die Tileset Grafik und eine Liste mit Tile-Typen.
  8. class Tileset(object):
  9.     # Im Konstruktor laden wir die Grafik
  10.     # und erstellen ein leeres Dictionary für die Tile-Typen.
  11.     def __init__(self, image, colorkey, tile_width, tile_height):
  12.         self.image = Utils.load_image(image, colorkey)
  13.         self.tile_width = tile_width
  14.         self.tile_height = tile_height
  15.         self.tile_types = dict()
  16.  
  17.     # Neuen Tile-Typ hinzufügen.
  18.     def add_tile(self, name, start_x, start_y):
  19.         self.tile_types[name] = TileType.TileType(name, start_x, start_y, self.tile_width, self.tile_height)
  20.  
  21.     # Versuchen, einen Tile-Type über seinen Name in der Liste zu finden.
  22.     # Falls der Name nicht existiert, geben wir None zurück.
  23.     def get_tile(self, name):
  24.         try:
  25.             return self.tile_types[name]
  26.         except KeyError:
  27.             return None

Tilemap.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import random
  4. import pygame
  5. import Tileset
  6.  
  7. # Die Tilemap Klasse verwaltet die Tile-Daten, die das Aussehen der Karte beschreiben.
  8. class Tilemap(object):
  9.     def __init__(self):
  10.         # Wir erstellen ein neues Tileset.
  11.         # Hier im Tutorial fügen wir manuell vier Tile-Typen hinzu.
  12.         self.tileset = Tileset.Tileset("tileset.png", (255, 0, 255), 32, 32)
  13.         self.tileset.add_tile("grass", 0, 0)
  14.         self.tileset.add_tile("mud", 32, 0)
  15.         self.tileset.add_tile("water", 64, 0)
  16.         self.tileset.add_tile("block", 0, 32)
  17.  
  18.         # Festlegen der Startposition der Kamera. Hier (0, 0).
  19.         self.camera_x = 0
  20.         self.camera_y = 0
  21.  
  22.         # Die Größe der Maps in Tiles.
  23.         self.width = 30
  24.         self.height = 25
  25.  
  26.         # Erstellen einer leeren Liste für die Tile Daten.
  27.         self.tiles = list()
  28.  
  29.         # Manuelles Befüllen der Tile-Liste:
  30.         # Jedes Feld bekommt ein zufälliges Tile zugewiesen.
  31.         for i in range(0, self.height):
  32.             self.tiles.append(list())
  33.             for j in range(0, self.width):
  34.                 x = random.randint(0, 4)
  35.                 if x == 0:
  36.                     self.tiles[i].append("grass")
  37.                 elif x == 1:
  38.                     self.tiles[i].append("water")
  39.                 elif x == 2:
  40.                     self.tiles[i].append("mud")
  41.                 else:
  42.                     self.tiles[i].append("block")
  43.  
  44.  
  45.     # Hier rendern wir den sichtbaren Teil der Karte.
  46.     def render(self, screen):
  47.         # Zeilenweise durch die Tiles durchgehen.
  48.         for y in range(0, int(screen.get_height() / self.tileset.tile_height) + 1):
  49.             # Die Kamera Position mit einbeziehen.
  50.             ty = y + self.camera_y
  51.             if ty >= self.height or ty < 0:
  52.                 continue
  53.             # Die aktuelle Zeile zum einfacheren Zugriff speichern.
  54.             line = self.tiles[ty]
  55.             # Und jetzt spaltenweise die Tiles rendern.
  56.             for x in range(0, int(screen.get_width() / self.tileset.tile_width) + 1):
  57.                 # Auch hier müssen wir die Kamera beachten.
  58.                 tx = x + self.camera_x
  59.                 if tx >= self.width or tx < 0:
  60.                     continue
  61.                 # Wir versuchen, die Daten des Tiles zu bekommen.
  62.                 tilename = line[tx]
  63.                 tile = self.tileset.get_tile(tilename)
  64.                 # Falls das nicht fehlschlägt können wir das Tile auf die screen-Surface blitten.
  65.                 if tile is not None:
  66.                     screen.blit(self.tileset.image, (x * self.tileset.tile_width, y * self.tileset.tile_height), tile.rect)
  67.  
  68.  
  69.     # Tastendrücke verarbeiten:
  70.     def handle_input(self, key):
  71.         # Pfeiltaste links oder rechts erhöht bzw. verringert die x-Position der Kamera.
  72.         if key == pygame.K_LEFT:
  73.             self.camera_x += 1
  74.         if key == pygame.K_RIGHT:
  75.             self.camera_x -= 1
  76.  
  77.         # Und das gleiche nochmal für die y-Position.
  78.         if key == pygame.K_UP:
  79.             self.camera_y += 1
  80.         if key == pygame.K_DOWN:
  81.             self.camera_y -= 1

Teil_2.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. # Pygame Modul importieren.
  4. import pygame
  5.  
  6. # Unser Tilemap Modul ebenfalls importieren.
  7. import Tilemap
  8.  
  9. # Überprüfen, ob die optionalen Text- und Sound-Module geladen werden konnten.
  10. if not pygame.font: print('Fehler pygame.font Modul konnte nicht geladen werden!')
  11. if not pygame.mixer: print('Fehler pygame.mixer Modul konnte nicht geladen werden!')
  12.  
  13. def main():
  14.     # Initialisieren aller Pygame-Module und 
  15.     # Fenster erstellen (wir bekommen eine Surface, die den Bildschirm repräsentiert).
  16.     pygame.init()
  17.     screen = pygame.display.set_mode((800, 600))
  18.  
  19.     # Titel des Fensters setzen, Mauszeiger nicht verstecken und Tastendrücke wiederholt senden.
  20.     pygame.display.set_caption("Pygame-Tutorial: Tilemap")
  21.     pygame.mouse.set_visible(1)
  22.     pygame.key.set_repeat(1, 30)
  23.  
  24.     # Clock Objekt erstellen, das wir benötigen, um die Framerate zu begrenzen.
  25.     clock = pygame.time.Clock()
  26.  
  27.     # Wir erstellen eine Tilemap.
  28.     map = Tilemap.Tilemap()
  29.  
  30.     # Die Schleife, und damit unser Spiel, läuft solange running == True.
  31.     running = True
  32.     while running:
  33.         # Framerate auf 30 Frames pro Sekunde beschränken.
  34.         # Pygame wartet, falls das Programm schneller läuft.
  35.         clock.tick(30)
  36.  
  37.         # screen Surface mit Schwarz (RGB = 0, 0, 0) füllen.
  38.         screen.fill((0, 0, 0))
  39.  
  40.         # Alle aufgelaufenen Events holen und abarbeiten.
  41.         for event in pygame.event.get():
  42.             # Spiel beenden, wenn wir ein QUIT-Event finden.
  43.             if event.type == pygame.QUIT:
  44.                 running = False
  45.  
  46.             # Wir interessieren uns auch für "Taste gedrückt"-Events.
  47.             if event.type == pygame.KEYDOWN:
  48.                 # Wenn Escape gedrückt wird posten wir ein QUIT-Event in Pygames Event-Warteschlange.
  49.                 if event.key == pygame.K_ESCAPE:
  50.                     pygame.event.post(pygame.event.Event(pygame.QUIT))
  51.  
  52.                 # Alle Tastendrücke auch der Tilemap mitteilen.
  53.                 map.handle_input(event.key)
  54.  
  55.         # Die Tilemap auf die screen-Surface rendern.
  56.         map.render(screen)
  57.  
  58.         # Inhalt von screen anzeigen
  59.         pygame.display.flip()
  60.  
  61.  
  62. # Überprüfen, ob dieses Modul als Programm läuft und nicht in einem anderen Modul importiert wird.
  63. if __name__ == '__main__':
  64.     # Unsere Main-Funktion aufrufen.
  65.     main()

Animation einer Spielfigur

Der Plan

Screenshot der animierten Spielfigur.

Nachdem wir nun bereits eine einfache Tilemap programmiert haben, wollen wir jetzt eine Spielfigur über unsere Karte laufen lassen. Dazu erstellen wir eine Player-Klasse. In umserem einfachen Tutorial soll diese Klasse folgende Dinge können: ein Bild an der Position der Spielfigur anzeigen, die Position verändern, wenn der Spieler die Pfeiltasten drückt, und die Spielfigur animieren, um eine Gehbewegung dazustellen. Die ersten beiden Punkte können wir bereits programmieren. Schauen wir uns also erstmal an, wie das mit der Animation funktioniert.

Die Animation-Klasse

Die Gehbewegung der Spielfigur entsteht, indem wir in schneller Folge die Grafiken der Spielfigur austauschen. In diesem Tutorial speichern wir alle Einzelbilder (Frames) einer Animation, in einer einzigen Bilddatei. Außerdem müssen die Frames in einer Reihe nebeneinander liegen und alle gleich groß sein.

Wir beginnen damit, eine Klasse für eine Animation zu programmieren.

  1. # Die Klasse kümmert sich um eine einfache Animation:
  2. class Animation(object):
  3.     def __init__(self, image, start_x, start_y, num, width, height, duration):
  4.         # Die Surface speichern,
  5.         # alle Einzelbilder müssen in einer Reihe liegen.
  6.         self.image = image
  7.  
  8.         # Dazu müssen wir wissen, an welcher Position die Frames beginnen,
  9.         # wie viele Frames die Animation hat,
  10.         # sowie die Breite und Höhe der Animation kennen.
  11.         self.start_x = start_x
  12.         self.start_y = start_y
  13.         self.num = num
  14.         self.width = width
  15.         self.height = height
  16.  
  17.         # Und natürlich auch, nach welchem Zeitraum wir das nächsten Frame anzeigen sollen.
  18.         self.duration = duration
  19.  
  20.         # Die aktuelle Zeit und das aktuellen Frame speichern wir ebenfalls.
  21.         self.time = 0
  22.         self.current = 0

Im Konstruktor passiert nicht viel. Um die Animation darstellen zu können, benötigen wir ein paar Daten: Natürlich das Bild, wir erwarten hier aber keinen Dateinamen sondern, direkt eine Surface mit den Bilddaten. Das hat den Vorteil, dass wir in einem Bild mehrere Animationen unterbringen können und nicht unnötigerweise das Bild für jede Animation komplett geladen wird. Aus diesem Grund müssen wir auch wissen, an welcher Position im Bild die Animation beginnt: start_x und start_y. Dazu brauchen wir die Abmessungen der Animation (width, height) und die Anzahl der Einzelbilder (num). Jetzt fehlt noch die Zeitspanne, für die ein Frame angezeigt, wird bevor zum nächsten gewechselt wird: duration.

Das Rendern der Animation erledigen wir in einer Zeile:

  1.     # Das aktuelle Frame an einer bestimmten Position rendern:
  2.     def render(self, screen, pos):
  3.         # Welchen Bereich aus der Grafik müssen wir anzeigen?
  4.         # Die x-Position können wir aus der Breite und der Start-Position berechnen,
  5.         # die restlichen Werte kennen wir bereits.
  6.         screen.blit(self.image, pos, pygame.Rect(self.start_x + (self.width * self.current), self.start_y, self.width, self.height))

Wir benötigen hier wieder eine Surface, auf die wir zeichnen sollen, und dazu eine Positionsangabe. Diese beiden Parameter geben wir so direkt an die blit-Methode weiter. Fehlt nur noch die Information, welcher Bereich des Bilds genau dargestellt werden soll. Dazu basteln wir ein Rechteck (pygame.Rect) mit den folgenden Daten: Der Beginn unserer Animation ist in start_x und start_y gespeichert. Da wir festgelegt haben, dass sich unsere Frames alle in einer Reihe befinden müssen, addieren wir die Breite des Einzelbildes multipliziert mit der Nummer des aktuellen Einzelbild zur x-Position dazu. Das bringt uns exakt zu dem Einzelbild, das wir anzeigen wollen. Die Größe der Animation kennen wir bereits.

Jetzt müssen wir die Animation nur noch aktualisieren, denn momentan würde immer nur das gleiche Bild (das erste) angezeigt.

  1.     # Die update-Methode rufen wir einmal pro Frame auf:
  2.     def update(self, time=1):
  3.         # Die vergangene Zeit addieren
  4.         self.time += time
  5.  
  6.         # Falls wir den Anzeige-Zeitraum überschreiten, ...
  7.         if self.time > self.duration:
  8.             # ... setzten wir die Zeit zurück und gehen zum nächsten Frame.
  9.             self.time = 0
  10.             self.current += 1
  11.             # Sicherstellen, dass das aktuelle Frame auch verfügbar ist.
  12.             if self.current >= self.num:
  13.                 self.current = 0

Ein Einzelbild der Animation soll immer nur eine bestimmte Zeit lang angezeigt werden. Der update-Methode übergeben wir einfach die Zeit, die seit dem letzten Aufruf der Methode vergangen ist. Hier in dem Tutorial machen wir das auf Frame-Basis, deshalb erhöhen wir standardmäßig die Zeit um 1 und rufen die update-Methode später in jedem Frame genau einmal auf. Ab Zeile 22 überprüfen wir, ob genügend viel Zeit vergangen ist und wir ein neues Frame anzeigen müssen. Wenn die Zeit größer als die Anzeigedauer für ein Einzelbild ist, setzen wir zuerst die Zeit zurück auf 0 und erhöhen die Nummer des aktuellen Einzelbildes. Im Tutorial lassen wir unsere Animation einfach am Anfang weiterspielen wenn sie fertig ist (Looping), deshalb sorgen wir noch dafür, dass current im erlaubten Wertebereich bleibt.

Und damit ist unsere Animations-Klasse schon fertig. Den Rest erledigt die Player-Klasse.

Die Player-Klasse

Unsere Player-Klasse verwendet jetzt die soeben programmierte Animationsklasse, um zwei Animationen darzustellen: Laufen nach links und rechts. Wir machen es uns wieder einfach und erstellen dafür einfach zwei Animation-Objekte. Nicht vergessen: Der Konstuktor der Animation-Klasse erwartet keinen Bildnamen, sondern das geladene Bild.

  1. # Die Player Klasse verwendet zwei Animationen, um eine steuerbare Spielfigur dazustellen.
  2. class Player(object):
  3.     def __init__(self):
  4.         # Bild laden und erste Animation erstellen: 
  5.         self.anim_image_right = Utils.loadImage("tileset.png", (255, 0, 255))
  6.         self.anim_right = Animation.Animation(self.anim_image_right, 32, 32, 2, 32, 64, 15)

Die loadImage-Funktion kennen wir noch von früher, wir verwenden sogar das selbe Bild wie für die Tilemap (und verdrängen kurz, dass das Bild natürlich jetzt unnötigerweise zweimal geladen wird). In Zeile 10 erstellen wir dann die Animation für das nach rechts gehen: Unser soeben geladenes Bild, Anfangsposition ist (32, 32), wir haben 2 Einzelbilder, die wir anzeigen möchten, die Breite eines Frames ist 32, die Höhe 64, und wir wollen eine Dauer von 15 (in unserem Fall heißt das 15 Frames).

Kommen wir zur "links gehen"-Animation. Normalerweise müssten wir dafür neue Grafiken erstellen. Hier würde es sogar reichen, die Animation einfach zu spiegeln. Aber geht es nicht noch einfacher? Natürlich. Im Modul pygame.transform (Dokumentation) gibt es die Methode flip:

pygame.transform.flip(Surface, xbool, ybool): return Surface

Damit können wir unser Bild einfach horizontal (das brauchen wir) oder vertikal spiegeln. Dabei wird eine neue Surface erstellt, die wir für unsere zweite Animation verwenden können.

  1.         # Die Grafik spiegeln und in einer neuen Surface speichern,
  2.         # dann können wir die linke Animation erstellen.
  3.         self.anim_image_left = pygame.transform.flip(self.anim_image_right, True, False)
  4.         self.anim_left = Animation.Animation(self.anim_image_left, 32, 32, 2, 32, 64, 15)

Und diese zweite Animation erstellen wir nach dem selben Muster wie zuvor. Wir können uns außerdem für später merken: Im transform-Modul gibt es noch weitere Funktionen zum Skalieren oder Rotieren von Surfaces. Der Rest des Konstruktors speichert nur noch die Position des Spielers, die Blickrichtung und ob wir gerade laufen oder nicht:

  1.         # Startposition des Players festlegen und
  2.         # merken, in welche Richtung wir schauen, und ob wir überhaupt laufen.
  3.         self.pos_x = 10*32
  4.         self.pos_y = 13*32        
  5.         self.dir = 0
  6.         self.walking = False

Diese Werte kann der Spieler später über die Pfeiltasten beeinflussen. Das wird in der handle_input-Methode erledigt:

  1.     def handle_input(self, key):
  2.         # Linke Pfeiltaste wird gedrückt:
  3.         if key == pygame.K_LEFT:
  4.             # x-Position der Spielfigur anpassen,
  5.             # die Blickrichtung festlegen
  6.             # und den Laufen-Zustand einschalten.
  7.             self.pos_x -= 1
  8.             self.dir = -1
  9.             self.walking = True
  10.  
  11.         # Und nochmal für die rechte Pfeiltaste.
  12.         if key == pygame.K_RIGHT:
  13.             self.pos_x += 1
  14.             self.dir = 1
  15.             self.walking = True

Das ist sehr ähnlich zur handle_input-Methode unserer Tilemap-Klasse. Je nach dem, welche Pfeiltaste gedrückt wird, erhöhen bzw. verringern wir die x-Position des Spielers (damit kann er nach rechts und links laufen) und passen die Blickrichtung an: -1 für links und +1 für rechts. Außerdem sind wir in beiden Fällen am laufen, setzen walking also auf True. Diese Information benötigen wir zum Rendern:

  1.     def render(self, screen):
  2.         # Die Blickrichtung ist links:
  3.         if self.dir == -1:
  4.             # Wenn der Spieler die linke oder rechte Pfeiltaste gedrückt hat sind wir am laufen,
  5.             if self.walking:                
  6.                 # nur dann die Animation updaten.
  7.                 self.anim_left.update()
  8.             # Blickrichtung links rendern.
  9.             self.anim_left.render(screen, (self.pos_x, self.pos_y))   
  10.         else:
  11.             # Und das gleiche nochmal für rechts:
  12.             if self.walking:
  13.                 self.anim_right.update()
  14.             self.anim_right.render(screen, (self.pos_x, self.pos_y))
  15.  
  16.         # De Laufen-Zustand zurücksetzen, im nächsten Frame bleiben wir stehen.
  17.         self.walking = False

Die render-Methode sieht komplizierter aus als sie es ist. Wir haben drei mögliche Zustände, in denen sich der Spieler befinden kann: Stehen, links gehen und rechts gehen. Dafür haben wir vorher die Blickrichtung gespeichert. Schauen wir zuerst mal, was passiert, wenn der Spieler weder die rechte noch die linke Pfeiltaste gedrückt hat: dir ist 0 (das haben wir im Konstruktor so festgelegt), wir kommen also zu Zeile 26. Der Einfachheit halber lassen wir in diesem Fall den Spieler einfach immer nach rechts schauen, später wird die Blickrichtung erhalten bleiben. Da walking = False ist, aktualisieren wir die Animation nicht, das Einzelbild verändert sich also nicht. Am Ende der Methode setzen wir walking wieder auf False, damit wir im nächsten Frame stehen (natürlich nur falls, der Spieler keine Pfeiltaste mehr drückt). Die Blickrichtung setzen wir nicht zurück, damit wissen wir im nächsten Frame noch, welche Animation wir anzeigen sollen, auch wenn der Spieler keine Pfeiltaste gedrückt hat. Der Zustand für "rechts gehen" (mit dir ist 1) ist exakt der gleiche wie der, den wir eben angesehen haben, und für die Variante "links gehen" (dir ist -1) aktualisieren und zeichnen wir anstatt der rechten Animation eben die linke.

Jetzt fehlt nur noch die Einbindung der Player-Klasse in unser Beispielspiel.

Zusammenbasteln

Wann erstellen wir ein Player-Objekt? Wenn das Level geladen wird. Hier im Tutorial passiert das alles im Konstruktor der Tilemap-Klasse. Zuerst erweitern wir das Tileset um zwei neue Tiles:

  1.         self.tileset.add_tile("grass-mud", 0, 64)
  2.         self.tileset.add_tile("empty", 0, 96)

Und dann passen wir die Generierung der Tilemap etwas an:

  1.         for i in range(0, self.height):
  2.             self.tiles.append(list())
  3.             for j in range(0, self.width):
  4.                 if i == 14:
  5.                     self.tiles[i].append("grass") 
  6.                 elif i == 15:
  7.                     self.tiles[i].append("grass-mud")
  8.                 elif i > 15:
  9.                     self.tiles[i].append("mud")
  10.                 else:
  11.                     self.tiles[i].append("empty")

Um am Ende des Konstuktors endlich einen Spieler erstellen zu können:

  1.         self.player = Player.Player()

In der render-Methode rendern wir den Spieler:

  1.         self.player.render(screen)

Und in der handle_input-Methode reichen wir die Tastendrücke durch:

  1.         self.player.handle_input(key)

Und fertig. Mit den Pfeiltasten können wir unseren animierten Player jetzt nach rechts und links über die Tilemap bewegen.

Hier nochmal der neue Code im Überblick:

TileType.py, Tileset.py und Utils.py haben sich nicht verändert.

Animation.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4.  
  5. # Die Klasse kümmert sich um eine einfache Animation:
  6. class Animation(object):
  7.     def __init__(self, image, start_x, start_y, num, width, height, duration):
  8.         # Die Surface speichern,
  9.         # alle Einzelbilder müssen in einer Reihe liegen.
  10.         self.image = image
  11.  
  12.         # Dazu müssen wir wissen, an welcher Position die Frames beginnen,
  13.         # wie viele Frames die Animation hat,
  14.         # sowie die Breite und Höhe der Animation kennen.
  15.         self.start_x = start_x
  16.         self.start_y = start_y
  17.         self.num = num
  18.         self.width = width
  19.         self.height = height
  20.  
  21.         # Und natürlich auch, nach welchem Zeitraum wir das nächsten Frame anzeigen sollen.
  22.         self.duration = duration
  23.  
  24.         # Die aktuelle Zeit und das aktuellen Frame speichern wir ebenfalls.
  25.         self.time = 0
  26.         self.current = 0
  27.  
  28.  
  29.     # Die update-Methode rufen wir einmal pro Frame auf:
  30.     def update(self, time=1):
  31.         # Die vergangene Zeit addieren
  32.         self.time += time
  33.  
  34.         # Falls wir den Anzeige-Zeitraum überschreiten, ...
  35.         if self.time > self.duration:
  36.             # ... setzten wir die Zeit zurück und gehen zum nächsten Frame.
  37.             self.time = 0
  38.             self.current += 1
  39.             # Sicherstellen, dass das aktuelle Frame auch verfügbar ist.
  40.             if self.current >= self.num:
  41.                 self.current = 0
  42.  
  43.  
  44.     # Das aktuelle Frame an einer bestimmten Position rendern:
  45.     def render(self, screen, pos):
  46.         # Welchen Bereich aus der Grafik müssen wir anzeigen?
  47.         # Die x-Position können wir aus der Breite und der Start-Position berechnen,
  48.         # die restlichen Werte kennen wir bereits.
  49.         screen.blit(self.image, pos, pygame.Rect(self.start_x + (self.width * self.current), self.start_y, self.width, self.height))

Player.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4. import Utils
  5. import Animation
  6.  
  7. # Die Player Klasse verwendet zwei Animationen, um eine steuerbare Spielfigur dazustellen.
  8. class Player(object):
  9.     def __init__(self):
  10.         # Bild laden und erste Animation erstellen: 
  11.         self.anim_image_right = Utils.load_image("tileset.png", (255, 0, 255))
  12.         self.anim_right = Animation.Animation(self.anim_image_right, 32, 32, 2, 32, 64, 15)  
  13.  
  14.         # Die Grafik spiegeln und in einer neuen Surface speichern,
  15.         # dann können wir die linke Animation erstellen.
  16.         self.anim_image_left = pygame.transform.flip(self.anim_image_right, True, False)
  17.         self.anim_left = Animation.Animation(self.anim_image_left, 32, 32, 2, 32, 64, 15)
  18.  
  19.         # Start-Position des Players festlegen und
  20.         # merken in welche Richtung wir schauen und ob wir überhaupt laufen.
  21.         self.pos_x = 10*32
  22.         self.pos_y = 13*32        
  23.         self.dir = 0
  24.         self.walking = False
  25.  
  26.  
  27.     def render(self, screen):
  28.         # Die Blickrichtung ist links:
  29.         if self.dir == -1:
  30.             # Wenn der Spieler die linke oder rechte Pfeiltaste gedrückt hat sind wir am laufen,
  31.             if self.walking:                
  32.                 # nur dann die Animation updaten.
  33.                 self.anim_left.update()
  34.             # Blickrichtung links rendern.
  35.             self.anim_left.render(screen, (self.pos_x, self.pos_y))   
  36.         else:
  37.             # Und das gleiche nochmal für rechts:
  38.             if self.walking:
  39.                 self.anim_right.update()
  40.             self.anim_right.render(screen, (self.pos_x, self.pos_y))
  41.  
  42.         # De Laufen-Zustand zurücksetzen, im nächsten Frame bleiben wir stehen.
  43.         self.walking = False
  44.  
  45.  
  46.     def handle_input(self, key):
  47.         # Linke Pfeiltaste wird gedrückt:
  48.         if key == pygame.K_LEFT:
  49.             # x-Position der Spielfigur anpassen,
  50.             # die Blickrichtung festlegen
  51.             # und den Laufen-Zustand einschalten.
  52.             self.pos_x -= 1
  53.             self.dir = -1
  54.             self.walking = True
  55.  
  56.         # Und nochmal für die rechte Pfeiltaste.
  57.         if key == pygame.K_RIGHT:
  58.             self.pos_x += 1
  59.             self.dir = 1
  60.             self.walking = True

Tilemap.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4. import Tileset
  5. import Player
  6.  
  7. # Die Tilemap Klasse verwaltet die Tile-Daten, die das Aussehen der Karte beschreiben.
  8. class Tilemap(object):
  9.     def __init__(self):
  10.         # Wir erstellen ein neues Tileset.
  11.         # Hier im Tutorial fügen wir manuell vier Tile-Typen hinzu.
  12.         self.tileset = Tileset.Tileset("tileset.png", (255, 0, 255), 32, 32)
  13.         self.tileset.add_tile("grass", 0, 0)
  14.         self.tileset.add_tile("mud", 32, 0)        
  15.         self.tileset.add_tile("grass-mud", 0, 64)
  16.         self.tileset.add_tile("empty", 0, 96)
  17.  
  18.         # Festlegen der Startposition der Kamera. Hier (0, 0).
  19.         self.camera_x = 0
  20.         self.camera_y = 0
  21.  
  22.         # Die Größe der Maps in Tiles.
  23.         self.width = 30
  24.         self.height = 25
  25.  
  26.         # Erstellen einer leeren Liste für die Tile Daten.
  27.         self.tiles = list()
  28.  
  29.         # Sehr einfache Karte basteln:
  30.         for i in range(0, self.height):
  31.             self.tiles.append(list())
  32.             for j in range(0, self.width):
  33.                 if i == 14:
  34.                     self.tiles[i].append("grass") 
  35.                 elif i == 15:
  36.                     self.tiles[i].append("grass-mud")
  37.                 elif i > 15:
  38.                     self.tiles[i].append("mud")
  39.                 else:
  40.                     self.tiles[i].append("empty")
  41.  
  42.         # Player-Objekt erstellen.
  43.         self.player = Player.Player()
  44.  
  45.  
  46.     # Hier rendern wir den sichtbaren Teil der Karte.
  47.     def render(self, screen):
  48.         # Zeilenweise durch die Tiles durchgehen.
  49.         for y in range(0, int(screen.get_height() / self.tileset.tile_height) + 1):
  50.             # Die Kamera Position mit einbeziehen.
  51.             ty = y + self.camera_y
  52.             if ty >= self.height or ty < 0:
  53.                 continue
  54.             # Die aktuelle Zeile zum einfacheren Zugriff speichern.
  55.             line = self.tiles[ty]
  56.             # Und jetzt spaltenweise die Tiles rendern.
  57.             for x in range(0, int(screen.get_width() / self.tileset.tile_width) + 1):
  58.                 # Auch hier müssen wir die Kamera beachten.
  59.                 tx = x + self.camera_x
  60.                 if tx >= self.width or tx < 0:
  61.                     continue
  62.                 # Wir versuchen, die Daten des Tiles zu bekommen.
  63.                 tilename = line[tx]
  64.                 tile = self.tileset.get_tile(tilename)
  65.                 # Falls das nicht fehlschlägt können wir das Tile auf die screen-Surface blitten.
  66.                 if tile is not None:
  67.                     screen.blit(self.tileset.image, (x * self.tileset.tile_width, y * self.tileset.tile_height), tile.rect)
  68.  
  69.         # Und zuletzt den Player rendern.
  70.         self.player.render(screen)
  71.  
  72.  
  73.     # Tastendrücke an den Player weiterreichen:
  74.     def handle_input(self, key):        
  75.         self.player.handle_input(key)

Teil_3.py

  1. # -*- coding: UTF-8 -*-
  2.  
  3. # Pygame Modul importieren.
  4. import pygame
  5.  
  6. # Unser Tilemap Modul
  7. import Tilemap
  8.  
  9. # Überprüfen, ob die optionalen Text- und Sound-Module geladen werden konnten.
  10. if not pygame.font: print('Fehler pygame.font Modul konnte nicht geladen werden!')
  11. if not pygame.mixer: print('Fehler pygame.mixer Modul konnte nicht geladen werden!')
  12.  
  13. def main():
  14.     # Initialisieren aller Pygame-Module und 
  15.     # Fenster erstellen (wir bekommen eine Surface, die den Bildschirm repräsentiert).
  16.     pygame.init()
  17.     screen = pygame.display.set_mode((800, 600))
  18.  
  19.     # Titel des Fensters setzen, Mauszeiger nicht verstecken und Tastendrücke wiederholt senden.
  20.     pygame.display.set_caption("Pygame-Tutorial: Animation")
  21.     pygame.mouse.set_visible(1)
  22.     pygame.key.set_repeat(1, 30)
  23.  
  24.     # Clock-Objekt erstellen, das wir benötigen, um die Framerate zu begrenzen.
  25.     clock = pygame.time.Clock()
  26.  
  27.     # Wir erstellen eine Tilemap.
  28.     map = Tilemap.Tilemap()
  29.  
  30.     # Die Schleife, und damit unser Spiel, läuft solange running == True.
  31.     running = True
  32.     while running:
  33.         # Framerate auf 30 Frames pro Sekunde beschränken.
  34.         # Pygame wartet, falls das Programm schneller läuft.
  35.         clock.tick(30)
  36.  
  37.         # screen Surface mit Schwarz (RGB = 0, 0, 0) füllen.
  38.         screen.fill((198, 209, 255))
  39.  
  40.         # Alle aufgelaufenen Events holen und abarbeiten.
  41.         for event in pygame.event.get():
  42.             # Spiel beenden, wenn wir ein QUIT-Event finden.
  43.             if event.type == pygame.QUIT:
  44.                 running = False
  45.  
  46.             # Wir interessieren uns auch für "Taste gedrückt"-Events.
  47.             if event.type == pygame.KEYDOWN:
  48.                 # Wenn Escape gedrückt wird posten wir ein QUIT-Event in Pygames Event-Warteschlange.
  49.                 if event.key == pygame.K_ESCAPE:
  50.                     pygame.event.post(pygame.event.Event(pygame.QUIT))
  51.  
  52.                 # Alle Tastendrücke auch der Tilemap mitteilen.
  53.                 map.handle_input(event.key)
  54.  
  55.         # Die Tilemap auf die screen-Surface rendern.
  56.         map.render(screen)
  57.  
  58.         # Inhalt von screen anzeigen
  59.         pygame.display.flip()
  60.  
  61.  
  62. # Überprüfen, ob dieses Modul als Programm läuft und nicht in einem anderen Modul importiert wird.
  63. if __name__ == '__main__':
  64.     # Unsere Main-Funktion aufrufen.
  65.     main()

Sourcecode herunterladen

Du kannst dir den Sourcecode (und die Tileset-Grafik) zu diesem Tutorial hier herunterladen. Unter Windows kannst du die Beispiele starten, indem du doppelt auf die Dateien Teil_1.py, Teil_2.py und Teil_3.py klickst. Funktioniert das nicht, solltest du die Installationen von Python und Pygame nochmal überprüfen.

Und jetzt?

Jetzt ist der geeignete Zeitpunkt, um einen intensiveren Blick in die Dokumentation von Pygame zu werfen. Denn Pygame kann noch deutlich mehr als das, was wir hier im Tutorial betrachtet haben. Wir haben uns aber bereits eine nette Basis für ein kleines Spiel gebastelt, auf die du jetzt aufsetzen kannst.

Also los, Spiele wollen programmiert werden!

Meine Werkzeuge
Namensräume
Varianten
Aktionen
Navigation
Werkzeuge