Einstieg in die Objektorientierung mit einer einfachen Planetensimulation in Pygame
https://oer-informatik.de/python_pygame_oop
tl/dr; (ca. 12 min Lesezeit): Eine Variable auf der einen Seite. Eine Funktion auf der anderen Seite. Viele Probleme lassen sich so darstellen. Manchmal ist es aber gerade die Verknüpfung von beidem (die Objektorientierung), die die Programmierung erstaunlich einfach macht. Am Beispiel einer Planetensimulation mit Hilfe von Pygame soll das Konzept der Objektorientierten Programmierung greifbar gemacht werden.
Der Ablauf einer Game-Engine und das Framework pygame
Bevor wir in die Objektorientierung abtauchen, wollen wir einen kurzen Blick auf den Aufbau einer Gameengine werfen. Ziel soll es schließlich sein, eine kleine objektorientierte Planetensimulation zu erstellen.
Gameengines nutzen häufig einen Ablauf, den man in zunächst zwei Bereiche gliedern kann: Die Initialisierung und darauf folgend eine Schleife, in der immer die gleichen Schritte wiederholt werden. Als Programmablaufplan sieht das etwa so aus:

Die einzelnen Unterprogramme haben häufig die folgenden Funktionen:
setup()
: Alle nötigen Vorarbeiten werden geleistet, um das Programm zu starten: Defaultwerte werden gesetzt und alle Operationen werden ausgeführt, die genau einmal zu Beginn erledigt werden müssen.Danach folgt die Schleife, die solange durchlaufen wird, bis das Programm beendet wird. Darin im Wesentlichen drei Arten von Operation, die sich in diesen drei Funktionen zusammenfassen lassen:
events()
: Welche Ereignisse müssen ausgewertet werden? Hier wird z.B. nach Maus- und Tastertureingaben gesucht. Nach dem E.V.A.-Modell entspricht das der Eingabe.update()
: Hierin befindet sich die eigentliche Spiellogik: Änderung von Positionen werden berechnet, Wertungen vorgenommen. Nach dem E.V.A.-Modell also die Verarbeitung.draw()
: Hier werden schließlich die einzelnen Objekte gezeichnet. Folglich ist dies die Ausgabe gemäß E.V.A.-Modell.
Das Programm selbst in der Kurzübersicht besteht aus vier aufeinanderfolgende Phasen:
Wir importieren pygame,
deklarieren global genutzte Variablen,
implementieren die nötigen Funktionen
und erstellen unten schließlich den eigentlichen Programmaufruf.
Ein einfaches Programm, dass beispielsweise einen Ball an den Rändern des Fensters abprallen lässt, könnte so aussehen:
Beispiel 1 (ohne Objekte): Ein abprallender Ball mit pygame
Ein einfaches Beispiel bildet die Grundlage vieler Spiele: Abprallen an den Rändern:

