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.

MathematikBearbeiten

#!/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 sin(u) (Sinus), cos(u) (Kosinus), und tan(u) (Tangens) erwarten ihre Argumente im Modus Radiant.

Um zwischen den Modi Radiant und Dezimalgrad umrechnen zu können, gibt es die Funktionen radians(d) und degrees(r). 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 asin(x) (Arkussinus), acos(x) (Arkuskosinus) und atan(x) (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 sqrt( ). 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 a // b und der Operator für Modulo-Rechnung das Prozent-Zeichen a % b. Um gleichzeitig ganzzahlige Division und Modulo-Ergebnisse als Tupel zu erhalten, können Sie die Funktion divmod(a, b) benutzen.

Bilder ladenBearbeiten

Bilder lädt man in PyGame mit der Funktion image.load(dateiname):

def ladeBild(dateiname):
    tmp = pygame.image.load(dateiname)
    surface = tmp.convert()
    return surface

Man erhält ein Surface-Objekt, welches man mit convert() 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.

image.load(dateiname) kann verschiedene Dateiformate laden, darunter JPG, PNG und BMP.

Für die folgenden Beispiele benötigen Sie eine Bilddatei namens wand1.png. In unserem Beispiel ist diese 64[1] Pixel im Quadrat groß.

Bilder darstellenBearbeiten

Um eine Bild-Surface (quelle) auf ein Fenster-Surface (ziel) zu zeichnen, benötigen wir die Methode ziel.blit(quelle, position).

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 display.update(rechteck). Wird kein Rechteck angegeben, wird der gesamte Bildschirm neu gezeichnet, sonst nur das ausgewählte Rechteck.

Tipp:display.update(rechteck) 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.
 
Ein Bild in der oberen linken Ecke

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 zoomenBearbeiten

Um Bilder einfach größer beziehungsweise kleiner zu rechnen und dann darzustellen, bietet sich die Funktion transform.scale(Quelle, (Breite, Höhe)) 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 kachelnBearbeiten

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 kachelRechteckMitBild(Quelle, Ziel, Rechteck) legt das Ursprungsbild Quelle einige Male nebeneinander, bis eine Reihe voll ist (innere while-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 (ausschnittBreite) und die Höhe (ausschnittHoehe) des Bildes, das wir noch zeichnen müssen. Dieses kann eine enge Spalte oder Zeile vom Ursprungsbild sein.

 
Zoomen und Kacheln bei verschieden großen Rechtecken

Hier folgt ein vollständiges Beispiel, welches den Unterschied in der Darstellung von fuelleRechteckMitBild() und kachelRechteckMitBild() 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 -zeilenBearbeiten

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()


zeile und spalte 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 (x, y) 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 pygame.Surface() eine neue Surface erzeugt, die aus einer Spalte besteht. In diese Surface wird die betreffende Spalte hineinkopiert. Anschließend sorgt pygame.transform.scale() 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 pygame.display.update() das betreffende Rechteck mit.


Das folgende Programm zeigt, wie bildspalteZoom() arbeitet. Darüberhinaus nutzen wir die Modulo-Rechnung, um innerhalb der Grenzen der Fenstergröße und des erlaubten Höhenbereiches zu bleiben:

 
gezoomete Spalten
#!/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()


HauptschleifeBearbeiten

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 HauptschleifeBearbeiten

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 HauptschleifeBearbeiten

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.

PollenBearbeiten

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 HauptschleifeBearbeiten

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.

AnregungenBearbeiten

Diese Liste besteht aus einigen Anregungen, um sich etwas tiefer mit den vorhandenen Beispielen zu beschäftigen.

  • Schreiben Sie die Funktion bildzeileZoom(zeile, bild, ziel, (x, y), neueBreite), die das Gleiche wie bildspalteZoom(spalte, bild, ziel, (x, y), neueHoehe) 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 (s % WINHEIGHT) + 1?


AnmerkungenBearbeiten

  1. Auf diese Größe kommt es nicht wirklich an. In den Beispielanwendungen benutzen wir eine Textur direkt aus GIMP.
  2. 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...