Im heutigen Artikel wirst du in PyGame lernen, dein Spiel in Klassen aufzuteilen. Warum genau das dir das Programmieren deines Spiels enorm erleichtern wird, erfährst du in den folgenden Absätzen.
Inhaltsverzeichnis
1. Warum Klassen das PyGame Lernen erleichtern
Im letzten Beitrag haben wir eine einfache Spielersteuerung programmiert. Dazu gehört ein rotes Rechteck, das unseren Spieler darstellt und den wir mit den Pfeiltasten bewegen können. Damit haben wir schon mal eine gute Basis geschaffen, auf der sich ein Spiel aufbauen lässt.
Wenn wir uns unseren Code allerdings genauer ansehen, stellen wir fest, dass dieser ein ziemliches Durcheinander ist. Das Problem liegt darin, dass wir unser Programm bisher nicht objektorientiert geschrieben haben.
Das heißt: Wir haben den gesamten Code in eine main.py-Datei geschrieben.
Bei kleineren Programmen und Spielen funktioniert das auch noch. Wenn wir allerdings ein größeres Projekt umsetzen möchten, das beispielsweise viele verschiedene Gegnertypen enthalten soll, wir mit einer ordentlichen Kollisionsabfrage arbeiten und Items sowie ein Score System integrieren möchten, kommen wir bei einem solchen Durcheinander schnell an unsere Grenzen.
Deshalb werden wir in diesem Projekt objektorientiert arbeiten und unser Spiel in verschiedene Klassen aufteilen. Dabei wirst du in PyGame lernen, wie man eine Game-Klasse erstellt, die das Spiel an sich handelt und beispielsweise die Hauptgameplayloop enthält.
Außerdem werden wir eine separate Klasse für den Spieler anlegen.
2. Den Code aufräumen und vorbereiten
Bevor wir das Programm ordentlich in Klassen aufteilen können, räumen wir erst einmal den Code etwas auf.
Den Code, der etwas mit dem Player und der Spielersteuerung allgemein zu tun hatte, werden wir jetzt einfach mal löschen. Anschließend wirst du in Pygame lernen, das Ganze wieder neu zu programmieren.
Wir löschen also zuerst die player_x und die player_y Variablen:
Den Bereich, in dem wir die Spielersteuerung programmiert haben, entfernen wir ebenfalls:
Auch die pygame.draw.rect-Funktion, mit welcher wir den Spieler auf den Bildschirm gezeichnet haben, löschen wir:
Wir haben jetzt also wieder ein einfaches leeres PyGame Window, in welchem nach wie vor der graue Hintergrund gezeichnet wird.
3. Die Klasse "Game" in PyGame erstellen
Nun werden wir eine Klasse namens „Game“ erstellen. In dieser verwalten wir unser Spiel. Dafür bewegen wir die Maus unter den bisherigen Code und definieren die Klasse mit dem Schlüsselwort „class“. Anschließend weisen wir ihr den Namen „Game“ zu und setzen einen Doppelpunkt.
pygame.quit()
class Game:
Hier beginnen wir mit dem Konstruktor der Klasse. Das ist die Funktion, die aufgerufen wird, wenn wir ein Objekt von dieser Klasse erstellen. Das macht man in Python folgendermaßen:
class Game:
def __init__(self):
Vor und nach dem Wort "init" setzen wir jeweils zwei Unterstriche und als Parameter übergeben wir self. Darin möchten wir nun das Spiel initialisieren. Das Erste, das wir dafür tun müssen, ist pygame.init() aufzurufen:
class Game:
def__init__(self):
pygame.init()
Diesen Schritt müssen wir umsetzen, um PyGame überhaupt nutzen zu können.
Den Code darüber können wir auch noch löschen, da wir diesen ohnehin noch einmal schreiben werden:
Das heißt, darin initialisieren wir zuerst einmal PyGame. Als Nächstes werden wir unser Window, also das Fenster, in dem unser Spiel stattfindet, definieren. Dafür benötigen wir eine Breite und eine Höhe.
Wir schreiben dazu:
pygame.init()
self.window_width = 800
Das ist die Bildschirmbreite.
Für die Bildschirmhöhe schreiben wir den folgenden Code:
self.window_width = 800
self.window_height = 600
Das Fenster erstellen wir mit der folgenden Zeile:
self.window_height = 600
self.window = pygame.display.set_mode()
Darin übergeben wir ein Tupel mit der Breite und der Höhe für unser Display:
self.window = pygame.display.set_mode((self.window_width, self.window_height))
Anschließend legen wir noch den Titel "Coin Collector" für unser Fenster fest, da dieser den Namen unseres Spiels beschreibt. Das machen wir folgendermaßen:
self.window = pygame.display.set_mode((self.window_width, self.window_height))
pygame.display.set_caption("Coin Collector")
Zuvor hatten wir bereits ein Clock-Objekt, um unser Delta-Timing zu implementieren. Dieses erstellen wir nun auch wieder:
pygame.display.set_caption("Coin Collector")
self.clock = pygame.time.Clock()
Damit ist die Initialisierung erst mal abgeschlossen!
4. Die run()-Funktion
Am Ende rufen wir noch die run-Funktion auf, die wir auch gleich definieren werden:
self.clock = pygame.time.Clock()
self.run()
Diese run-Funktion enthält die Hauptloop unseres Spiels. Wir definieren sie folgendermaßen:
self.run()
def run(self):
Dieser übergeben wir auch eine Referenz auf sich selbst, also auf das Objekt, in dem diese aufgerufen wird.
Darin bauen wir jetzt wieder die Hauptschleife. Das heißt, wir setzen erst einmal running auf True und erstellen dann die while-running-Schleife. Wir überprüfen darin die verschiedenen Events, die jeden Frame registriert werden:
An dieser Stelle implementieren wir auch noch das Delta-Timing:
running = False
self.delta_time = self.clock.tick(60) / 1000
Damit haben wir wieder für jeden Frame die Delta-Time. Danach sorgen wir dafür, dass der Bildschirm mit einer grauen Farbe ausgefüllt wird:
self.delta_time = self.clock.tick(60) / 1000
self.window.fill((25, 25, 25))
Anschließend schreiben wir noch:
self.window.fill((25, 25, 25))
pygame.display.update()
Wenn wir die Schleife verlassen, möchten wir den folgenden Code ausführen:
pygame.display.update()
pygame.quit()
5. Ein Game-Objekt erstellen
Danach erstellen wir ein neues Objekt vom Typ Game, also von dieser Klasse:
pygame.quit()
game = Game()
Dabei wird der Konstruktor selbst und im Konstruktor run aufgerufen, sodass wir in der running-Schleife landen.
Wenn wir das Ganze einmal ausprobieren und ausführen, sehen wir wieder unser Fenster.
6. Eine neue Datei für den Player anlegen
Jetzt möchten wir unseren Player wieder zurückbringen. Dafür erstellen wir eine neue Python-Datei.
Wir bewegen uns zunächst links auf „coincollector“, machen darauf einen Rechtsklick, klicken dann auf „new“ und abschließend auf „python file“. Die Datei nennen wir einfach mal „Player“.
Wir haben nun also unsere player.py-Datei und importieren direkt an erster Stelle PyGame.
import pygame
7. Die Klasse "Player" erstellen
Jetzt erstellen wir eine Klasse namens „Player“, in der wir wieder einen Konstruktor benötigen. Wir schreiben dafür den folgenden Code:
import pygame
class Player:
def __init__(self, game, x, y):
Wir übergeben self als Parameter und erstellen zunächst die Eigenschaften, also Variablen unseres Player-Objekts.
Die x-Koordinate, auf der sich unser Player befindet, nennen wir self.x. Diese initialisieren wir mit einem Wert, den wir dann auch als Parameter übergeben.
Das heißt, zuerst erstellen wir die Parameterliste. Dabei übergibt das Programm self automatisch. Dann übergeben wir eine Referenz zum Game-Objekt, damit wir dort auch beispielsweise auf das Delta-Timing zugreifen können. Anschließend geben wir noch einen x- sowie einen y-Wert an, an welchem wir unseren Player im Raum spawnen lassen werden.
Wir initialisieren x also mit dem Parameter x und y mit dem Parameter y.
class Player:
def __init__(self, game, x, y):
self.x = x
self.y = y
Danach schreiben wir, dass self.game gleich dem übergebenen Game Objekt ist.
self.y = y
self.game = game
Jetzt brauchen wir noch eine Surface, auf die wir unseren Spieler zeichnen können.
Das ist ganz einfach self.surface. Wir nehmen diese von unserem Game Objekt game.window. Es handelt sich dabei um das Window, das wir innerhalb unseres Game-Objekts erstellt haben und auf welches wir unser Spiel rendern.
self.game = game
self.surface = game.window
8. Methoden für den Player anlegen
Jetzt erstellen wir weitere Methoden, die für den Player relevant sind. Zuerst einmal haben alle Spielobjekte eine Update-Methode, die jeden Frame aufgerufen wird.
def update(self):
pass
Darin können wir beispielsweise weitere Methoden aufrufen, die unser Spiel jeden Frame aufrufen soll.
Zum Beispiel eine Methode namens „Movement“, die wir folgendermaßen erstellen:
def movement(self, speed):
In das Movement schreiben wir dieselbe Logik, die wir bereits vorher programmiert hatten:
def movement(self, speed):
keys = pygame.key.get_pressed()
if keys[pygame.K_RIGHT]:
self.x += speed * self.game.delta_time
Die speed-Variable haben wir zwar noch nicht erstellt, allerdings übergeben wir diese schon mal als Parameter.
self.x += speed * self.game.delta_time
elif keys[pygame.K_LEFT]:
self.x -= speed * self.game.delta_time
Das vertikale Movement sieht wieder so aus:
if keys[pygame.K_DOWN]:
self.y += speed * self.game.delta_time
elif keys[pygame.K_UP]:
self.y -= speed * self.game.delta_time
Jetzt müssen wir diese Methode nur noch jeden Frame aufrufen. Dafür schreiben wir die folgende Zeile Code in Update und legen einen Wert von 300 fest:
def update(self):
self.movement(300)
def movement(self, speed):
Damit haben wir also unser Movement erstellt. Jetzt brauchen wir noch eine Funktion, um unser Objekt zu rendern. Wir erstellen dafür noch eine draw-Methode:
self.movement(300)
def draw(self):
Diese nimmt wieder den Parameter self an. Darin schreiben wir:
def draw(self):
pygame.draw.rect(self.surface, "red", (self.x, self.y, 64, 64))
Wir zeichnen also wieder ein rotes Rechteck auf der Surface, die wir im Konstruktor initialisiert haben. Dieses wird auf die Position self.x und self.y gesetzt. Für die Größe wählen wir eine Breite von 64 Pixeln und eine Höhe von 64 Pixeln.
Daraufhin können wir auch in Update erst mal draw aufrufen:
self.movement(300)
self.draw()
9. Den Player in PyGame importieren
Jetzt können wir uns zurück in die main.py-Datei bewegen und den Player importieren.
import pygame
import player
Damit importieren wir das Modul player.py. Das ermöglicht uns, in unserer init-Methode ein Player-Objekt zu erstellen:
self.clock = pygame.time.Clock()
self.player = player.Player
self.run()
Wir greifen also auf das Modul
player zu und möchten dort ein Objekt von der Klasse
Player erzeugen. Darin übergeben wir zunächst das Gameobjekt, welches self ist und legen für die Position, an der es gespawnt werden soll, 32, 32 fest:
self.clock = pygame.time.Clock()
self.player = player.Player(self, 32, 32)
self.run()
In der run-Methode innerhalb der Hauptschleife unseres Spiels schreiben wir, dass wir jeden Frame player.update aufrufen.
self.window.fill((25, 25, 25))
self.player.update()
pygame.display.update()
Wenn wir den ganzen Code jetzt starten, sehen wir unseren Player und können diesen sogar bewegen:
Das Schöne an der objektorientierten
Herangehensweise ist, dass wir ganz einfach weitere Objekte erzeugen können. Zum Beispiel können wir einen Player 2 bei Position 100, 100 erstellen:
self.player = player.Player(self, 32, 32)
self.player2 = player.Player(self, 100, 100)
Dann schreiben wir folgendes:
self.player.update()
self.player2.update()
Führen wir das Programm nun noch einmal aus, sehen wir zwei Spielfiguren:
Das heißt, wir können auf diese Art und Weise deutlich einfacher die einzelnen Komponenten unseres Spiels managen.
In der player.py-Datei haben wir nur die Logik für unseren Spieler, während sich in der main.py-Datei die Logik für unser Game Handling befindet. Somit haben wir mit dem objektorientierten Ansatz für deutlich mehr Übersicht gesorgt, alles logisch voneinander getrennt und damit ein besseres Softwaredesign erzielt.
Auf diese Weise können wir nun schon deutlich einfacher unsere Spiele programmieren!