Wir benötigen als globale Variablen zusätzlich zu unserem Beispiel oben noch die Koordinaten des Balls und die Geschwindigkeit in jede Richtung:
import pygame
DISPLAY_HEIGHT = 600
DISPLAY_WIDTH = 800
is_running = True
speed_x, speed_y = 0.1, 0.1
pos_x, pos_y = 1, 1
In der setup()
-Funktion wird das pygame-Framework initialisiert und das eigentliche Spielfenster erstellt (screen
). Weil wir auf screen
auch außerhalb dieser Funktion zugreifen müssen, wird es als global
deklariert (wir werden später noch elegantere Wege kennenlernen).
def setup():
global screen
pygame.init()
screen = pygame.display.set_mode([DISPLAY_WIDTH,DISPLAY_HEIGHT])
In der Methode events()
sollen später Tastatur- und Mausereignisse überprüft werden. Vorerst langt uns, das Spiel zu beenden, wenn das Spielfenster geschlossen wird:
def events():
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
return True
Jetzt kommt die eigentliche Logik: die update()
-Methode berechnet neue Werte für unsere Koordinaten und Geschwindigkeiten - daher muss sie auf diese vier Variablen schreibend zugreifen. Wir lösen das wieder über global
. Darauf folgt die Änderung der Position um den als speed
angegebenen Wert (pos_x += speed_x
). Anschließend wird geprüft, ob der Fensterrand erreicht wurde. Falls der Ball den Rand berührt wird die Bewegungsrichtung umgekehrt (speed_x *= -1
).
def update():
global pos_x
global pos_y
global speed_x
global speed_y
pos_x += speed_x
pos_y += speed_y
if (pos_x>DISPLAY_WIDTH) or (pos_x<=0) : speed_x *= -1
if (pos_y>DISPLAY_HEIGHT) or (pos_y<=0) : speed_y *= -1
Die draw()
-Funktion zeichnet den Bildschirmhintergrund zunächst schwarz (fill()
), zeichnet dann den Ball an die aktuellen Koordinaten (.circle()
) und aktiviert schließlich das neue display (flip()
).
def draw():
screen.fill((0, 0, 0))
pygame.draw.circle(screen, (255, 255, 0), (int(pos_x), int(pos_y)), 20)
pygame.display.flip()
Das eigentliche Hauptprogramm ist unverändert zum obigen Codebeispiel:
Damit ist der Grundaufbau erstellt. Jetzt kann es daran gehen, in unser Spiel Objekte aufzunehmen.
Beispiel 2 - jetzt wird’s objektorientiert: Die pygame
-Planetensimulation
Werfen wir zunächst einen Blick auf die fertige Simulation und versuchen zu beschreiben, was wir sehen.

Das es eine einfache Planetensimulation wird, hatte ich ja bereits gesagt. Welche Eigenschaften hat der blaue Planet - die Erde? Ein paar Dinge lassen sich leicht beobachten:
Der Planet hat einen Radius.
Die Umlaufbahn des Planeten hat einen Radius.
Er bewegt sich auf der Umlaufbahn.
Er bewegt sich mit einer bestimmten Geschwindigkeit.
Er hat eine definierte Farbe.
Er hat eine definierte Form.
Was wir hier beschrieben haben ist zweierlei: Zustand auf der einen Seite und Verhalten auf der anderen Seite. Das Verhalten ändert oder nutzt jeweils einen bestimmten (zugehörigen) Zustand - move()
verändert z.B. die Position eines konkreten Planeten. Wir verknüpfen die Variablen, die den Zustand beschreiben, und die Funktionen, die einen konkreten Zustand nutzen oder anpassen in einer neuen Einheit: einer Klasse.
Eine Klasse gibt nur die Struktur vor, wie eine konkrete Instanz gebaut werden muss - im Planetenbeispiel:

Dieser Bauplan eines Planeten ist allgemein formuliert und nicht für einen spezifischen Planeten. Diesen Bauplan nennt man in der Objektorientierung Klasse. Aus Klassen lassen sich konkrete, identifizierbare Instanzen erstellen: die Planeten erde
, uranus
und mars
etwa. Diese Instanzen nennt man in der Objektorientierung Objekte. Jedes Objekt verfügt über einen eigenen Zustand (eine eigene Identität). Alle Objekte einer Klasse verfügen aber über das gleiche Verhalten (nutzen die gleichen Funktionen).
Die Variablen einer Klasse, die den Objektzustand speichern, nennt man Attribute. Sie werden in der zweiten Sektion des UML-Klassendiagramms notiert.
Die Funktionen einer Klasse, die die Attribute nutzen und ändern, nennt man Methoden. Sie werden in der untersten Sektion des UML-Klassendiagramms notiert.
Die Implementierung der Klasse Planet
Eine Instanz einer Klasse wird erstellt, in dem ich den Konstruktor aufrufe, und ihm alle gewünschten Parameter übergebe. In den meisten Programmiersprachen wird der Konstruktor aufgerufen mit einem Methodennamen, der wie die Klasse heißt. In Python wir ein neuer Planet z.B. über den folgenden aufruf instanziiert:
Natürlich müssen wir die Klasse selbst noch implementieren. Wir schreiben die relevanten Methoden der Klasse in eine eigene Suite, die wir mit class Planet
überschreiben (Klassennamen werden i.d.R. groß geschrieben):
class Planet:
def __init__(self, pygame, umlaufZeit=10, abstandZumZentrum=60, radius=10, color=(0, 0, 255)):
self.umlaufZeit = umlaufZeit
self.abstandZumZentrum = abstandZumZentrum
self.xZentrum = DISPLAY_WIDTH / 2
self.yZentrum = DISPLAY_HEIGHT / 2
self.xPosition = 0
self.yPosition = 0
self.radius = radius
self.color = color
self.startzeit = time.time()
self.pygame = pygame
def move(self): # den Planeten bewegen
# Umlaufwinkel aus Zeit und Umlaufzeit berechnen, dann die Koordinaten (Trigonomie)
alpha = math.pi * (time.time()-self.startzeit) / (self.umlaufZeit)
self.xPosition = round(self.xZentrum + self.abstandZumZentrum * (sin(alpha)))
self.yPosition = round(self.yZentrum + self.abstandZumZentrum * (cos(alpha)))
def draw(self): # den Planeten zeichnen
pygame.draw.circle(screen, self.color, (self.xPosition, self.yPosition), self.radius)
Es fällt auf, dass alle drei Methoden als ersten Parameter self
übergeben bekommen: über die self
-Refernz erhalten Methoden in Python Zugriff auf den Objektzustand. Alle Attribute (also Variablen des Objektzustands) werden über diese self
-Referenz angesprochen.
Die Methode __init__()
ist der Konstruktor der Klasse (in vielen anderen Programmiersprachen würde sie einfach Planet()
heißen). Sie setzt den Ausgangszustand und legt fest, welche Parameter bei Objekterzeugung übergeben werden müssen.
Den Rumpf des Programms
Am Programmrumpf öndert sich im Vergleich zum ersten Beispiel nicht viel. Für die Kreisbewegung brauchen wir Sinus- und Cosinusfunktionen aus der Mathe-Bibliothek:
Darauf folgt wieder die Größe des Darstellungsfensters, danach die Planetenklasse (Quelltext siehe oben):
DISPLAY_HEIGHT = 600
DISPLAY_WIDTH = 800
is_running = True
TITLE = "Planetensimulation"
class Planet:
# ... siehe oben
Ab jetzt werden nur noch Details angepasst: in der setup()
-Methode werden die Planeten erzeugt:
def setup():
global screen
pygame.init()
screen = pygame.display.set_mode([DISPLAY_WIDTH,DISPLAY_HEIGHT])
# Erzeugung eines Planeten. Die Parameter werden an Methode Planet.__init__() übergeben
global erde
erde = Planet(pygame, umlaufZeit=10, abstandZumZentrum=DISPLAY_WIDTH/5, radius=15) # Parameter umlaufZeit, umlaufbahn
Events haben wir zunächst noch gar keine weiteren - wir können also die Methode von oben übernehmen:
def events():
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
return True
In der Update-Methode muss der Planet bewegt werden:
Und in der draw()
-Methode wird die Sonne ins Zentrum und die erde an der jeweiligen Position gezeichnet:
def draw():
screen.fill((0, 0, 0))
pygame.draw.circle(screen, (255, 255, 0), (DISPLAY_WIDTH//2, DISPLAY_HEIGHT//2), 25)
erde.draw()
pygame.display.flip()
Am Ende wird alles in gewohnter Weise aufgerufen:
Wir sind nicht allein im Weltall… neue Planeten in Listen speichern
Wenn das Programm erstmal läuft ist es ein leichtes, neben der Erde noch Merkur, Venus, Mars, Jupiter, Saturn, Uranus, Neptun… um die Erde kreisen zu lassen. Natürlich muss man beim Maßstab etwas mogeln, sonst sieht man nur schwarz…

Wenn viele Planeten erstellt werden sollen ist ganz schön nervig, wenn man alle einzeln aufrufen muss bei der Initialisierung, beim Updaten und beim Zeichnen.
Das lässt sich alles enrom vereinfachen, wenn wir alle Planeten in einer Liste (planeten
) speichern, über die wir dann iterieren können. Die Liste erstellen wir zu Beginn einmal - also in der setup()
-Funktion:
def setup():
global screen
pygame.init()
screen = pygame.display.set_mode([DISPLAY_WIDTH,DISPLAY_HEIGHT])
global planeten
planeten = []
planeten.append(Planet(pygame, umlaufZeit=1, abstandZumZentrum=DISPLAY_WIDTH/8, radius=3, color=(0,120,120)))
planeten.append(Planet(pygame, umlaufZeit=2, abstandZumZentrum=DISPLAY_WIDTH/7, radius=4, color=(120,120,0)))
planeten.append(Planet(pygame, umlaufZeit=3, abstandZumZentrum=DISPLAY_WIDTH/6, radius=10, color=(0,0,255)))
planeten.append(Planet(pygame, umlaufZeit=4, abstandZumZentrum=DISPLAY_WIDTH/5, radius=5, color=(255,0,0)))
planeten.append(Planet(pygame, umlaufZeit=5, abstandZumZentrum=DISPLAY_WIDTH/4, radius=30, color=(0,255,0)))
planeten.append(Planet(pygame, umlaufZeit=8, abstandZumZentrum=DISPLAY_WIDTH/3, radius=20, color=(120,0,120)))
planeten.append(Planet(pygame, umlaufZeit=16, abstandZumZentrum=DISPLAY_WIDTH/2, radius=10, color=(200,100,100)))
Fehlt nur noch die Anpassung der update()
- und draw()
-Funktion, die jeweils die entsprechenden Methoden aller Objekte in der Liste aufrufen müssen. Und schon können wir davon profitieren, dass wir Listen haben, durch die iteriert werden kann:
def update():
for planet in planeten:
planet.move()
def draw():
screen.fill((0, 0, 0))
pygame.draw.circle(screen, (255, 255, 0), (DISPLAY_WIDTH//2, DISPLAY_HEIGHT//2), 25)
for planet in planeten:
planet.draw()
pygame.display.flip()
Kapselung der Attribute
Es ist guter Stil, nur die Attribute von Außen zugreifbar zu machen, die dort auch benötigt werden. Außerdem greift man nicht direkt auf die Attribute zu, sondern nutzt Getter- und Settermethoden. Der Weg der Wahl, dies in Python zu tun sind Properties (weitere Infos dazu finden sich hier).
Die Attribute werden in Python zur Kapselung versteckt (nur über den Klassennamen zugreifbar gemacht). Dazu muss jedem Attribut bei jedem Vorkommen ein doppelter Unterstrich vorangestellt werden. Zum Beispiel im Konstruktor sieht das so aus:
def __init__(self, pygame, umlaufZeit=10, abstandZumZentrum=60, radius=10, color=(0, 0, 255)):
self.__umlaufZeit = umlaufZeit
self.__abstandZumZentrum = abstandZumZentrum
self.__xZentrum = DISPLAY_WIDTH / 2
self.__yZentrum = DISPLAY_HEIGHT / 2
self.__xPosition = 0
self.__yPosition = 0
self.__radius = radius
self.__color = color
self.__startzeit = time.time()
self.__pygame = pygame
(in den anderen Methoden entsprechend.)
Zusätzlich müssen bei den Attributen, auf die von Außen zugegriffen werden soll, noch Properties erzeugt werden:
@property
def xPosition(self) -> int:
return self.__xPosition
@property
def yPosition(self) -> int:
return self.__xPosition
Das langt für einen lesenden Zugriff (und entspricht Getter-Methoden, ohne Setter-Methoden)
Monde erzeugen: Objektbeziehungen zwischen Planeten
Objekte können zueinander in Beziehungen stehen. Beispielsweise kann ein Planet um einen anderen kreisen (Astronomen mögen mir verzeihen, dass wir hier keine gesonderte Klasse Mond einführen). Die Klasse benötigt dazu im Konstruktor ein zusätzliches Attribut centerobject
, das als Defaultwert die Mitte des Ausgabefensters hat:
def __init__(self, pygame, umlaufZeit=10, abstandZumZentrum=60, radius=10, color=(0, 0, 255), centerobject=None):
#... alle anderen Attribut-Defaultwerte, siehe oben
self.__centerobject = centerobject
if (self.__centerobject == None):
self.__xZentrum = DISPLAY_WIDTH / 2
self.__yZentrum = DISPLAY_HEIGHT / 2
Wenn das Zentrum des jeweiligen Planeten ein anderer Planet ist, müssen dessen Koordinaten jedes Mal zur Berechnung der neuen Position ausgelesen werden.
def move(self): # den Planeten bewegen
# Umlaufwinkel aus Zeit und Umlaufzeit berechnen, dann die Koordinaten (Trigonomie)
alpha = math.pi * (time.time()-self.__startzeit) / (self.__umlaufZeit)
# --->8--- schnipp ---8<--- ab hier neu
if type(self.__centerobject)==Planet:
self.__xZentrum = self.__centerobject.xPosition
self.__yZentrum = self.__centerobject.yPosition
# --->8--- /schnipp ---8<--- bis hier neu
self.__xPosition = round(self.__xZentrum + self.__abstandZumZentrum * (-cos(alpha)))
self.__yPosition = round(self.__yZentrum + self.__abstandZumZentrum * (-sin(alpha)))
Schließlich muss noch ein neues Objekt erzeugt werden, dass um ein zweites kreist:
def setup():
global screen
pygame.init()
screen = pygame.display.set_mode([DISPLAY_WIDTH,DISPLAY_HEIGHT])
global planeten
planeten = []
erde = Planet(pygame, umlaufZeit=7, abstandZumZentrum=0.7*DISPLAY_WIDTH/2, radius=20, color=(0,0,255))
planeten.append(erde)
mond = Planet(pygame, umlaufZeit=1, abstandZumZentrum=0.1*DISPLAY_WIDTH/2, radius=7, color=(153,77,0), centerobject=erde)
planeten.append(mond)
Fazit
Mithilfe einer Planetensimulation haben wir uns die Möglichkeit angesehen, mit Pygame eine einfache graphische Ausgabe zu erstellen. Mit der Gameloop haben wir nun alles an der Hand, um uns an die Entwicklung einfacher Spiele zu wagen.
Links und weiter Informationen
Sehr lesenswert hierzu sind die Beiträge in Jörg Kantels Blog Schockwellenreiter, der in der Objektorientierung noch einen Schritt weiter geht, und auch die Oberfläche selbst zum Objekt macht (und andere interessante Artikel hat)
Die Website vom Framework PyGame
Quellen und offene Ressourcen (OER)
Die Ursprungstexte (als Markdown), Grafiken und zugrunde liegende Diagrammquelltexte finden sich (soweit möglich in weiterbearbeitbarer Form) in folgendem git-Repository:
https://gitlab.com/oer-informatik/python-basics/erste-schritte-mit-python
Sofern nicht explizit anderweitig angegeben sind sie zur Nutzung als Open Education Resource (OER) unter Namensnennung (H. Stein, oer-informatik.de) freigegeben gemäß der Creative Commons Namensnennung 4.0 International Lizenz (CC BY 4.0).
Hinweis zur Nachnutzung
Dieses Werk und dessen Inhalte sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Erste Schritte mit Python” von Hannes Stein, Lizenz: CC BY 4.0. Die Quellen dieses Werks sind verfügbar auf GitLab.