Spielewelten mit Raycasting: Programmierung
In diesem Kapitel geht es um einige Details bei der Programmierung, hauptsächlich darum, wie man Bilder darstellt und bestimmte Bildbereiche auswählt. Wir werden diese Techniken benötigen, sobald wir die Spielewelt mit Texturen -Bildern für Wände, Decken und Böden- versehen.
Die Beispiele orientieren sich an der Programmiersprache Python. Auf Wikibooks gibt es zur Zeit das Buch Python unter Linux. Es enthält ein Kapitel über PyGame, eine Spielebibliothek, mit der wir uns hier beschäftigen. Wir gehen davon aus, dass Sie mindestens das Kapitel PyGame im Wikibuch Python unter Linux gelesen haben.
Mathematik
#!/usr/bin/python
# -*- coding: utf-8 -*-
import math
# Cosinus von 30°
print math.cos(math.radians(30))
# Sinus von 30°
print math.sin(math.radians(30))
# RAD -> DEG
print math.degrees(math.pi)
# Arkus Tangens von 0,5
print math.atan(0.5)
Die Funktionen Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile (Sinus), Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile (Kosinus), und Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile (Tangens) erwarten ihre Argumente im Modus Radiant.
Um zwischen den Modi Radiant und Dezimalgrad umrechnen zu können, gibt es die Funktionen Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile und Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile. Sie rechnen von Dezimalgrad nach Radiant beziehungsweise von Radiant nach Dezimalgrad. Alle in Python eingebauten trigonometrischen Funktionen benutzen Radiant.
Zu den trigonometrischen Funktionen gibt es die jeweiligen Umkehrfunktionen Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile (Arkussinus), Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile (Arkuskosinus) und Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile (Arkustangens), die aus einem Argument wieder einen Winkel zaubern können -der ebenfalls in Radiant ausgegeben wird.
#!/usr/bin/python
# -*- coding: utf-8 -*-
import math
def pythagoras(a, b):
cQuadrat = a * a + b * b
seite = math.sqrt(cQuadrat)
return seite
print pythagoras(4, 3)
Dieses Beispiel zeigte nochmal den Umgang mit der Wurzelfunktion Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile. Der Satz des Pythagoras wird hier angewendet, um die Länge der großen Seite eines rechtwinkeligen Dreiecks zu bestimmen.
In Python ist der Operator, der ganzzahlige Division durchführt Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile und der Operator für Modulo-Rechnung das Prozent-Zeichen Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile. Um gleichzeitig ganzzahlige Division und Modulo-Ergebnisse als Tupel zu erhalten, können Sie die Funktion Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile benutzen.
Bilder laden
Bilder lädt man in PyGame mit der Funktion Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile:
def ladeBild(dateiname):
tmp = pygame.image.load(dateiname)
surface = tmp.convert()
return surface
Man erhält ein Surface-Objekt, welches man mit Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile an die aktuellen Einstellungen (zum Beispiel Farbtiefe) anpasst. Damit wird die Darstellung schneller. Wir schreiben uns hier eine Funktion, die beide Schritte übernimmt und die endgültig vorbereitete Grafiksurface zurückliefert.
Die Surface selbst enthält Angaben über die Größe des Bildes, die Auflösung und weitere Details.
Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile kann verschiedene Dateiformate laden, darunter JPG, PNG und BMP.
Für die folgenden Beispiele benötigen Sie eine Bilddatei namens Spielewelten mit Raycasting/ Vorlagen:Dateiname. In unserem Beispiel ist diese 64[1] Pixel im Quadrat groß.
Bilder darstellen
Um eine Bild-Surface (quelle) auf ein Fenster-Surface (ziel) zu zeichnen, benötigen wir die Methode Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile.
def blitteBild(quelle, ziel, position):
rechteck = ziel.blit(quelle, position)
pygame.display.update(rechteck)
quelle und ziel sind jeweils Surfaces, position ist eine Angabe, wo die linke obere Ecke des Bildes platziert werden soll. Diese Position wird als 2-Tupel mit der x- und y-Koordinate übergeben. Das Ergebnis der Blit-Operation muss noch auf dem Bildschirm erscheinen. Dafür benutzen wir die Funktion Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile. Wird kein Rechteck angegeben, wird der gesamte Bildschirm neu gezeichnet, sonst nur das ausgewählte Rechteck.
- Tipp:Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile ist langsam! Es ist eine gute Strategie, bei vielen darzustellenden Bildern zuerst alle Bilder zu blitten, und dann genau einmal den Bildschirm aufzufrischen, wobei das kleinstmögliche Rechteck, das alle Bilder enthält, genommen werden sollte.

