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

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.

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

Ein einfaches Skript zum Testen der Installation:

  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.Cock-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() == 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 nachdem ob wir ein Bild mit Alpha-Kanal haben oder nicht.
  8.     if image.get_alpha() == 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.  
  19.         image.set_colorkey(colorkey, pygame.RLEACCEL)
  20.  
  21.     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. class TileType(object):
  2.     def __init__(self, name, startX, startY, width, height):
  3.         self.__name = name
  4.         self.__rect = pygame.rect.Rect(startX, startY, width, height)
  5.  
  6.     def getName(self):
  7.         return self.__name
  8.  
  9.     def getRect(self):
  10.         return self.__rect

Wir speichern als erstes den Namen bzw. die ID des Tile-Typs in der privaten Variable __name. Über die Funktion getName können wir ihn wieder auslesen. 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. class Tileset(object):
  2.     def __init__(self, image, colorkey, tileWidth, tileHeight):
  3.         self.__image = loadImage(image, colorkey)
  4.         self.__tileWidth = tileWidth
  5.         self.__tileHeight = tileHeight
  6.  
  7.         self.__tileTypes = dict()

Um das Bild anzeigen zu können, bzw. Ausschnitte daraus, erstellen wir eine Funktion getImage, die das Bild zurückliefert. Außerdem ermöglichen wir es, das Bild gegen eine neues auszutauschen. Wenn während des Spiels so ein Tausch des Tileset-Bildes öfter vorkommen soll, ist es aber ratsamer, alle Bilder zum Beispiel in einer Liste zu speichern und nicht jedes Mal neu zu laden.

  1.     def getImage(self):
  2.         return self.__image
  3.  
  4.     def setImage(self, image, colorkey):
  5.         self.__image = loadImage(image, colorkey)

Ebenfalls sehr einfach sind die drei Funktionen, um die Größe der Tiles zu bekommen:

  1.     def getTileWidth(self):
  2.         return self.__tileWidth
  3.  
  4.     def getTileHeight(self):
  5.         return self.__tileHeight
  6.  
  7.     def getTileSize(self):
  8.         return (self.__tileWidth, self.__tileHeight)

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.     def addTile(self, name, startX, startY):
  2.         self.__tileTypes[name] = TileType.TileType(name, startX, startY, self.__tileWidth, self.__tileHeight)

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

  1.     def getTile(self, name):
  2.         try:
  3.             return self.__tileTypes[name]
  4.         except KeyError:
  5.             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. class Tilemap(object):
  2.     def __init__(self):        
  3.         self.__tileset = Tileset.Tileset("tileset.png", (255, 0, 255), 32, 32)
  4.         self.__tileset.addTile("grass", 0, 0)
  5.         self.__tileset.addTile("mud", 32, 0)
  6.         self.__tileset.addTile("water", 64, 0)
  7.         self.__tileset.addTile("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.         self.__cameraX = 0
  2.         self.__cameraY = 0
  3.  
  4.         self.__width = 10
  5.         self.__height = 10
  6.  
  7.         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.         for i in range(0, self.__width):
  2.             self.__tiles.append(list())
  3.             for j in range(0, self.__height):
  4.                 x = random.randint(0, 4)
  5.                 if x == 0:
  6.                     self.__tiles[i].append("grass")
  7.                 elif x == 1:
  8.                     self.__tiles[i].append("water")
  9.                 elif x == 2:
  10.                     self.__tiles[i].append("mud")
  11.                 else:
  12.                     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 Breite bzw. Höhe der Karte durchlaufen. Für jedes i (eine Zeile) erstellen wir eine neue Liste, in der wir die Spalten mit einem Zufallswert 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.     def render(self, screen):
  2.         for y in range(0, int(screen.get_height() / self.__tileset.getTileHeight()) + 1):
  3.             ty = y + self.__cameraY
  4.             if ty >= self.__height or ty < 0:
  5.                 continue
  6.             col = self.__tiles[ty]
  7.             for x in range(0, int(screen.get_width() / self.__tileset.getTileWidth()) + 1):
  8.                 tx = x + self.__cameraX
  9.                 if tx >= self.__width or tx < 0:
  10.                     continue
  11.                 tilename = col[tx]
  12.                 tile = self.__tileset.getTile(tilename)
  13.                 if tile is not None:
  14.                     screen.blit(self.__tileset.getImage(), (x * self.__tileset.getTileWidth(), y * self.__tileset.getTileHeight()), tile.getRect())

Wir werden die Funktion Zeile für Zeile durchgehen:

  1. Wir wollen nur so viele Tiles rendern, wie auf den Bildschirm passen. Um das herauszufinden, dividieren wir einfach die Höhe des Bildschirms durch die Höhe der Tiles. Um schwarze Rahmen zu vermeiden, addieren wir noch 1 hinzu.
  2. Die y-Position auf dem Bildschirm ist nicht gleich der Position in unserer Tile-Liste, diese Position speichern wir in der Variable ty. Um den korrekten Wert zu erhalten, müssen wir nur die Kameraposition hinzuaddieren.
  3. Wir testen, ob wir uns überhaupt noch innerhalb der Map-Grenze befinden. Wenn nicht, überspringen wir das aktuelle Tile.
  4. Die Tiles werden spaltenweise gerendert, hier speichern wir die aktuelle Spalte.
  5. In der zweiten for-Schleife durchlaufen wie die Breite des Bildschrims.
  6. Auch die x-Position wird berechnet, genau wie die y-Position.
  7. Natürlich muss auch hier getestet werden, ob wir die Grenzen der Map einhalten.
  8. Als Nächstes holen wir uns den Namen des Tiles, das wir rendern wollen, aus der aktuellen Spalte.
  9. Über das Tileset erhalten wir das Tile, oder None, wenn keines existiert.
  10. Und wenn wir ein Tile haben, wird dieses auf den Bildschrim gerendert.
  11. In der Tilemap ist das Bild mit allen Tiles gespeichert, das ist unsere Quelle für die blit-Funktion. Die Positionen multiplizieren wir noch mit der Größe der Tiles, um keine Überschneidung mit dem vorherigen Tile zu bekommen. Zuletzt der wichtigste Parameter: Der Ausschnitt, der von dem Bild verwendet werden soll, ist in der Tile-Klasse gespeichert.

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

  1.     def handleInput(self, key):
  2.         if key == pygame.K_LEFT:
  3.             self.__cameraX += 1
  4.         if key == pygame.K_RIGHT:
  5.             self.__cameraX -= 1
  6.  
  7.         if key == pygame.K_UP:
  8.             self.__cameraY += 1
  9.         if key == pygame.K_DOWN:
  10.             self.__cameraY -= 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.handleInput(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:

TileType.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4.  
  5. class TileType(object):
  6.     def __init__(self, name, startX, startY, width, height):
  7.         self.__name = name
  8.         self.__rect = pygame.rect.Rect(startX, startY, width, height)
  9.  
  10.     def getName(self):
  11.         return self.__name
  12.  
  13.     def getRect(self):
  14.         return self.__rect

Tileset.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4. import TileType
  5.  
  6. def loadImage(filename, colorkey = None):
  7.     image = pygame.image.load(filename)
  8.  
  9.     if image.get_alpha() == None:
  10.         image = image.convert()
  11.     else:
  12.         image = image.convert_alpha()
  13.  
  14.     if colorkey is not None:
  15.         if colorkey is -1:
  16.             colorkey = image.get_at((0,0))
  17.  
  18.         image.set_colorkey(colorkey, pygame.RLEACCEL)
  19.  
  20.     return image
  21.  
  22.  
  23. class Tileset(object):
  24.     def __init__(self, image, colorkey, tileWidth, tileHeight):
  25.         self.__image = loadImage(image, colorkey)
  26.         self.__tileWidth = tileWidth
  27.         self.__tileHeight = tileHeight
  28.  
  29.         self.__tileTypes = dict()
  30.  
  31.     def getImage(self):
  32.         return self.__image
  33.  
  34.     def setImage(self, image, colorkey):
  35.         self.__image = loadImage(image, colorkey)
  36.  
  37.     def getTileWidth(self):
  38.         return self.__tileWidth
  39.  
  40.     def getTileHeight(self):
  41.         return self.__tileHeight
  42.  
  43.     def getTileSize(self):
  44.         return (self.__tileWidth, self.__tileHeight)
  45.  
  46.     def addTile(self, name, startX, startY):
  47.         self.__tileTypes[name] = TileType.TileType(name, startX, startY, self.__tileWidth, self.__tileHeight)
  48.  
  49.     def getTile(self, name):
  50.         try:
  51.             return self.__tileTypes[name]
  52.         except KeyError:
  53.             return None

Tilemap.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4. import random
  5. import Tileset
  6.  
  7. class Tilemap(object):
  8.     def __init__(self):        
  9.         self.__tileset = Tileset.Tileset("tileset.png", (255, 0, 255), 32, 32)
  10.         self.__tileset.addTile("grass", 0, 0)
  11.         self.__tileset.addTile("mud", 32, 0)
  12.         self.__tileset.addTile("water", 64, 0)
  13.         self.__tileset.addTile("block", 0, 32)
  14.  
  15.         self.__cameraX = 0
  16.         self.__cameraY = 0
  17.  
  18.         self.__width = 10
  19.         self.__height = 10
  20.  
  21.         self.__tiles = list()
  22.  
  23.         for i in range(0, self.__width):
  24.             self.__tiles.append(list())
  25.             for j in range(0, self.__height):
  26.                 x = random.randint(0, 4)
  27.                 if x == 0:
  28.                     self.__tiles[i].append("grass")
  29.                 elif x == 1:
  30.                     self.__tiles[i].append("water")
  31.                 elif x == 2:
  32.                     self.__tiles[i].append("mud")
  33.                 else:
  34.                     self.__tiles[i].append("block")   
  35.  
  36.  
  37.     def render(self, screen):
  38.         for y in range(0, int(screen.get_height() / self.__tileset.getTileHeight()) + 1):
  39.             ty = y + self.__cameraY
  40.             if ty >= self.__height or ty < 0:
  41.                 continue
  42.             col = self.__tiles[ty]
  43.             for x in range(0, int(screen.get_width() / self.__tileset.getTileWidth()) + 1):
  44.                 tx = x + self.__cameraX
  45.                 if tx >= self.__width or tx < 0:
  46.                     continue
  47.                 tilename = col[tx]
  48.                 tile = self.__tileset.getTile(tilename)
  49.                 if tile is not None:
  50.                     screen.blit(self.__tileset.getImage(), (x * self.__tileset.getTileWidth(), y * self.__tileset.getTileHeight()), tile.getRect())
  51.  
  52.  
  53.     def handleInput(self, key):
  54.         if key == pygame.K_LEFT:
  55.             self.__cameraX += 1
  56.         if key == pygame.K_RIGHT:
  57.             self.__cameraX -= 1
  58.  
  59.         if key == pygame.K_UP:
  60.             self.__cameraY += 1
  61.         if key == pygame.K_DOWN:
  62.             self.__cameraY -= 1

Tutorial.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4.  
  5. if not pygame.font: print('Fehler: pygame.font Modul konnte nicht geladen werden!')
  6. if not pygame.mixer: print('Fehler: pygame.mixer Modul konnte nicht geladen werden!')
  7.  
  8. import Tilemap
  9.  
  10. def main():
  11.     pygame.init()
  12.     screen = pygame.display.set_mode((800, 600))
  13.     pygame.display.set_caption("Pygame Tutorial")
  14.     pygame.mouse.set_visible(1)
  15.     pygame.key.set_repeat(1, 30)
  16.  
  17.     clock = pygame.time.Clock()
  18.  
  19.     map = Tilemap.Tilemap()
  20.  
  21.     running = 1
  22.     while running:
  23.         clock.tick(30)
  24.  
  25.         screen.fill((0, 0, 0))
  26.  
  27.         for event in pygame.event.get():
  28.             if event.type == pygame.QUIT:
  29.                 running = 0
  30.  
  31.             if event.type == pygame.KEYDOWN:
  32.                 if event.key == pygame.K_ESCAPE:
  33.                     pygame.event.post(pygame.event.Event(pygame.QUIT))   
  34.  
  35.                 map.handleInput(event.key)
  36.  
  37.         map.render(screen)
  38.  
  39.         pygame.display.flip()
  40.  
  41.  
  42. if __name__ == '__main__':
  43.     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. class Animation(object):
  2.     def __init__(self, image, startX, startY, num, width, height, duration):
  3.         self.__image = image
  4.         self.__startX = startX
  5.         self.__startY = startY
  6.         self.__num = num
  7.         self.__width = width
  8.         self.__height = height
  9.         self.__duration = duration
  10.  
  11.         self.__time = 0
  12.         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: startX und startY. 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. def render(self, screen, pos):
  2.         screen.blit(self.__image, pos, pygame.Rect(self.__startX + (self.__width * self.__current), self.__startY, 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 __startX und __startY 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. def update(self, time = 1):
  2.         self.__time += time
  3.  
  4.         if self.__time > self.__duration:
  5.             self.__time = 0
  6.             self.__current += 1
  7.             if self.__current >= self.__num:
  8.                 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. class Player(object):
  2.     def __init__(self):
  3.         self.__anim_image_right = loadImage("tileset.png", (255, 0, 255))
  4.         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.         self.__anim_image_left = pygame.transform.flip(self.__anim_image_right, True, False)
  2.         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.         self.__posX = 10*32
  2.         self.__posY = 13*32        
  3.         self.__dir = 0
  4.         self.__walking = False

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

  1.     def handleInput(self, key):
  2.         if key == pygame.K_LEFT:
  3.             self.__posX -= 1
  4.             self.__dir = -1
  5.             self.__walking = True
  6.         if key == pygame.K_RIGHT:
  7.             self.__posX += 1
  8.             self.__dir = 1
  9.             self.__walking = True

Das ist sehr ähnlich zur handleInput-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.         if self.__dir == -1:   
  3.             if self.__walking:
  4.                 self.__anim_left.update()
  5.             self.__anim_left.render(screen, (self.__posX, self.__posY))   
  6.         else:      
  7.             if self.__walking:
  8.                 self.__anim_right.update()
  9.             self.__anim_right.render(screen, (self.__posX, self.__posY))
  10.  
  11.         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.addTile("grass-mud", 0, 64)
  2.         self.__tileset.addTile("empty", 0, 96)

Und dann passen wir die Generierung der Tilemap etwas an:

  1.         for i in range(0, self.__width):
  2.             self.__tiles.append(list())
  3.             for j in range(0, self.__height):
  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 handleInput-Methode reichen wir die Tastendrücke durch:

  1.         self.__player.handleInput(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:

Animation.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4.  
  5. class Animation(object):
  6.     def __init__(self, image, startX, startY, num, width, height, duration):
  7.         self.__image = image
  8.         self.__startX = startX
  9.         self.__startY = startY
  10.         self.__num = num
  11.         self.__width = width
  12.         self.__height = height
  13.         self.__duration = duration
  14.  
  15.         self.__time = 0
  16.         self.__current = 0
  17.  
  18.  
  19.     def update(self, time = 1):
  20.         self.__time += time
  21.  
  22.         if self.__time > self.__duration:
  23.             self.__time = 0
  24.             self.__current += 1
  25.             if self.__current >= self.__num:
  26.                 self.__current = 0
  27.  
  28.  
  29.     def render(self, screen, pos):
  30.         screen.blit(self.__image, pos, pygame.Rect(self.__startX + (self.__width * self.__current), self.__startY, self.__width, self.__height))

Player.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4. import Animation
  5. import Tileset              # für loadImage
  6.  
  7. class Player(object):
  8.     def __init__(self):
  9.         self.__anim_image_right = Tileset.loadImage("tileset.png", (255, 0, 255))
  10.         self.__anim_right = Animation.Animation(self.__anim_image_right, 32, 32, 2, 32, 64, 15)  
  11.  
  12.         self.__anim_image_left = pygame.transform.flip(self.__anim_image_right, True, False)
  13.         self.__anim_left = Animation.Animation(self.__anim_image_left, 32, 32, 2, 32, 64, 15)
  14.  
  15.         self.__posX = 10*32
  16.         self.__posY = 13*32        
  17.         self.__dir = 0
  18.         self.__walking = False
  19.  
  20.  
  21.     def render(self, screen):          
  22.         if self.__dir == -1:   
  23.             if self.__walking:
  24.                 self.__anim_left.update()
  25.             self.__anim_left.render(screen, (self.__posX, self.__posY))   
  26.         else:      
  27.             if self.__walking:
  28.                 self.__anim_right.update()
  29.             self.__anim_right.render(screen, (self.__posX, self.__posY))
  30.  
  31.         self.__walking = False
  32.  
  33.  
  34.     def handleInput(self, key):
  35.         if key == pygame.K_LEFT:
  36.             self.__posX -= 1
  37.             self.__dir = -1
  38.             self.__walking = True
  39.         if key == pygame.K_RIGHT:
  40.             self.__posX += 1
  41.             self.__dir = 1
  42.             self.__walking = True

Tilemap.py:

  1. # -*- coding: UTF-8 -*-
  2.  
  3. import pygame
  4. import random
  5. import Tileset
  6. import Player
  7.  
  8. class Tilemap(object):
  9.     def __init__(self):        
  10.         self.__tileset = Tileset.Tileset("tileset.png", (255, 0, 255), 32, 32)
  11.         self.__tileset.addTile("grass", 0, 0)
  12.         self.__tileset.addTile("mud", 32, 0)
  13.         self.__tileset.addTile("water", 64, 0)
  14.         self.__tileset.addTile("block", 0, 32)
  15.  
  16.         self.__tileset.addTile("grass-mud", 0, 64)
  17.         self.__tileset.addTile("empty", 0, 96)
  18.  
  19.         self.__cameraX = 0
  20.         self.__cameraY = 0
  21.  
  22.         self.__width = 25
  23.         self.__height = 25
  24.  
  25.         self.__tiles = list()
  26.  
  27.         for i in range(0, self.__width):
  28.             self.__tiles.append(list())
  29.             for j in range(0, self.__height):
  30.                 if i == 14:
  31.                     self.__tiles[i].append("grass") 
  32.                 elif i == 15:
  33.                     self.__tiles[i].append("grass-mud")                    
  34.                 elif i > 15:
  35.                     self.__tiles[i].append("mud")
  36.                 else:
  37.                     self.__tiles[i].append("empty")
  38.  
  39.         self.__player = Player.Player()
  40.  
  41.  
  42.     def render(self, screen):
  43.         for y in range(0, int(screen.get_height() / self.__tileset.getTileHeight()) + 1):
  44.             ty = y + self.__cameraY
  45.             if ty >= self.__height or ty < 0:
  46.                 continue
  47.             col = self.__tiles[ty]
  48.             for x in range(0, int(screen.get_width() / self.__tileset.getTileWidth()) + 1):
  49.                 tx = x + self.__cameraX
  50.                 if tx >= self.__width or tx < 0:
  51.                     continue
  52.                 tilename = col[tx]
  53.                 tile = self.__tileset.getTile(tilename)
  54.                 if tile is not None:
  55.                     screen.blit(self.__tileset.getImage(), (x * self.__tileset.getTileWidth(), y * self.__tileset.getTileHeight()), tile.getRect())
  56.  
  57.         self.__player.render(screen)
  58.  
  59.  
  60.     def handleInput(self, key):
  61.         self.__player.handleInput(key)

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