Lernziele
Es wird nicht erwartet, dass Sie unseren Code oder die verwendeten Befehle auswendig aufschreiben können. Es geht darum, die Logik unserer Lösung und die besprochene Theorie zu verstehen. Insbesondere:
- Sie verstehen unseren Code, könnten Schlüsselstellen erklären, und kurze Teile davon reproduzieren, wenn Sie alle nötigen Befehle erhalten. Spezifisch:
- Sie verstehen das Turtle-Fenster als Koordinatensystem und die Logik unserer Helfervariabeln LEFT, RIGHT, TOP, BOTTOM, GROUND.
- Sie können erklären, was das
global
Keyword in Funktionen bezweckt.- Sie verstehen, wie wir bei der Steuerung die neue X-Koordinate berechnen und wieso wir window.onkeyrelease() nutzen.
- Sie verstehen theoretische Konzepte:
- Das globale Speicherframe und dass Funktionen temporäre “lokale” Speicherframes nutzen, die wieder zerstört werden.
- Sie kennen alle primitiven Datentypen (Bools, Integers, Floats, Strings) und wissen, dass diese in Python immutable (nicht an Ort und Stelle veränderbar) sind.
Abhängigkeiten überlegen
Im Plenum
Überlegen wir uns, welche Teile von welchen anderen Teilen im Spiel abhangen. Damit ist folgende Frage gemeint: Was muss existieren, damit dieses Teil existieren kann?
Lösung
Daraus ergibt sich eine natürliche Abfolge unserer Arbeit:
- Fenster
- Raumschiff oder Turtle-Invaders
- Laser (braucht zumindest das Raumschiff)
- Punkteanzeige braucht sicher die Laser und die Turtle-Invaders um zu zählen
Fenster und Raumschiff erstellen
Diesen ersten Teil erkläre und programmiere ich in diesem Video vor.
Fenster
Sie kennen Turtle so, dass sich automatisch ein Fenster öffnet. Bei diesem Spiel brauchen wir mehr Kontrolle, deswegen kreieren wir das Fenster selbst mit turtle.Screen()
.
import turtle
# Wir kontrollieren das Fenster selbst
window = turtle.Screen()
window.setup(0.5, 0.75) # Breite und Höhe relativ zum Bildschirm
window.bgcolor("#202020") # Hintergrund in Hex-RGB
window.title("Turtle Invaders") # Titel des Fensters
# Temporär, damit es sich nicht gleich wieder schliesst
turtle.done()
Raumschiff
Jetzt bauen wir unser Raumschiff. Dazu können Sie sich ein gif-Bild im Internet suchen oder dieses Beispielbild verwenden.
Speichern Sie das Bild als
spaceship.gif
in Ihrem Projektordner ab. Wir fügen das Schiff wie folgt als Turtle ins Programm ein.
import turtle
# Wir kontrollieren das Fenster selbst
window = turtle.Screen() # Das Fenster kreieren
window.setup(0.5, 0.75) # Breite und Höhe relativ zum Bildschirm
window.bgcolor("#202020") # Hintergrund in Hex-RGB
window.title("Turtle Invaders") # Titel des Fensters
# Unser Raumschiff
ship = turtle.Turtle()
turtle.register_shape('spaceship.gif') # Das Bild laden und registrieren
ship.shape('spaceship.gif') # Das Bild der Turtle zuweisen
ship.penup() # Das Raumschiff soll nichts zeichnen
# Temporär, damit es sich nicht gleich wieder schliesst
turtle.done()
Zusatz: gif-Bilder rotieren nicht, so kreieren Sie Vektorgrafiken
Eigene Bilder werden nicht rotiert, wenn sich die Turtle dreht. Wenn Sie - anders als in diesem Spiel - Ihr Raumschiff auch rotieren wollen, müssen Sie entweder die Bilder jeweils austauschen oder eine eigene Figur mit Vektoren machen. Ich habe hier versucht, einen X-Wing aus Star Wars zu machen… 🫤
shape = ( (-10,8), (-8,8), (-8,3), (-3,5), (-1,20), (1,20), (3,5), (8,3), (8,8), (10,8), (10,1), (5,0), (4,-3), (-4,-3), (-5,0), (-10,1), ) turtle.register_shape("xwing", shape) ship.shape("xwing")
Raumschiff positionieren
Jetzt möchten wir das Raumschiff am unteren Rand positionieren, 5% der Fensterhöhe vom Rand entfernt. Versuchen Sie dazu eine gute Lösung zu finden, die auf allen Bildschirmen funktionieren würde. Hierzu diese Tipps.
- Experimentieren Sie drauf los. Man muss oft mit einem System etwas herumspielen, bis man versteht, wie es funktioniert.
ship.setposition(50, -200)
setzt das Raumschiff auf die Position x = 50 und y = -200. (Es gibt auchship.setx()
undship.sety()
.)window.window_width()
gibt Ihnen die Pixelbreite des Fensterswindow.window_height()
gibt Ihnen die Pixelhöhe des Fensters
Lösung Raumschiff-Position
Mit
ship.setx()
undship.sety()
könnten Sie herausgefunden haben, dass das Fenster ein Koordinatensystem ist.
Das hilft uns, die Ränder und den unteren Rand zu definieren.
import turtle # Wir kontrollieren das Fenster selbst window = turtle.Screen() window.setup(0.5, 0.75) # Breite und Höhe relativ zum Bildschirm window.bgcolor("#202020") # Hintergrund in Hex-RGB window.title("Turtle Invaders") # Titel des Fensters # Einige Konstanten für das Fenster, die uns hilfreich sein werden LEFT = -window.window_width() / 2 RIGHT = window.window_width() / 2 TOP = window.window_height() / 2 BOTTOM = -window.window_height() / 2 GROUND = 0.9 * BOTTOM # Unser Raumschiff ship = turtle.Turtle() turtle.register_shape('spaceship.gif') ship.shape('spaceship.gif') ship.penup() # Das Raumschiff soll nichts zeichnen ship.setposition(0, GROUND) # Das Raumschiff soll unten im Bild sein # Temporär, damit es sich nicht gleich wieder schliesst turtle.done()
Animationen kontrollieren
Sie werden merken, dass Ihr Schiff zu Beginn eine Weile braucht, bis es den unteren Rand erreicht hat. Das passiert, weil die Turtle animiert ist. Wie müssen das unterbinden.
Wir lösen das, indem wir die automatischen Animationen mit window.tracer(0)
komplett abstellen und manuell window.update()
auslösen, wenn wir ein neues Bild berechnet haben wollen.
import turtle
# Wir kontrollieren das Fenster selbst
window = turtle.Screen()
window.setup(0.5, 0.75) # Breite und Höhe relativ zum Bildschirm
window.bgcolor("#202020") # Hintergrund in Hex-RGB
window.title("Turtle Invaders") # Titel des Fensters
window.tracer(0)
# Einige Konstanten für das Fenster, die uns hilfreich sein werden
LEFT = -window.window_width() / 2
RIGHT = window.window_width() / 2
TOP = window.window_height() / 2
BOTTOM = -window.window_height() / 2
GROUND = 0.9 * BOTTOM
# Unser Raumschiff
ship = turtle.Turtle()
turtle.register_shape('spaceship.gif')
ship.shape('spaceship.gif')
ship.penup() # Das Raumschiff soll nichts zeichnen
ship.setposition(0, GROUND) # Das Raumschiff soll unten im Bild sein
# Temporär, damit es sich nicht gleich wieder schliesst
turtle.done()
running = True
while running:
window.update()
Steuerung des Raumschiffs
Diesen zweiten Teil erkläre und programmiere ich in diesem Video vor.
Unsere Art, das Fenster zu kreieren, wirft weiter Dividenden ab. Weil jetzt können wir Tastenanschläge und Klicks in diesem Fenster abfangen und bestimmen, welche Funktionen ausgeführt werden sollen. Ein Beispiel:
# Führe die Funktion move_left() aus, wenn die linke Pfeiltaste gedrückt wird.
window.onkeypress(move_left, "Left")
# Führe die Funktion stop_moving() aus, wenn die linke Pfeiltaste losgelassen wird.
window.onkeyrelease(stop_moving, "Left")
Dabei wird sich uns bei fast jeder Steuerungsart immer dasselbe Problem stellen, auf das wir jetzt kurz eingehen.
Das Spiel mit der Taste q beenden
Als Beispiel versuchen wir zu implementieren, dass das Spiel abbricht, wenn q
gedrückt wird.
def quit():
print("quit() wird ausgeführt!")
running = False
print("'running' in quit() ist", running)
# Führt die Funktion quit() aus, wenn q gedrückt wird.
window.onkeypress(quit, "q")
window.listen()
Wieso klappt das nicht? Diskutieren Sie
Das wird nicht funktionieren. Die Funktion wird zwar ausgeführt und
running
istFalse
, aber diewhile
-Schleife bricht trotzdem nicht ab. Was läuft schief?Zur Veranschaulichung habe ich Ihnen das Szenario auf Python Tutor visualisiert.
Lösung
Das
running = False
kreiert im lokalen Speicherbereich (“Frame”) der Funktion temporär kurz eine Variablerunning
, die zusammen mit dem ganzen Frame wieder gelöscht wird, wenn die Funktion ausgeführt wurde.
Um innerhalb einer Funktion auf eine globale Variable im Hauptprogramm zuzugreifen und sie zu verändern, muss man das Schlagwort global
verwenden. (Hier das geänderte Beispiel auf Python Tutor.)
import turtle
# Wir kontrollieren das Fenster selbst
window = turtle.Screen()
window.setup(0.5, 0.75) # Breite und Höhe relativ zum Bildschirm
window.bgcolor("#202020") # Hintergrund in Hex-RGB
window.title("Turtle Invaders") # Titel des Fensters
window.tracer(0)
# Einige Konstanten für das Fenster, die uns hilfreich sein werden
LEFT = -window.window_width() / 2
RIGHT = window.window_width() / 2
TOP = window.window_height() / 2
BOTTOM = -window.window_height() / 2
GROUND = 0.9 * BOTTOM
# Unser Raumschiff
ship = turtle.Turtle()
turtle.register_shape('spaceship.gif')
ship.shape('spaceship.gif')
ship.penup() # Das Raumschiff soll nichts zeichnen
ship.setposition(0, GROUND) # Das Raumschiff soll unten im Bild sein
# Steuerung
def quit():
global running
running = False
window.onkeypress(quit, "q")
window.listen()
running = True
while running:
window.update()
Links und rechts steuern
Sie sind dran
Versuchen Sie nun eine Steuerung zu bauen, um das Raumschiff links und rechts zu bewegen. Sie können das lösen, wie Sie wollen. Im Plenum werden wir dazu folgende Funktionen gebrauchen.
window.onkeypress(move_left, "Left")
führt eine Funktion aus, wenn eine Taste gedrückt wird.window.onkeyrelease(stop_moving, "Left")
führt eine Funktion aus, wenn eine Taste losgelassen wird.ship.xcor()
gibt Ihnen die aktuelle X-Koordinate des Schiffes.ship.setx(x)
verschiebt das Schiff zur X-Koordinatex
.
Mögliche erste Lösung
import turtle # Wir kontrollieren das Fenster selbst window = turtle.Screen() window.setup(0.5, 0.75) # Breite und Höhe relativ zum Bildschirm window.bgcolor("#202020") # Hintergrund in Hex-RGB window.title("Turtle Invaders") # Titel des Fensters window.tracer(0) # Einige Konstanten für das Fenster, die uns hilfreich sein werden LEFT = -window.window_width() / 2 RIGHT = window.window_width() / 2 TOP = window.window_height() / 2 BOTTOM = -window.window_height() / 2 GROUND = 0.9 * BOTTOM # Unser Raumschiff SHIP_STEP = 10 # Schrittweite für das Raumschiff ship = turtle.Turtle() turtle.register_shape('spaceship.gif') ship.shape('spaceship.gif') ship.penup() # Das Raumschiff soll nichts zeichnen ship.setposition(0, GROUND) # Das Raumschiff soll unten im Bild sein # Steuerung def quit(): global running running = False def move_left(): new_x = ship.xcor() - SHIP_STEP ship.setx(new_x) def move_right(): new_x = ship.xcor() + SHIP_STEP ship.setx(new_x) window.onkeypress(quit, "q") window.onkeypress(move_left, "Left") window.onkeypress(move_right, "Right") window.listen() running = True while running: window.update()
Probleme, die Sie sehr wahrscheinlich überwinden möchten, sind folgende:
- Wenn man nur
onkeypress
-Ereignisse nutzt und eine Taste gedrückt hält, wird das Schiff zunächst einmal bewegt und erst nach einer Verzögerung weiterbewegt. Das ist, weil eine automatische Repetitionslogik des Betriebsystems nach einer gewissen Verzögerung (z.B. 300ms) so tut, als würde die Taste immer wieder gedrückt. - Die Steuerung kann die X-Koordinate unendlich erhöhen, was dazu führt, dass ihr Schiff das Fenster verlässt.
- Die
while
-Schleife läuft sehr schnell ab. Wie bei microbit gibt es auch hier eine Schlaffunktion. Sie finden sie im Pakettime
, das sie importieren müssen, alstime.sleep(400)
.
Eine Lösung ohne diese Probleme
import turtle import time # Wir kontrollieren das Fenster selbst window = turtle.Screen() window.setup(0.5, 0.75) # Breite und Höhe relativ zum Bildschirm window.bgcolor("#202020") # Hintergrund in Hex-RGB window.title("Turtle Invaders") # Titel des Fensters window.tracer(0) # Einige Konstanten für das Fenster, die uns hilfreich sein werden LEFT = -window.window_width() / 2 RIGHT = window.window_width() / 2 TOP = window.window_height() / 2 BOTTOM = -window.window_height() / 2 GROUND = 0.9 * BOTTOM # Unser Raumschiff SHIP_STEP = 1 # Schrittweite für das Raumschiff ship_direction = 0 # -1 = nach links, 0 = halt, +1 = nach rechts ship = turtle.Turtle() turtle.register_shape('spaceship.gif') ship.shape('spaceship.gif') ship.penup() # Das Raumschiff soll nichts zeichnen ship.setposition(0, GROUND) # Das Raumschiff soll unten im Bild sein # Steuerung def quit(): global running running = False def move_left(): global ship_direction ship_direction = -1 def move_right(): global ship_direction ship_direction = 1 def stop_moving(): global ship_direction ship_direction = 0 window.onkeypress(quit, "q") window.onkeypress(move_left, "Left") window.onkeypress(move_right, "Right") window.onkeyrelease(stop_moving, "Left") window.onkeyrelease(stop_moving, "Right") window.listen() running = True while running: new_x = ship.xcor() + SHIP_STEP * ship_direction if LEFT < new_x < RIGHT: ship.setx(new_x) window.update() time.sleep(.001) # Temporäre Lösung, um die Animation zu verlangsamen
Challenge: Mögliche Erweiterungen
- Bauen Sie einen Rand ein, dass das Schiff auch auf den Extrempositionen sichtbar bleibt.
- Verfeinern Sie die Steuerung weiter: Wenn man ganz schnell zwischen links und rechts abwechselt, gibt es wieder eine Verzögerung! Können Sie das beheben?
- Wie würden Sie eine Steuerung bauen, dass Sie ihr Raumschiff auch vorwärts bewegen könnten? (Achtung dazu: Wie weiter oben erwähnt, rotiert das gif-Bild nicht, wenn Sie die Turtle drehen. Es wäre also sinnvoller, z.B. einfach
ship.shape('turtle')
zu verwenden, oder ein eigenes Raumschiff mit Vektoren zu bauen.) - Später werden wir den Code in verschiedene Dateien refaktorieren, aktuell
main.py
,window.py
undship.py
. Machen Sie eine Sicherheitskopie und versuchen Sie das mal.
Exkursion zu Datentypen
In Python sind alle primitiven Datentypen wie Boolesche Werte (bool
), Ganzzahlen (int
), Gleitkommazahlen (float
) und Zeichenketten (str
) immutable (unveränderlich). Das bedeutet, dass ihre Werte nach der Erstellung nicht mehr an Ort und Stelle verändert werden können. Wenn Sie den Wert einer dieser Datentypen ändern, wird im Speicher ein neues Objekt erstellt, und die Variable wird auf dieses neue Objekt verweisen. Deswegen mussten wir bei der Steuerung das global
Keyword verwenden: Weil wir de facto die Speicherposition des Variabel ändern.
Listen sind der einzige strukturierte Datentyp, den Sie bislang kennen. Strukturiert bedeutet, dass Listen mehrere andere Datentypen in sich tragen können. Als Analogie können Sie sich ein Bücherregal vorstellen, dass allerlei Objekte enthalten kann.
liste = ["hallo", 4, 3.14, True]
^ ^ ^ ^
str int float bool
Anders als die primitiven Datentypen sind Listen mutable (veränderbar). Das heisst: Sie können die Elemente einer Liste verändern, ohne dass die Liste im Speicher verschoben werden muss. Die Speicheradresse der Liste verändert sich nicht, sie kann an Ort und Stelle verändert werden. Für die Laser und die Turtle-Invaders wird uns das hilfreich sein.
Es gibt auch immutable strukturierte Datentypen! Die zwei Kategorien sind nicht vollends deckungsgleich. Falls Sie die
Zum Schluss noch eine kurze Demonstration. Aktivieren Sie die Linie global mein_int, mein_float, mein_string, mein_bool
, um den Unterschied zu sehen.
Unter der Kühlerhaube ist der Grund für dieses Verhalten, dass unveränderbare Datentypen jeweils an einem anderen Ort im Speicher neu geschrieben werden, wenn ihr Wert verändert wird. Das können Sie in dieser Demonstration direkt beobachten.
# Wir definieren ein Integer, ein String und eine Liste
integer = 2
string = "hallo"
liste = [integer, string]
# Wir speichern die Speicheradressen
int_addr1 = hex(id(integer))
str_addr1 = hex(id(string))
list_addr1 = hex(id(liste))
# Wir verändern alle
integer = 3
string = "halo"
liste.append("velo")
# Wir speichern die Speicheradressen erneut
int_addr2 = hex(id(integer))
str_addr2 = hex(id(string))
list_addr2 = hex(id(liste))
print()
print("Speicheradresse des Integers:")
print(int_addr1)
print(int_addr2)
print("Sind Sie gleich?", int_addr1 == int_addr2)
print()
print("Speicheradresse des Strings:")
print(str_addr1)
print(str_addr2)
print("Sind Sie gleich?", str_addr1 == str_addr2)
print()
print("Speicheradresse der Liste:")
print(list_addr1)
print(list_addr2)
print("Sind Sie gleich?", list_addr1 == list_addr2)
print()