Hier folgt ein vollständiges Beispiel einer Anwendung, die lediglich ein Bild in der Größe Pixel darstellt. Dieses Bild wird in die obere linke Ecke bei den Koordinaten (10, 10) positioniert. Ein Tastendruck beendet das Programm:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pygame
import sys
WINWIDTH = 640
WINHEIGHT = 480
def ladeBild(dateiname):
tmp = pygame.image.load(dateiname)
surface = tmp.convert()
return surface
def blitteBild(quelle, ziel, position):
rechteck = ziel.blit(quelle, position)
pygame.display.update(rechteck)
def init(breite, hoehe):
pygame.init()
screen = pygame.display.set_mode((breite, hoehe))
screen.fill((200, 200, 200))
pygame.display.update()
return screen
def hauptschleife():
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
sys.exit()
if __name__ == '__main__':
screen = init(WINWIDTH, WINHEIGHT)
bild = ladeBild('wand1.png')
blitteBild(bild, screen, (10, 10))
hauptschleife()
Bilder zoomen
Um Bilder einfach größer beziehungsweise kleiner zu rechnen und dann darzustellen, bietet sich die Funktion Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile an. Sie verändert die Größe einer Surface Quelle auf eine gegebene Breite wie auch Höhe.
Die folgende Funktion füllt ein Rechteck mit einem Bild, das größer oder kleiner gerechnet wird, um es innerhalb eines Rechtecks darstellen zu können:
def fuelleRechteckMitBild(quelle, ziel, (x0, y0, w, h)):
tmp = pygame.transform.scale(quelle, (w, h))
rechteck = ziel.blit(tmp, (x0, y0))
pygame.display.update(rechteck)
Bilder kacheln
Bilder kann man nicht nur auf eine bestimmte Größe Zoomen, man kann sie auch nebeneinander legen, um ein Rechteck zu kacheln. Dieses Rechteck darf größer oder kleiner sein, als das Ursprungsbild. Wenn es größer ist, werden mehrere Bilder nebeneinander gelegt, wenn es kleiner ist, dann wird nur ein Ausschnitt vom Bild geblittet.
def kachelRechteckMitBild(quelle, ziel, (x0, y0, w, h)):
# Bildbreite, Bildhöhe
bBreite, bHoehe = quelle.get_size()
# Verbleibende Höhe, die es noch zu kacheln gilt
restHoehe = h
# Aktuelle Y-Position
y = y0
while restHoehe > 0:
# Höhe des Bildes, das wir gleich blitten
ausschnittHoehe = bHoehe if restHoehe >= bHoehe else restHoehe
restHoehe -= ausschnittHoehe
restBreite = w
x = x0
while restBreite > 0:
ausschnittBreite = bBreite if restBreite >= bBreite else restBreite
# Blittet einen Ausschnitt des Bildes "quelle" an die angegebene Position
ziel.blit(quelle, (x, y), (0, 0, ausschnittBreite, ausschnittHoehe))
restBreite -= ausschnittBreite
x += ausschnittBreite
y += ausschnittHoehe
pygame.display.update((x0, y0, w, h))
Die Funktion Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile legt das Ursprungsbild Quelle einige Male nebeneinander, bis eine Reihe voll ist (innere Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile-Schleife. Dann wird der Y-Wert um die Höhe des Bildes vergrößert und wir kacheln die nächste Zeile.
Dabei müssen wir ständig darauf bedacht sein, dass wir nicht zu viel kacheln, also am rechten wie auch am unteren Rand vielleicht nur Teile des Bildes zu zeichnen. Hierzu berechnen wir die Breite (Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile) und die Höhe (Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile) des Bildes, das wir noch zeichnen müssen. Dieses kann eine enge Spalte oder Zeile vom Ursprungsbild sein.

Hier folgt ein vollständiges Beispiel, welches den Unterschied in der Darstellung von Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile und Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile zeigt:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pygame
import sys
WINWIDTH = 640
WINHEIGHT = 480
def ladeBild(dateiname):
tmp = pygame.image.load(dateiname)
surface = tmp.convert()
return surface
def init(breite, hoehe):
pygame.init()
screen = pygame.display.set_mode((breite, hoehe))
screen.fill((200, 200, 200))
pygame.display.update()
return screen
def fuelleRechteckMitBild(quelle, ziel, (x0, y0, w, h)):
tmp = pygame.transform.scale(quelle, (w, h))
rechteck = ziel.blit(tmp, (x0, y0))
pygame.display.update(rechteck)
def kachelRechteckMitBild(quelle, ziel, (x0, y0, w, h)):
# Bildbreite, Bildhöhe
bBreite, bHoehe = quelle.get_size()
# Verbleibende Höhe, die es noch zu kacheln gilt
restHoehe = h
# Aktuelle Y-Position
y = y0
while restHoehe > 0:
# Höhe des Bildes, das wir gleich blitten
ausschnittHoehe = bHoehe if restHoehe >= bHoehe else restHoehe
restHoehe -= ausschnittHoehe
restBreite = w
x = x0
while restBreite > 0:
ausschnittBreite = bBreite if restBreite >= bBreite else restBreite
# Blittet einen Ausschnitt des Bildes "quelle" an die angegebene Position
ziel.blit(quelle, (x, y), (0, 0, ausschnittBreite, ausschnittHoehe))
restBreite -= ausschnittBreite
x += ausschnittBreite
y += ausschnittHoehe
pygame.display.update((x0, y0, w, h))
def hauptschleife():
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
sys.exit()
if __name__ == '__main__':
screen = init(WINWIDTH, WINHEIGHT)
bild = ladeBild('wand1.png')
fuelleRechteckMitBild(bild, screen, (10, 10, 10, 10))
kachelRechteckMitBild(bild, screen, (200, 10, 10, 10))
fuelleRechteckMitBild(bild, screen, (10, 50, 100, 100))
kachelRechteckMitBild(bild, screen, (200, 50, 100, 100))
hauptschleife()
Bilderspalten und -zeilen
Hat man einmal ein Bild geladen und es als Surface gespeichert, dann kann man mit den folgenden Funktionen eine einzelne Zeile oder Spalte auswählen und anzeigen.
def bildzeile(zeile, bild, ziel, (x, y)):
assert(zeile >= 0 and zeile < bild.get_height())
width = bild.get_width()
ziel.blit(bild, (x, y), (0, zeile, width, 1))
pygame.display.update()
def bildspalte(spalte, bild, ziel, (x, y)):
assert(spalte >= 0 and spalte < bild.get_width())
height = bild.get_height()
ziel.blit(bild, (x, y), (spalte, 0, 1, height))
pygame.display.update()
Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile und Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile sind hierbei die betreffenden Zeilen- oder Spaltennummern des Bildes. Natürlich darf die ausgewählte Spalte nicht außerhalb des Bildes liegen, ebenso verhält es sich bei der Zeile. Aus dem Bild wird die betreffende Region ausgewählt und an die Zielkoordinaten Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile geblittet. Anschließend wird das gesamte Grafikfenster neu gezeichnet.
Für das Raycasting-Verfahren benötigen wir später eine Möglichkeit, wie man aus einem Bild eine Spalte in einer gewissen Höhe auswählt. Wenn das Bild selbst aber eine andere Höhe hat, muss man dieses zoomen oder kacheln. Wir stellen Ihnen hier eine Funktion vor, die eine Bilderspalte auswählt und diese auf eine neue Höhe zoomt:
def bildspalteZoom(spalte, bild, ziel, (x, y), neueHoehe):
assert(spalte >= 0 and spalte < bild.get_width() and neueHoehe > 0)
height = bild.get_height()
einspaltig = pygame.Surface((1, height), 0, bild)
einspaltig.blit(bild, (0, 0), (spalte, 0, 1, height))
gestretcht = pygame.transform.scale(einspaltig, (1, neueHoehe))
ziel.blit(gestretcht, (x, y), (0, 0, 1, neueHoehe))
pygame.display.update( (x, y, 1, neueHoehe) )
Zuerst wird mit Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile eine neue Surface erzeugt, die aus einer Spalte besteht. In diese Surface wird die betreffende Spalte hineinkopiert. Anschließend sorgt Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile dafür, dass die Spalte auf die neue Höhe skaliert wird. Dieses skalierte Bild wird dann geblittet. Nur den geblitteten Bereich neu zu zeichenen führt zu einem enormen Geschwindigkeitsgewinnn, darum geben wir Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile das betreffende Rechteck mit.
Das folgende Programm zeigt, wie Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile arbeitet. Darüberhinaus nutzen wir die Modulo-Rechnung, um innerhalb der Grenzen der Fenstergröße und des erlaubten Höhenbereiches zu bleiben:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import pygame
import sys
WINWIDTH = 640
WINHEIGHT = 480
def ladeBild(dateiname):
tmp = pygame.image.load(dateiname)
surface = tmp.convert()
return surface
def init(breite, hoehe):
pygame.init()
screen = pygame.display.set_mode((breite, hoehe))
screen.fill((200, 200, 200))
pygame.display.update()
return screen
def bildspalteZoom(spalte, bild, ziel, (x, y), neueHoehe):
assert(spalte >= 0 and spalte < bild.get_width() and neueHoehe > 0)
height = bild.get_height()
einspaltig = pygame.Surface((1, height), 0, bild)
einspaltig.blit(bild, (0, 0), (spalte, 0, 1, height))
gestretcht = pygame.transform.scale(einspaltig, (1, neueHoehe))
ziel.blit(gestretcht, (x, y), (0, 0, 1, neueHoehe))
pygame.display.update( (x, y, 1, neueHoehe) )
def hauptschleife():
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
sys.exit()
if __name__ == '__main__':
screen = init(WINWIDTH, WINHEIGHT)
bild = ladeBild('wand1.png')
for s in xrange(WINWIDTH):
bildspalteZoom(s % bild.get_width(), bild, screen, (s, 0), (s % WINHEIGHT) + 1)
hauptschleife()
Hauptschleife
Wir haben bereits Beispiele für Hauptschleifen in den hier vorgestellten Programmen gefunden. Da es in diesem Kapitel besonders um die Spieleprogrammierung geht, wollen wir einige Techniken im Umgang mit Hauptschleifen vorstellen.
In der Hauptschleife werden die Nachrichten, also zum Beispiel Tastendrücke und Mausbewegungen, bearbeitet. Nach der Initialisierung des Programms ist die Hauptschleife die einzige Funktion, die dauerhaft ausgeführt wird. Wird sie verlassen, beendet sich das Programm. An dieser Stelle passieren also alle Interaktionen, ebenso wie die gesamte Spielelogik.
Ereignisgesteuerte Hauptschleife
Eine ereignisgesteuerte Hauptschleife besteht darin, nur auf Ereignisse zu reagieren, wenn der Benutzer etwas tut. Das betrifft die schon angemerkten Tastendrücke, Joystickbewegungen und Mausbewegungen. Solche Schleifen findet man am ehesten in Anwendungsprogrammen und Spielen, bei denen es keine "Hintergrundaktionen" gibt.
Zeitgesteuerte Hauptschleife
Zeitgesteuerte Hauptschleifen eignen sich in den Fällen, in denen die Benutzerinteraktion nicht die einzige Grundlage für das Spielegeschehen ist. Sollen sich Monster unabhängig von der eigenen Spielfigur bewegen, dann passiert das unabhängig davon, ob der Spieler gerade eine Taste drückt oder nicht. In diesen Fällen kann man zu zeitgesteuerten Schleifen greifen.
Pollen
Beim Pollen wird die Hauptschleife so oft es geht ausgeführt. Bei jedem Durchgang werden aktuelle Ereignisse abgefragt -nicht nur dann, wenn Ereignisse anliegen-, die Spielelogik wird aufgerufen und das Programm läuft "so schnell es geht". Solche Programme laufen auf unterschiedlicher Hardware verschieden schnell[2] .
Framebasierende Hauptschleife
Eine framebasierende Hauptschleife ist ein Spezialfall der zeitgesteuerten Hauptschleife. Hierbei wird darauf geachtet, dass man eine bestimmte Bildwiederholrate bekommt. Es wird also kontinuierlich die Zeit gemessen, in der man in der Hauptschleife verweilt und dann versucht, alle zum Beispiel 50 ms (entspricht 20 Bildern/Sekunde) ein neues Bild zu erzeugen.
Anregungen
Diese Liste besteht aus einigen Anregungen, um sich etwas tiefer mit den vorhandenen Beispielen zu beschäftigen.
- Schreiben Sie die Funktion Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile, die das Gleiche wie Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile macht, nur eben mit Zeilen.
- Im Text heißt es, man könne Bildspalten statt zu zoomen auch kacheln. Schreiben Sie eine Funktion, die genau das macht.
- Wie könnte man im Beispielprogramm zu Bilderspalten und -zeilen die Grafikausgabe beschleunigen?
- Was bedeutet in ebendiesem Programm Spielewelten mit Raycasting/ Vorlagen:Quellcodezeile?
Anmerkungen
- ↑ Auf diese Größe kommt es nicht wirklich an. In den Beispielanwendungen benutzen wir eine Textur direkt aus GIMP.
- ↑ Der Autor dieser Zeilen hat mal auf einem einigermaßen neuen Computer ein Spiel ("Elite"-Clon) gespielt, welches in den 80er Jahren mit dieser Technik programmiert wurde. Er wurde schneller abgeschossen, als er eine Taste drücken konnte...