**Python - Mondlandung** CoderDojo Steyr, Wolfgang Stöcher, 14.3.2023 # Idee Wir wollen unser erstes graphisches Spiel entwickeln: lande die Mondfähre sicher auf dem Mond! Nebenbei werden wir etwas über die Physik der Bewegung lernen. ![](.\lander_inAction.png) # Pygame als Basis Wir verwenden für die Programmierung die Programmiersprache Python (siehe die Python Homepage [www.python.org](https://www.python.org/)), genauer gesagt Python 3, also eine Version von Python, deren Bezeichnung mit 3 beginnt, z.B. Python 3.7.1. Für die Entwicklung unseres graphischen Spiels muss Python auf deinem Rechner installiert sein. Hilfe dazu findest du auf der [Python Homepage](https://www.python.org/about/gettingstarted/) und im Python Wiki (siehe [wiki.python.org](https://wiki.python.org/moin/BeginnersGuide/Download)). Zusätzlich brauchen wir das Python-Modul [pygame](https://www.pygame.org), das nicht standardmäßig installiert ist. Hilfe zur Installation findest du auf der Projektseite [pygame.org](https://www.pygame.org) unter [Getting Started](https://www.pygame.org/wiki/GettingStarted). Kurz zu Bildschirmkoordinaten: Bei der grafischen Darstellung auf dem Bildschirm hat es sich eingebürgert, den Ursprung des Koordinatensystems nach links oben zu setzen. Mit der ersten Koordinate geht man nach rechts, mit der zweiten Koordinate nach unten. Pygame hält sich auch an diese Konvention. Die erste Koordinate unserer Mondfähre werden wir fix auf die Mitte des Spielfensters setzen. Entlang der zweiten Koordinate, also vertikal, soll sich die Mondfähre dann bewegen. # Eine erste Minimalversion unseres Spiels Als nächstes benötigen wir noch die Dateien aus [`LunarLander_minimal.zip`](/CoDoSteyr/LunarLander_minimal.zip) (oder von [hier](https://www.steyrerbrains.at/CoDoSteyr/LunarLander_minimal.zip) oder von [hier](http://www.steyrerbrains.at/CoDoSteyr/LunarLander_minimal.zip)): entpacke das ganze Verzeichnis `LunarLander` irgendwo auf deinem Rechner, am besten in der Nähe von anderen Python-Programmen, die du schon geschrieben hast. Wenn das Doppelklicken auf `LunarLander\LunarLander_minimal.py` keine langsam sinkende Mondfähre in einem eigenen Fenster auf den Bildschirm bringt, benenne `LunarLander\startGame_minimal.bat.tmpl` auf `LunarLander\startGame_minimal.bat` um und passe, falls notwendig, den Pfad zu `python.exe` deiner Python-Installation so an, dass du `LunarLander_minimal.py` mit Hilfe von `startGame_minimal.bat` starten kannst. Die minimale Version unseres Spiels zur Mondlandung mit pygame sieht so aus: ```python import pygame FPS = 33 # Anzahl der Aktualisierungen bzw. Frames pro Sekunde COLOR_BLACK = (0, 0, 0) # Farbe als Tupel (rot, grün, blau); Werte in 0..255 SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600 # Größe des Spielfensters in Pixel LANDER_X = SCREEN_WIDTH/2 # Mondfähre horizontal zentrieren class LunarLander(pygame.sprite.Sprite): def __init__(self): # Konstruktor (Initialisierungsroutine) super().__init__() # Initialisierung der Basisklasse self.image = pygame.image.load('images/lander.png') self.rect = self.image.get_rect() # Breite und Höhe von Bild holen self.rect.topleft = (LANDER_X, 20) # Mondfähre oben mittig platzieren def update(self): self.rect.top += 1 # bewege Mondfähre um 1 Pixel nach unten def main(): screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) fpsClock = pygame.time.Clock() lander = LunarLander() # Mondfähre anlegen ... spritesAll = pygame.sprite.Group(lander) # ... und in Gruppe packen while True: if pygame.event.get(pygame.QUIT): # wurde pygame Fenster geschlossen? return # Programm beenden! if lander.rect.bottom < SCREEN_HEIGHT: # Mondfähre noch im Flug? spritesAll.update() # Mondfähre aktualisieren screen.fill(COLOR_BLACK) # schwarzer Hintergrund spritesAll.draw(screen) # Mondfähre zeichnen pygame.display.update() # Bildschirm aktualisieren fpsClock.tick(FPS) # warten, bis der nächste Frame dran ist if __name__ == '__main__': # wurde Python-Datei als Skript gestartet? main() ``` \pagebreak Für's erste brauchen wir nur das Modul `pygame`. Dann werden ein paar Konstante definiert, die den Code besser lesbar machen: * `FPS`: Die "frames per second" geben an, wie oft pro Sekunde die Position unserer Mondfähre und der Bildschirm aktualisiert werden sollen. * `COLOR_BLACK`: In `pygame` werden Farben als Triple mit den Anteilen von Rot, Grün und Blau angegeben. Die Farbanteile müssen jeweils im Zahlen bereich von 0-255 liegen. Die Farbe schwarz entsteht, wenn alle 3 Farbanteile auf 0 gesetzt werden. Weiß entsteht, wenn alle 3 Farbanteile auf das Maximum (255) gesetzt werden. * `SCREEN_WIDTH`, `SCREEN_HEIGHT`: Die Größe des Spielfensters legen wir mit 800 Pixel für die Breite und 600 Pixel für die Höhe fest. * `LANDER_X`: Die horizontale Position unserer Mondfähre setzen wir in die Mitte des Bildschirms. Als nächstes brauchen wir unsere Mondfähre. Beweglich graphische Objekte werden in Spielen gerne als "Sprites" bezeichnet. Dazu gibt es in `pygame` die Klasse `pygame.sprite.Sprite`, von der wir unsere `class LunarLander` ableiten. Zur Initialisierung unserer Mondfähre müssen wir das zugehörige Bild aus dem Unterverzeichnis `images` laden. Die Position eines Sprites im Spielfenster gibt man über das Attribut `rect` an. Wir wählen eine Position in der Mitte, 20 Pixel unterhalb des oberen Rands. Um gleich eine Bewegung zu sehen, lassen wir in jedem Aktualisierungsschritt die Mondfähre um ein Pixel sinken. Das geschieht in der Methode `update`. Für die Initialisierung des Spiels muss der Bildschirm (`screen`) angelegt werden. Und wir legen uns auch gleich ein Uhren-Objekt `fpsClock` an, mit dem wir die Geschwindigkeit des Spiels steuern können. Dann können wir schon ein Objekt `lander` für unsere Mondfähre anlegen. Um sie auf den Bildschirm zeichnen zu können, muss sie noch einer Sprite-Gruppe zugeordnet werden. Dann kann die Hauptschleife für unser Spiel schon beginnen. Zuerst fragen wir ab, ob das Fenster geschlossen wurde, und beenden in diesem Fall das Programm. (Wenn man das weglässt, erlaubt `pygame` kein einfaches Beenden des Spiels und das Programm müsste über den Taskmanager abgeschossen werden.) Dann aktualisieren wir die Position der Mondfähre, aber nur solange sie nicht den unteren Bildschirmrand erreicht hat. Jetzt kommt das Zeichnen: * `screen.fill(COLOR_BLACK)`: Der Hintergrund wird jedes Mal auf schwarz gesetzt. * `spritesAll.draw(screen)`: Die Sprite-Gruppe, die die Mondfähre enthält, wird gezeichnet. * `pygame.display.update()`: Dann kann der Bildschirm aktualisiert werden. Zum Abschluss der Hauptschleife lassen wir die Ausführung so lange verzögern, dass wir die gewünschte Spielschnelligkeit (33 Aktualisierungsschritte pro Sekunde) erreichen; das übernimmt `fpsClock.tick` für uns. Würden wir diese Zeile weglassen, würde die Mondfähre viel schneller fallen (je nach Rechnergeschwindigkeit). Für die kommenden Erweiterungen unseres Spiels zur Mondlandung kannst du eine Kopie von `LunarLander_minimal.py` unter dem Namen `LunarLander.py` anlegen und in dieser Datei weiterentwickeln. \pagebreak # Die Physik der Beschleunigung und des freien Falls Galileo Galilei hat mit seinen Fallexperimenten erste Messwerte für die Beschleunigung im freien Fall geliefert und dabei festgestellt, dass die Beschleunigung im Schwerefeld unabhängig von der Masse ist - wobei man auf der Erde den Luftwiderstand ausblenden muss, auf dem Mond ist dieser ohnehin nicht vorhanden. Isaac Newton hat für Bewegungen unter Krafteinwirkung bzw. beschleunigte Bewegungen ein einfaches Rechenmodell entwickelt. Wenn wir die Rotation, die wir hier nicht brauchen, weglassen, wird der Bewegungszustand eines Körpers durch seine Position und seine Geschwindigkeit festgelegt, die in jedem Aktualisierungsschritt so berechnet werden: ----- Newtons Rechenmodell für Bewegungen unter Krafteinwirkung 1. **Bestimme alle Kräfte, die auf einen Körper einwirken und addiere diese**. Auf unsere Mondfähre wirkt zuerst nur die Anziehungskraft des Mondes, später kommt noch die Schubkraft der Mondfähre hinzu, wenn sie aktiviert wird.
$F = F_{Mondanziehung} + F_{Schub}$
Beachte, dass Kräfte eine Richtung haben. Wir nehmen an, dass die Mondanziehungskraft, die nach unten wirkt, positiv ist. Die Schubkraft wirkt nach oben und soll bei uns negativ sein. 1. **Die Beschleunigung ist proportional zur Kraft, und zwar mit der Masse als Faktor**:
$F = m \cdot a$ bzw. $a = F/m$
Da wir keine exakte Simulation durchführen, können wir die Masse $m$ als 1 annehmen und statt der Kräfte gleich direkt mit den Beschleunigungen rechnen:
$a = a_{Mondanziehung} - a_{Schub}$
Hier haben wir das Vorzeichen aus $a_{Schub}$ (das bei uns im Gegensatz zu $F_{Schub}$ jetzt positiv sein soll) herausgezogen, daher kommt das Minuszeichen. 1. **Die Beschleunigung ändert die Geschwindigkeit mit der Zeit**:
$v_{neu} = v_{alt} + a \cdot \Delta t$
($\Delta t$, sprich "delta t", ist die Zeitdifferenz $t_{neu} - t_{alt}$) Wir wählen kleine Zeitintervalle und nehmen während dieser die Beschleunigung als jeweils konstant an. In einem Programm schreibt sich das dann z.B. so:
`v += a*dt` 1. **Die Geschwindigkeit ändert die Position mit der Zeit**:
$p_{neu} = p_{alt} + v \cdot \Delta t$
Wir wählen hier die gleichen kleinen Zeitintervalle und nehmen während dieser auch die Geschwindigkeit als jeweils konstant an. In einem Programm schreibt sich das dann z.B. so:
`p += v*dt` ----- \pagebreak Die aktuelle Zeit in Sekunden (seit 1.1.1970, 0:00 Uhr) bekommt man in python mit `time.time()`. Um die Zeitdifferenz seit der letzten Aktualisierung berechnen zu können, merken wir uns den vorigen Aktualisierungszeitpunkt im Attribut `timeOfLastPhysicsUpdate`. Damit können wir unsere Klasse `LunarLander` mit folgender Methode ausstatten: ```python def physics_update(self): timeNow = time.time() dt = timeNow - self.timeOfLastPhysicsUpdate acceleration = ACCELERATION_MOON self.velocity += dt*acceleration self.posY += dt*self.velocity self.timeOfLastPhysicsUpdate = timeNow ``` Das Attribut `timeOfLastPhysicsUpdate` ist beim ersten Aufruf noch nicht angelegt. Wir könnten jetzt überlegen, wann wir das Attribut am besten anlegen (bei der Initialisierung ist es noch zu bald, da zu diesem Zeitpunkt das Spiel und damit die Beschleunigung noch nicht begonnen hat). Einfacher ist es aber, die Ausführung von Python "krachen" zu lassen (die Berechnung von `dt` wirft dann eine ["Ausnahme" (engl. exception)](https://docs.python.org/3/tutorial/errors.html) vom Typ `AttributeError`), und diese Ausnahme dann mittels des Python-Konstrukts `try` - `except` abzufangen: ```python def physics_update(self): timeNow = time.time() try: dt = timeNow - self.timeOfLastPhysicsUpdate acceleration = ACCELERATION_MOON self.velocity += dt*acceleration self.posY += dt*self.velocity except AttributeError: # erster Aufruf: timeOfLastPhysicsUpdate unbekannt pass # fehlendes Attribut timeOfLastPhysicsUpdate ignorieren self.timeOfLastPhysicsUpdate = timeNow ``` Das Holen der aktuellen Zeit und Speichern im Attribut `timeOfLastPhysicsUpdate` wird auf jeden Fall gemacht. Die Neuberechnung der Attribute `velocity` und `posY` erfolgt nur, wenn `timeOfLastPhysicsUpdate` schon definiert ist. Falls nicht, wird die Python-Anweisung `pass` ausgeführt, die nur ein Platzhalter für "nichts tun" ist. Weiters müssen wir noch das Modul `time` importieren und eine zusätzliche Konstante für die Graviationsbeschleunigung des Mondes anlegen. Auf einen exakten physikalischen Wert verzichten wir hier, wir wählen einfach einen Wert, der das Spiel interessant macht (experimentiere damit!): ```python import time # [...] ACCELERATION_MOON = 30 ``` Und die neuen Attribute unserer Klasse `LunarLander` für die beschleunigte Bewegung müssen wir bei der Initialisierung noch anlegen: ```python def __init__(self): # [...] self.velocity = 0 self.posY = 20 ``` \pagebreak Außerdem muss die `update`-Methode angepasst werden: ```python def update(self): self.physics_update() self.rect.top = self.posY ``` Jetzt bewegt sich unsere Mondfähre schon beschleunigt nach unten! # Interaktion - jetzt wird es zum Spielen Bisher haben wir eine Simulation, wie die Mondfähre im freien Fall auf dem Mond aufschlägt. Jetzt wollen wir das Bremsen einbauen, um sanft landen zu können. Dazu müssen wir zuerst die Klasse `LunarLander` um zwei Attribute ergänzen: * `isBoosted`: Dieses Attribut sagt uns, ob die Mondfähre gerade gebremst werden soll. * `enginePower`: Dieses Attribut sagt uns, wie stark die Bremswirkung ist. Beachte, dass die Stärke des Bremsschubs stärker als die Anziehung des Mondes sein muss, um wieder langsamer werden zu können. Die Initialisierungsroutine der Klasse `LunarLander` können wir somit wie folgt erweitern: ```python def __init__(self): # [...] self.enginePower = 2.5*ACCELERATION_MOON self.isBoosted = False ``` Jetzt müssen wir dem Spieler noch eine Möglichkeit geben, das Bremsen z.B. über einen Tastendruck zu aktivieren. Den Status aller Tasten der Tastatur kann man in `pygame` mit `pygame.key.get_pressed()` holen. Will man wissen, ob eine einzelne Taste gedrückt ist, muss man sich den Status dieser Taste über ihren Index heraussuchen. Dafür gibt es in `pygame` vor-definierte Konstante, z.B. `pygame.K_SPACE` für die Leertaste. Status `0` bedeutet, dass die Taste nicht gedrückt ist. Um also die Mondfähre in den "Boost"-Modus zu bringen, brauchen wir nur ```python lander.isBoosted = pygame.key.get_pressed()[pygame.K_SPACE] > 0 ``` unmittelbar vor `spritesAll.update()` einzufügen. Damit die Mondfähre im "Boost"-Modus auch wirklich abgebremst wird, müssen wir die Bremswirkung in der Beschleunigung berücksichtigen, indem wir folgende Zeilen ```python if self.isBoosted: acceleration -= self.enginePower ``` vor der Geschwindigkeitserhöhung (`self.velocity +=` ...) ergänzen. Damit haben wir unser erstes Arcade-Game in Python geschrieben und du kannst versuchen, möglichst sanft zu landen! \pagebreak # Erweiterungsmöglichkeiten Willst du das Programm noch ausbauen? Folgende Ideen dazu: * Prüfe die sanfte Landung: wenn die Mondfähre den Boden erreicht hat, muss die Geschwindigkeit (`self.velocity`, wenn innerhalb einer Methode, bzw. `lander.velocity`, wenn in `main()` abgefragt wird) unterhalb einer gewissen Grenze sein. Als graphische Rückmeldung könnte man bei zu hoher Geschwindigkeit für's erste die Mondfähre verschwinden lassen (`lander.rect.top` in `main()` auf `SCREEN_HEIGHT` setzen - so verschwindet die Mondfähre außerhalb, d.h. unterhalb, des Spielfensters). * Zeichne die Mondfähre mit Flamme (siehe `lunarlander\images\lander_flame.png`), wenn gerade gebremst wird. Dazu lädt man am besten beide Bilder zu Beginn in verschiedene Variable und setzt dann `self.image` (bzw. `lander.image`, wenn in `main()` programmiert) pro Aktualisierungsschritt je nachdem, ob gebremst wird oder nicht. * Führe eine Beschränkung für den vorhandenen Treibstoff ein (in `__init__` die Initialisierung `self.fuel = 10` hinzufügen). Bei jedem Aktualisierungsschritt mit Bremsen wird Treibstoff verbrannt (`self.fuel -= dt` in der Methode `physics_update` an der richtigen Stelle hinzufügen). Bremsen soll nur noch möglich sein, wenn Treibstoff vorhanden ist (am besten `lander.fuel > 0` in `main()` passend abfragen). # Übersetzungen Programme werden weltweit praktisch nur noch in Englisch geschrieben. Englisch hat z.B. den Vorteil, dass es mit den 26 Buchstaben auskommt, die in den meisten Programmiersprachen für Variablen, etc. erlaubt sind (während man die deutschen Umlaute z.B. umschreiben muss). Außerdem sind englische Wörter oft kürzer und allgemein bekannt. Daher ist auch der Code hier englisch geschrieben. Ein paar vielleicht nicht so geläufige Wörter sollen hier kurz übersetzt werden. English | Deutsch --------|--------- acceleration | Beschleunigung boost | anheben engine | Motor, Triebwerk flame | Flamme force | Kraft (daher $F$ in Formeln) fuel | Treibstoff key | Schlüssel, Taste lander | Landemodul lunar | zum Mond gehörig, Mond- rectangle | Rechteck velocity | Geschwindigkeit