Einstieg in die Objektorientierung mit einer einfachen Planetensimulation in Pygame

https://oer-informatik.de/python_pygame_oop

https://bildung.social/@oerinformatik/109863626275368034

tl/dr; (ca. 12 min Lesezeit): Der Aufbau des Spieleframeworks Pygame wird zunächst prozedural an einem Beispiel dargestellt. Später wird darauf eingegangen, wie die Bestandteile setup(), events(), update() und draw() objektorientiert umgesetzt werden können. Es werden unterschiedliche Objekte der Klasse Planet erzeugt und über eine Liste angesprochen. (Zuletzt geändert am 24.03.2025)

Eine Variable auf der einen Seite. Eine Funktion auf der anderen Seite. Viele Probleme lassen sich mit diesen beiden Konstruktionen in der Programmierung leicht darstellen. Manchmal ist es aber gerade die Verknüpfung von Variablen (Zustand) und Funktionen (Verhalten), die die Programmierung erstaunlich einfach macht: in Objekten bündeln wir Zustand und Verhalten. Am Beispiel einer Planetensimulation mithilfe 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:

Programmablaufplan einer Gameengine, mit den Unterprozessen init() setup() und der darauffolgenden Schleife von events() update() und draw().

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,

import pygame

deklarieren global genutzte Variablen,

 DISPLAY_HEIGHT = 600
 DISPLAY_WIDTH = 800
 is_running = True

implementieren die nötigen Funktionen

 def setup(): ...   
 def events(): ...
 def update(): ...
 def draw(): ...

und erstellen unten schließlich den eigentlichen Programmaufruf.

  setup()
  while is_running:
      is_running = events()
      update()
      draw()
  pygame.quit()

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:

Ein gelber, an den Rändern des Bilds abprallender Ball

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:

setup()

while is_running:
    is_running = events()
    update()
    draw()
pygame.quit()

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.

Planetensimulation, bei der sich ein blauer Kreis (die Erde) um einen gelben Kreis (die Sonne) dreht

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:

Planet

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:

erde = Planet(pygame, umlaufZeit=10, abstandZumZentrum=DISPLAY_WIDTH/5, radius=15)  # Parameter umlaufZeit, umlaufbahn

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:

import pygame
import math
import time
from math import sin, cos

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:

def update():
  erde.move()

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:

setup()

while is_running:
    is_running = events()
    update()
    draw()
pygame.quit()

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…

Planetensimulation, diesmal mit vielen Planeten) um einen gelben Kreis (die Sonne) dreht

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.

  • 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


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Einstieg in die Objektorientierung mit einer einfachen Planetensimulation in Pygame” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/python_pygame_oop veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/python-basics/erste-schritte-mit-python. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 24.03.2025.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: