Was du hier lernst
- Physik-Simulation
- Geschwindigkeit und Beschleunigung
- Kollisionserkennung mit Rechtecken
- Tastatur-Ereignisse
- Koordinatensysteme umrechnen
- Dateien lesen
So funktioniert die Mondlandung
Im Buch ist dieses Spiel die erste Hälfte des Kapitels »Evolution im Computer«: Erst landest du selbst, später übernimmt ein genetischer Algorithmus das Steuer. Die Spielwelt ist bewusst schlicht – eine Fähre von 20 × 30 Pixeln und eine Landefläche von 600 × 30 Pixeln. Das Buch nennt das augenzwinkernd Suprematismus: »Zwei rote Rechtecke auf schwarzem Grund«. Spannend wird es durch die Physik.
1. Ein bisschen Physik
Der Mond hat keine Luft, also gibt es keine Reibung – nur die Schwerkraft zieht. Die Fähre merkt sich ihre Höhe und ihre Geschwindigkeit, und calculateDelta rechnet beides für den nächsten Moment aus:
def calculateDelta(acceleration, velocity, position):
t = 1.0 / fps
h = 0.5 * acceleration * t**2 + velocity * t + position
v = acceleration + velocity
return v, h
t ist die Zeit zwischen zwei Bildern, bei 80 Bildern pro Sekunde also 1/80 Sekunde. Die Beschleunigung verändert die Geschwindigkeit, die Geschwindigkeit verändert die Höhe. Der Wert −1,635 ist die Fallbeschleunigung auf dem Mond, etwa ein Sechstel der Erdanziehung.
Schub als umgedrehte Schwerkraft
Das Buch vereinfacht die Bremsrakete geschickt: Statt Kräfte zu addieren, wird die Schwerkraft einfach ersetzt. Solange die Rakete brennt, gilt gravity = 2.0 statt −1,635 – die Fähre wird nach oben beschleunigt. Außerdem wächst die Geschwindigkeit pro Bild um die volle Beschleunigung, nicht um Beschleunigung mal Zeit. Die Zahlen sind deshalb Spiel-Einheiten, keine Meter pro Sekunde. Für ein Spiel zählt nur, dass es sich richtig anfühlt.
2. Die Steuerung: ein Befehl zur Zeit
handleEvent übersetzt Tastendrücke in einen Befehl – ein Tupel, das eine Richtung beschreibt. Lässt du eine Taste los, gilt wieder (0, 0):
if event.type == KEYDOWN:
if event.key == K_RIGHT:
command = (+1, 0)
if event.key == K_DOWN:
command = (0, +1)
if event.type == KEYUP:
command = (0, 0)
In der Hauptschleife wird aus dem Befehl eine Beschleunigung: (0, +1) zündet die Bremsrakete, (+1, 0) und (-1, 0) die Steuerdüsen. Für die Seite benutzt das Programm dieselbe Funktion calculateDelta noch einmal, nur mit eigenen Variablen. Weil die Fähre mit einer zufälligen Seitwärtsdrift startet (randint(5, 20)), musst du jedes Mal etwas anders gegensteuern.
3. Kollision zweier Rechtecke
Wann berührt die Fähre den Boden? collisionDetection vergleicht die Kanten der beiden Rechtecke – erst waagerecht, dann senkrecht:
if (rx1 >= gx1 and rx1 <= gx2) or (rx2 >= gx1 and rx2 <= gx2):
if (ry1 >= gy1 and ry1 <= gy2) or (ry2 >= gy1 and ry2 <= gy2):
return True
Liegt eine der beiden senkrechten Kanten der Fähre über der Landefläche und überlappen sich auch die Höhen, haben sich die Körper berührt. Für zwei Rechtecke genügt das. Sehr schnelle Objekte könnten zwischen zwei Bildern durch ein dünnes Hindernis hindurchspringen, doch dafür ist die Fähre zu langsam.
4. Die Landeregeln
Ob die Berührung eine Landung oder ein Absturz ist, entscheidet checkRules. Die Grenzwerte stehen als benannte Konstanten ganz oben im Programm – das empfiehlt das Buch selbst, denn eine nackte 12 im Code verrät nicht, was sie bedeutet:
if velocity < maxVelocity and sideVelocity <= maxSideVelocity:
return True
Die Anzeige oben links benutzt dieselben Konstanten und färbt die Werte grün, sobald sie zum Landen passen.
5. Oben ist, wo y groß ist
Im Buch zeichnet OpenGL, und dort zeigt die y-Achse nach oben – wie in der Schule. Die Leinwand im Browser zählt dagegen von oben nach unten. toScreen rechnet um, und drawRect nimmt als obere Ecke eines Rechtecks deshalb y + height:
def toScreen(x, y):
return x + 10, fieldHeight - y
So kann die gesamte Physik unverändert bleiben: Eine größere Höhe bedeutet weiter oben, genau wie im Original.
6. Gewonnen oder verloren
Nach der Landung zeigt showASCIIArt den Text aus »YouWon.txt« oder »GameOver.txt«. Jedes »X« in der Textdatei wird zu einem Klötzchen von 10 Pixeln. Weil y nach oben zählt, beginnt die Zeilennummer bei 30 und wird pro Zeile kleiner. Danach startet restartGame eine neue Runde mit neuer Seitwärtsdrift.
So gelingt die Landung
Lass die Fähre zuerst fallen und korrigiere nur die Seitwärtsdrift, bis »Seite« grün ist. Kurz über dem Boden bremst du dann kräftig. Wer zu früh bremst, schwebt ewig – bei den erlaubten Geschwindigkeiten ist der letzte Meter sehr langsam.
Probier mal
Jede Variante ändert den Code oben. Ein Klick auf »Ausprobieren« übernimmt die Änderung in den Editor und startet das Programm. »Zurücksetzen« holt das Original zurück.
-
01Anfängermodus
Die Grenzwerte für eine gelungene Landung stehen als Konstanten ganz oben. Mit großzügigeren Werten wird aus der Zitterpartie eine entspannte Landung. Die Anzeige passt sich automatisch an, weil sie dieselben Konstanten benutzt.
- maxVelocity = 12.0 + maxVelocity = 25.0 - maxSideVelocity = 2.0 + maxSideVelocity = 5.0 -
02Stärkere Bremsrakete
Im Buch ersetzt der Schub einfach die Schwerkraft: Statt −1,635 wirken beim Bremsen +2,0. Verdoppelst du den Wert, bremst die Fähre schneller – aber jede Korrektur fällt auch heftiger aus. Zu stark ist genauso schwer zu steuern wie zu schwach.
- gravity = 2.0 + gravity = 4.0 -
03Landung auf der Erde
Auf dem Mond ist die Schwerkraft etwa sechsmal schwächer als auf der Erde – daher der Wert 1,635. Mit 9,81 fällt die Fähre wie ein Stein, und die Bremsrakete muss entsprechend stärker sein. Viel Glück!
- gravity = -1.635 + gravity = -9.81 - gravity = 2.0 + gravity = 12.0 -
04Mehrere Düsen gleichzeitig
Im Buch merkt sich
handleEventnur den letzten Befehl: Wer bremst, kann nicht gleichzeitig zur Seite steuern. Fragst du stattdessen mitscreen.pressed()ab, welche Tasten gerade gedrückt sind, arbeiten alle Düsen zusammen – so steuert es sich viel natürlicher.- if command != (0, 0): - if command == (0, +1): - gravity = 2.0 - if command == (1, 0): - sideAcceleration = +0.5 - if command == (-1, 0): - sideAcceleration = -0.5 + # Tastenzustand statt »letzter Befehl«: alle Düsen gleichzeitig + if screen.pressed(K_DOWN) or screen.pressed(K_UP): + gravity = 2.0 + if screen.pressed(K_RIGHT): + sideAcceleration = +0.5 + if screen.pressed(K_LEFT): + sideAcceleration = -0.5
Vom Buch in den Browser
Im Buch zeichnet das Spiel mit OpenGL in ein pygame-Fenster, geschrieben für Python 2.6. Im Browser zeichnet es mit c4f, der kleinen Zeichen-API dieser Website. Physik, Kollision, Regeln und Ablauf sind die aus dem Buch.
| Im Buch (pygame + OpenGL) | Hier (c4f) |
|---|---|
glOrtho(-10, 800, -10, 600, …), y zeigt nach oben |
Screen(810, 610) und toScreen(x, y) |
glBegin(GL_QUADS) mit vier glVertex3f |
screen.rect(x, y, breite, höhe, farbe) |
pygame.event.poll() (ein Ereignis pro Bild) |
screen.events() (alle Ereignisse seit dem letzten Bild) |
pygame.display.flip() + pygame.time.delay(1000 / fps) |
await screen.frame(fps) |
pygame.time.delay(3000) |
await sleep(3) |
pygame.event.clear() |
screen.events() – die Ereignisse werden abgeholt und verworfen |
Weil der Browser zwischen zwei Bildern kurz die Kontrolle zurückbekommen muss, sind main() und die Funktionen mit Wartezeiten jetzt async, und await main() startet das Spiel.
Die gespiegelte y-Achse. glOrtho legte den Nullpunkt nach unten links, die Leinwand hat ihn oben links. Statt die ganze Physik umzuschreiben, rechnet nur das Zeichnen um: toScreen spiegelt die Höhe, und ein Rechteck beginnt oben bei y + height.
Steuerung. Hier beschleunigt ▶ die Fähre nach rechts. In der Originaldatei zum Buch waren die Vorzeichen andersherum: Dort feuerte ▶ die rechte Düse, und die schob die Fähre nach links – physikalisch korrekt, aber beim Spielen verwirrend. Die Taste ▲ war im Buch ohne Funktion und zündet jetzt ebenfalls die Bremsrakete, weil viele Spieler sie zuerst probieren. Für Handys gibt es Touch-Tasten unter dem Bild.
Neu hinzugekommen sind die Anzeige für Geschwindigkeit und Höhe, kleine Flammen an den aktiven Düsen, ein Sternenhimmel, ein kurzer Starthinweis und ein Ende der Runde, wenn die Fähre an der Landefläche vorbeifällt – im Buch fiel sie dann endlos weiter. Die ASCII-Art erscheint mittig statt unten links, und die Landeregeln benutzen benannte Konstanten.
Aufgeräumt: def resize((width, height)) – das Auspacken von Tupeln in der Parameterliste – gibt es in Python 3 nicht mehr; die Funktion entfällt ohnehin. Die ungenutzten Variablen gameCycle und direction stammen noch aus dem Snake-Kapitel und sind entfernt. print ist eine Funktion.
Zum Vergleich kannst du unten auch die erste Fassung aufklappen: LunarLander_v1.py kennt nur den freien Fall und die Bremsrakete.
Original aus dem Buch ansehen LunarLander_v2.py · Python 2
'''
Created on 25.12.2009
@author: Lars Heppert
'''
from OpenGL.GL import *
from OpenGL.GLU import *
from random import random, randint
import pygame, math, datetime
from pygame.locals import *
fieldWidth = 800
fieldHeight = 600
fps = 80
gameCycle = 0
direction = (0, 0)
command = (0, 0)
velocity = 0
sideVelocity = randint(5, 20)
height = 400
xPos = 200
def resize((width, height)):
if height == 0:
height = 1
glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(-10.0, fieldWidth, -10.0, fieldHeight, -6.0, 0.0)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
def init():
glClearColor(0.0, 0.0, 0.0, 0.0)
def clearScreen():
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
glTranslatef(0.0, 0.0, 3.0)
def drawRect(dimension):
x, y, width, height = dimension
glColor3f(1.0, 0.0, 0.0)
glBegin(GL_QUADS)
glVertex3f(x, y, 0.0)
glVertex3f(x + width, y, 0.0)
glVertex3f(x + width, y + height, 0.0)
glVertex3f(x, y + height, 0.0)
glEnd()
def calculateDelta(acceleration, velocity, position):
t = 1.0/fps
h = 0.5*acceleration*t**2 + velocity*t + position
v = acceleration + velocity
return v, h
def collisionDetection(rocket, ground):
rx1, ry1, width, height = rocket
rx2, ry2 = rx1 + width, ry1 + height
gx1, gy1, width, height = ground
gx2, gy2 = gx1 + width, gy1 + height
if (rx1 >= gx1 and rx1 <= gx2) or (rx2 >= gx1 and rx2 <= gx2):
# horizontal betrachtet befinden wir uns in Bodennaehe
if (ry1 >= gy1 and ry1 <= gy2) or (ry2 >= gy1 and ry2 <= gy2):
# die Koerper ueberdecken sich auch vertikal
return True
return False
def checkRules(velocity, sideVelocity):
velocity = abs(velocity)
sideVelocity = abs(sideVelocity)
print velocity, sideVelocity
if velocity < 12.0 and sideVelocity <= 2.0:
return True
else:
return False
def drawElement(element, color):
r, g, b = color
glColor3f(r, g, b)
glBegin(GL_QUADS)
for part in element:
x, y = part
x = x * 10.0
y = y * 10.0
glVertex3f(x, y, 0.0)
glVertex3f(9.0 + x, y, 0.0)
glVertex3f(9.0 + x, 9.0 + y, 0.0)
glVertex3f(x, 9.0 + y, 0.0)
glEnd()
def showASCIIArt(fileName, delay):
datei = open(fileName, "r")
asciiArt = []
y = 30
for zeile in datei:
x = -1
y -= 1
for zeichen in zeile:
if zeichen != '\n':
x += 1
if zeichen == 'X':
asciiArt.append((x, y))
clearScreen()
drawElement(asciiArt, (1.0, 0.0, 0.0))
pygame.display.flip()
pygame.time.delay(delay)
def showGameOver():
showASCIIArt("GameOver.txt", 3000)
restartGame()
def showYouWon():
showASCIIArt("YouWon.txt", 5000)
restartGame()
def restartGame():
# reinitialize the game (restart)
global direction, command, gameCycle, velocity, sideVelocity, height, xPos
gameCycle = 0
direction = (0, 0)
command = (0, 0)
velocity = 0
sideVelocity = randint(5, 20)
height = 400
xPos = 200
pygame.event.clear()
def handleEvent(event):
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
return False
global command
if event.type == KEYDOWN:
if event.key == K_RIGHT:
command = ( +1, 0)
if event.key == K_LEFT:
command = ( -1, 0)
if event.key == K_UP:
command = ( 0, -1)
if event.key == K_DOWN:
command = ( 0, +1)
if event.type == KEYUP:
command = (0, 0)
return True
def main():
pygame.init()
video_flags = OPENGL | HWSURFACE | DOUBLEBUF
screenSize = (fieldWidth, fieldHeight)
pygame.display.set_mode(screenSize, video_flags)
resize(screenSize)
init()
ground = 100, 50, 600, 30
global velocity, sideVelocity, height, xPos
while True:
gravity = -1.635
sideAcceleration = +0.0
if not handleEvent(pygame.event.poll()):
break
global command
if command != (0, 0):
if command == ( 0, +1):
gravity = 2.0
if command == ( 1, 0):
sideAcceleration = -0.5
if command == (-1, 0):
sideAcceleration = +0.5
velocity, height = calculateDelta(gravity, velocity, height)
sideVelocity, xPos = calculateDelta(sideAcceleration, sideVelocity, xPos)
clearScreen()
rocket = xPos, height, 20, 30
drawRect(rocket)
drawRect(ground)
if collisionDetection(rocket, ground):
if checkRules(velocity, sideVelocity):
showYouWon()
else:
showGameOver()
pygame.display.flip()
pygame.time.delay(int(1000.0/fps))
if __name__ == '__main__':
main()
Original aus dem Buch ansehen LunarLander_v1.py · Python 2
'''
Created on 25.12.2009
@author: Lars Heppert
'''
from OpenGL.GL import *
from OpenGL.GLU import *
import pygame, math, datetime
from pygame.locals import *
fieldWidth = 800
fieldHeight = 600
fps = 80
gameCycle = 0
direction = (0, 0)
command = (0, 0)
def resize((width, height)):
if height == 0:
height = 1
glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(-10.0, fieldWidth, -10.0, fieldHeight, -6.0, 0.0)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
def init():
glClearColor(0.0, 0.0, 0.0, 0.0)
def clearScreen():
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
glTranslatef(0.0, 0.0, 3.0)
def drawRect(x, y, width, height):
glColor3f(1.0, 0.0, 0.0)
glBegin(GL_QUADS)
glVertex3f(x, y, 0.0)
glVertex3f(x + width, y, 0.0)
glVertex3f(x + width, y + height, 0.0)
glVertex3f(x, y + height, 0.0)
glEnd()
def drawRocket(x, y):
width = 20.0
height = 30.0
drawRect(x, y, width, height)
def drawGround(x, y):
width = 600.0
height = 30.0
drawRect(x, y, width, height)
def calculateDelta(gravity, velocity, height):
print gravity
t = 1.0/fps
h = -0.5*gravity*t**2 + velocity*t + height
v = -gravity + velocity
return v, h
def handleEvent(event):
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
return False
global command
if event.type == KEYDOWN:
if event.key == K_RIGHT:
command = ( +1, 0)
if event.key == K_LEFT:
command = ( -1, 0)
if event.key == K_UP:
command = ( 0, -1)
if event.key == K_DOWN:
command = ( 0, +1)
if event.type == KEYUP:
command = (0, 0)
return True
def main():
pygame.init()
video_flags = OPENGL | HWSURFACE | DOUBLEBUF
screenSize = (fieldWidth, fieldHeight)
pygame.display.set_mode(screenSize, video_flags)
resize(screenSize)
init()
velocity = 0
height = 400
while True:
gravity = 1.635
if not handleEvent(pygame.event.poll()):
break
global command
if command != (0, 0):
if command == (0, +1):
gravity = -2.0
velocity, height = calculateDelta(gravity, velocity, height)
clearScreen()
drawRocket(200, height)
drawGround(100, 50)
pygame.display.flip()
pygame.time.delay(int(1000.0/fps))
if __name__ == '__main__':
main()