Spielewelten mit Raycasting: Raum

Im letzten Kapitel haben Sie das Spielfeld kennen gelernt, auf dem das Leben passiert. Jetzt geht es um die dreidimensionale Darstellung der Umgebung. Bisher kennen wir lediglich den Abstand der Spielfigur von der Wand. Wir werden am Ende des Kapitels die Welt so sehen, wie die Spielfigur auf dem Feld sie sieht.

GrundbegriffeBearbeiten

BlickfeldwinkelBearbeiten

 
Das gesamte Blickfeld

Wenn wir die Welt durch eine Kamera sehen, können wir mit dem Zoom einen mehr oder weniger großen Ausschnitt der Welt wahrnehmen. Der Zoom verändert dabei den Winkel, den wir auf ein mal sehen können. Bei uns Menschen beträgt dieser fast 90°, wobei wir nur einen kleinen Winkelbereich wirklich scharf wahrnehmen und einen Zoom haben wir leider auch nicht.

Das, worauf wir vor dem Computer blicken ist der Monitor oder genauer das betreffende Programmfenster, das unsere Aufmerksamkeit hat. Auf dieses Programmfenster wird die gesamte Welt projiziert. In der Zeichnung ist b der Bildschirm, der die Spielwelt darstellen soll. u ist der Blickfeldwinkel, also der Winkelbereich, in dem wir die Welt -ohne den Kopf zu verdrehen- sehen können.

Setzen sie sich einmal als Übung so nah vor Ihren Monitor, dass sie nichts außer den Monitor sehen können. Das ist ganz schön nah.

Statt Bildschirmfenster oder Programmfenster sagen wir nun auch Projektionsebene.

Abstand zur ProjektionsebeneBearbeiten

 
Berechnung der Entfernung zur Projektionsebene

Wir kennen nun unsere Projektionsebene, das ist unser Fenster mit typischen Ausmaßen von 640*480, 800*600 oder 1024*768 Bildpunkten und einen Blickfeldwinkel, den wir mit beispielsweise 60° vorgeben. Teilt man dieses große Dreieck in zwei Teile, erhält man je rechtwinkelige Dreiecke. Dann ist die Entfernung zur Projektionsebene

 

Diesen Wert nennen wir auch den Projektionsabstand.

WandprojektionBearbeiten

 
Die projizierte Höhe der Wand

Um nun die Höhe der Wand, wie sie auf dem Computermonitor erscheint, berechnen zu können bedienen wir uns des Strahlensatzes.

Es gilt:

 

In der Zeichnung ist die Wandhöhe mit hW gekennzeichnet. Diese geben wir später im Programm vor, sie kann zum Beispiel 64 Pixel betragen. Die Höhe der projizierten Wand, in der Zeichnung mit hP gekennzeichnet, ist kleiner als die echte Wandhöhe, wenn die Spielfigur sehr weit weg von der Wand steht. Die Entfernung der Spielfigur von der Wand ist eine Entfernung, die der oben berechneten Kästchenentfernung entspricht, allerdings bezogen auf Pixel statt auf Kästchen. Den Abstand zur Projektionsebene kennen wir schon.

Sind der Projektionsabstand und die Entfernung der Spielfigur von der Wand gleich groß, dann wird die Wand mit ihrer echten Wandhöhe (also beispielsweise 64 Pixel) dargestellt. Die Projektionsebene kann auch hinter der Wand liegen, dann wird die Wand größer dargestellt.

Diese Berechnungen werden wir in der Methode berechneHoehe() vornehmen.

SpaltenwinkelBearbeiten

 
Die Spielfigur sendet viele Strahlen aus...

Für jede Spalte unserer Projektonsebene senden wir einen Strahl von der Spielfigur zur Wand aus. Haben wir eine Projektionsebene von 640*480 Punkte, so senden wir also 640 Strahlen aus. Zu jedem dieser Strahlen gehört ein eigener Winkel. Der linke Strahl, der zur Spalte 0 gehört, ist durch den Blickfeldwinkel (in der Zeichnung grün dargestellt) begrenzt. Der rechte Strahl, der zur Spalte 639 ebenfalls. Allgemein kann man sagen, dass

  ist.

Der Linke Strahl ist also

 .

RaycastingBearbeiten

Haben wir viele Strahlen, die von einer Spielfigur ausgesendet werden bis zur Wand verfolgt und den entsprechenden Abstand zwischen Spielfigur und Wand berechnet, dann können wir die Höhe der Wand mit dem Strahlensatz berechnen. Für jeden Strahl, also jede Spalte der Projektionsebene, berechnen wir eine eigene Höhe und speichern diese zusammen mit dem Wandzeichen und der Nummer der gewählten Spalte in einer Liste. Diese Liste wird später einer Funktion übergeben, die diese Ansicht darstellt. Dieses Verfahren ist letztlich das, was Raycasting in Computerspielen macht.

Es wird 3DBearbeiten

Erste RaumklasseBearbeiten

Der folgende Code enthält die bisher besprochenen Grundbegriffe und Verfahren. Bitte beachten Sie, dass dieser Code auf einer leicht geänderten Variante der Spielfeld-Klasse zurückgreift. Die Methode Spielfeld.findeWand(winkel) findet Wände abhängig vom gewählten Parameter, der verschieden von Spielfeld.blickrichtung sein kann.

class Raum(object):

    def __init__(self, blickfeldwinkel, pBreite, pHoehe, wandhoehe):
        """Initialisiert den Raum
          blickfeldwinkel - gesamter Winkelbereich, den Spielfigur sehen kann
          pBreite, pHoehe - Ausmaße des Fensters, auf das projiziert werden soll"""
        self.blickfeldWinkel = blickfeldwinkel
        self.blickfeldWinkelD2 = blickfeldwinkel / 2.0
        self.mittelLinie = pHoehe // 2
        self.spaltenZahl = pBreite
        self.projektionsAbstand = (self.spaltenZahl / 2.0) /  math.tan(math.radians(self.blickfeldWinkelD2))
        self.spaltenWinkel = 1.0 * blickfeldwinkel / self.spaltenZahl
        self.wandHoeheEcht = 1.0 * wandhoehe


    def berechneHoehe(self, entfernung):
        """berechnet aus der Entfernung die scheinbare Wandhöhe"""
        h = self.projektionsAbstand / entfernung
        return int(h)


    def raycasting(self, spielfeld):
        anfangsWinkel = spielfeld.blickrichtung + self.blickfeldWinkelD2
        hoehenListe = []
        for spalte in xrange(self.spaltenZahl):
            winkel = (anfangsWinkel - spalte * self.spaltenWinkel) % 360
            FeldXpos, FeldYpos, entfernung = spielfeld.findeWand(winkel)
            zeichen = spielfeld.zeichen(FeldXpos, FeldYpos)
            hoehe = self.berechneHoehe(entfernung)
            hoehenListe.append((spalte, zeichen, hoehe))
        return hoehenListe
  • In der Methode __init__() werden Sie die meisten der Variablen wiederfinden, die wir in den Grundbegriffen erläutert haben. mittelLinie ist eine Linie, die in der Mitte waagerecht über Ihr Programmfenster verläuft. Wenn Sie gerade auf die Wand gucken, ist die Mittellinie auf Augenhöhe. Die Bedeutung für uns ist, dass wir die projizierten Wandelemente an dieser Linie symmetrisch ausrichten. Wir werden weiter unten im Kapitel auf die Mittellinie nochmal genauer eingehen. blickfeldWinkelD2 ist die Hälfte des Blickfeldwinkels. Diese Variable führen wir nur ein, weil sie praktisch ist.
  • Die Methode berechneHoehe() berechnet aus der gegebenen Entfernung zur Wand die Höhe der projizierten Wand. Die Entfernung darf nicht Null sein. Diese Rechnung sieht einfacher aus als der oben gezeigte Strahensatz, weil sich hier die Wandhöhe herauskürzt. Vollständig müsste die Berechnung lauten:
     
    weil die Entfernung in Kästchen und nicht in Pixel-Einheiten gemessen wird. wandHoeheEcht kürzt sich dabei raus.
  • Von der Methode raycasting() bekommen wir letztlich eine Liste von Höhenangaben zur jeweiligen Entfernung der Spielfigur unter einem Winkel. Die Winkel fangen bei anfangswinkel an und überstreichen dann den gesamten Blickfeldwinkel. Jede Spalte hat einen eigenen Winkel, der spaltenWinkel von der Nachbarspalte entfernt ist.
    Beachten Sie bitte, dass wir hier findeWand() mit einem Parameter aufrufen. Aus der Entfernung wird die Höhe berechnet, diese und einige weitere Angaben werden als Tupel verpackt der Liste angehängt. Diese Liste wird zum Schluss übergeben.

RaumdarstellungBearbeiten

Die folgende Funktion nimmt sich die Höhenliste, wie wir sie im vorherigen Abschnitt besprochen haben, und erzeugt mit ihr eine dreidimensionale Ansicht des Raumes:

def zeichneSpielfeld(screen, mittellinie, hoehen):
    screen.fill((200, 200, 200))
    for spalte, zeichen, hoehe in hoehen:
        x0, x1 = spalte, spalte
        # Zeichne in der Mitte
        y0 = mittellinie - hoehe // 2
        y1 = y0 + hoehe
        if zeichen == 'N':
            farbe = (128, 0, 0)
        elif zeichen == 'S':
            farbe = (128, 128, 0)
        elif zeichen == 'O':
            farbe = (213, 85, 0)
        elif zeichen == 'W':
            farbe = (160, 43, 43)
        else:
            farbe = (0, 200, 200)
        pygame.draw.line(screen, farbe, (x0, y0), (x1, y1), 1)
    pygame.display.update()

Hierbei wird lediglich pro Spalte eine Linie in einer passenden Farbe gemalt. Die Höhe dieser Linie ist die aus der Raum-Klasse berechnete Höhe. Die Linie wird jeweils an der Mittellinie ausgerichtet.

Einbettung in die HauptschleifeBearbeiten

Höhenberechnungen sind sehr aufwändig. Grund genug, sie nur möglichst selten durchzuführen. Die folgende Variante der Hauptschleife berechnet nur dann neue Höhen und stellt den Raum dar, wenn eine Taste gedrückt wurde. Dafür sorgt die Variable mussZeichnen, die nur dann True ist, wenn eine Taste gedrückt wurde:

def hauptschleife(raum, spielfeld, screen, breite, hoehe):
    mussZeichnen = False
    hoehen = raum.raycasting(spielfeld)
    zeichneSpielfeld(screen, raum.mittelLinie, hoehen)
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                mussZeichnen = True
                if event.key == pygame.K_UP:
                    spielfeld.geheVor()
                elif event.key == pygame.K_DOWN:
                    spielfeld.geheZurueck()
                elif event.key == pygame.K_LEFT:
                    spielfeld.dreheLinks()
                elif event.key == pygame.K_RIGHT:
                    spielfeld.dreheRechts()
            if mussZeichnen:
                hoehen = raum.raycasting(spielfeld)
                zeichneSpielfeld(screen, raum.mittelLinie, hoehen)
                mussZeichnen = False

BeispielprogrammBearbeiten

Spielfelddatei 3D-Ansicht Spielfeld
NNNNNNNNNN
W........O
W..SSSS..O
W........O
W..NNNN..O
W........O
W........O
W........O
W.......#O
SSSSSSSSSS
 
Der bunte Raum...


Das folgende Beispielprogramm erzeugt eine 3D-Darstellung der Karte. Sie können wie gewohnt mit den Pfeiltasten durch den Raum spazieren. Bitte achten Sie insbesondere auf Spielfeld.findeWand(), denn genau hier hat sich in der Spielfeld-Klasse etwas geändert

#!/usr/bin/python
# -*- coding: utf-8 -*-
 
import math
import pygame
import sys
 
 
class Spielfeld(object):
 
    def __init__(self, dateiname):
        self.felder = []
        # Blickrichtung als Winkel in Grad
        self.blickrichtung = 90
        # Datei einlesen
        datei = open(dateiname, 'r')
        for zeile in datei:
            self.felder.append(zeile[:-1])
        datei.close()
        self.breite = len(self.felder[0])
        self.hoehe = len(self.felder)
        for num, zeile in enumerate(self.felder):
            idx = zeile.find('#')
            if idx >= 0:
                self.position = (idx, num)
                self.felder[num] = self.felder[num].replace('#', '.')
                break
 
 
    def zeichen(self, x, y):
        """Liefert das Zeichen an der Position x, y """
        assert(0 <= x < self.breite)
        assert(0 <= y < self.hoehe)
        return self.felder[y][x]
 
 
    def istWand(self, zeichen):
        """True, wenn das Zeichen ein Wandzeichen ist """
        return zeichen in ('N', 'W', 'S', 'O')
 
 
    def geheVor(self):
        """gehe in Richtung der Blickrichtung """
        px, py = self.position
        qx = int(round(px + math.cos(math.radians(self.blickrichtung))))
        qy = int(round(py - math.sin(math.radians(self.blickrichtung))))
        zeichen = self.zeichen(qx, qy)
        if not self.istWand(zeichen): 
            self.position = (qx, qy)  
 
 
    def geheZurueck(self):
        """gehe entgegen der Blickrichtung """
        self.blickrichtung += 180
        self.geheVor()
        self.blickrichtung -= 180
 
 
    def dreheLinks(self):
        self.blickrichtung = (self.blickrichtung +5) % 360
 
 
    def dreheRechts(self):
        self.blickrichtung = (self.blickrichtung - 5) % 360
 
 
    def findeWandHV(self, px, py, dx, dy):
        """Findet eine Wand in auschließlich horizontaler und vertikaler Richtung.
        (px, py) ist die Feldkoordinate des Spielers,
        (dx, dy) ist die Schrittweite, die wir bei jeder Iteration gehen"""
        FeldXpos = px + dx
        FeldYpos = py + dy
        zeichen = self.zeichen(FeldXpos, FeldYpos)
        while not self.istWand(zeichen):
            # untersuche nächstes Feld
            FeldXpos = FeldXpos + dx
            FeldYpos = FeldYpos + dy
            zeichen = self.zeichen(FeldXpos, FeldYpos)
        entfernung = (dx * (FeldXpos - px) + dy * (FeldYpos - py)) - 0.5
        wand = (FeldXpos, FeldYpos, entfernung)
        return wand


    def findeWandDiagonal(self, px, py, dx, dy):
        """Findet eine Wand in diagonaler Richtung.
        (px, py) ist die Feldkoordinate des Spielers,
        (dx, dy) ist die Schrittweite, die wir bei jeder Iteration gehen"""
        mitteX = px + 0.5
        mitteY = py + 0.5
        schnittpunktX = mitteX + dx
        schnittpunktY = mitteY + dy
        # nächste Schrittweite 
        dx = dx * 2.0
        dy = dy * 2.0
        FeldXpos = int(schnittpunktX)
        FeldYpos = int(schnittpunktY)
        if 0 < FeldXpos < self.breite and 0 < FeldYpos < self.hoehe:
            zeichen = self.zeichen(int(FeldXpos), int(FeldYpos))
            while not self.istWand(zeichen):
                schnittpunktX = schnittpunktX + dx
                schnittpunktY = schnittpunktY + dy
                FeldXpos = int(schnittpunktX)
                FeldYpos = int(schnittpunktY)
                if not (0 < FeldXpos < self.breite and 0 < FeldYpos < self.hoehe): break
                zeichen = self.zeichen(FeldXpos, FeldYpos)
        entfernung = math.sqrt((schnittpunktX - mitteX) ** 2 + (schnittpunktY - mitteY) ** 2)
        wand = (FeldXpos, FeldYpos, entfernung)
        return wand


    def findeWand(self, winkel):
        px, py = self.position
        wand = (0, 0, 0)
        tangens = math.tan(math.radians(winkel))
        if winkel == 0:
            wand = self.findeWandHV(px, py, 1, 0)
        elif winkel > 0 and winkel < 90:
            dx = 0.501
            dy = -tangens * dx
            wand1 = self.findeWandDiagonal(px, py, dx, dy)
            dy = -0.501
            dx = -dy / tangens
            wand2 = self.findeWandDiagonal(px, py, dx, dy)
            if wand1[2] < wand2[2]:
                wand = wand1
            else:
                wand = wand2
        elif winkel == 90:
            wand = self.findeWandHV(px, py, 0, -1)
        elif winkel > 90 and winkel < 180:
            dx = -0.501
            dy = -tangens * dx
            wand1 = self.findeWandDiagonal(px, py, dx, dy)
            dy = -0.501
            dx = -dy / tangens
            wand2 = self.findeWandDiagonal(px, py, dx, dy)
            if wand1[2] < wand2[2]:
                wand = wand1
            else:
                wand = wand2
        elif winkel == 180:
            wand = self.findeWandHV(px, py, -1, 0)
        elif winkel > 180 and winkel < 270:
            dx = -0.501
            dy = -tangens * dx
            wand1 = self.findeWandDiagonal(px, py, dx, dy)
            dy = 0.501
            dx = -dy / tangens
            wand2 = self.findeWandDiagonal(px, py, dx, dy)
            if wand1[2] < wand2[2]:
                wand = wand1
            else:
                wand = wand2
        elif winkel == 270:
            wand = self.findeWandHV(px, py, 0, 1)
        elif winkel > 270 and winkel < 360:
            dx = 0.501
            dy = -tangens * dx
            wand1 = self.findeWandDiagonal(px, py, dx, dy)
            dy = 0.501
            dx = -dy / tangens
            wand2 = self.findeWandDiagonal(px, py, dx, dy)
            if wand1[2] < wand2[2]:
                wand = wand1
            else:
                wand = wand2
        return wand



class Raum(object):

    def __init__(self, blickfeldwinkel, pBreite, pHoehe, wandhoehe):
        """Initialisiert den Raum
          blickfeldwinkel - gesamter Winkelbereich, den Spielfigur sehen kann
          pBreite, pHoehe - Ausmaße des Fensters, auf das projiziert werden soll"""
        self.blickfeldWinkel = blickfeldwinkel
        self.blickfeldWinkelD2 = blickfeldwinkel / 2.0
        self.mittelLinie = pHoehe // 2
        self.spaltenZahl = pBreite
        self.projektionsAbstand = (self.spaltenZahl / 2.0) /  math.tan(math.radians(self.blickfeldWinkelD2))
        self.spaltenWinkel = 1.0 * blickfeldwinkel / self.spaltenZahl
        self.wandHoeheEcht = 1.0 * wandhoehe


    def berechneHoehe(self, entfernung):
        """berechnet aus der Entfernung die scheinbare Wandhöhe"""
        h = self.projektionsAbstand / entfernung
        return int(h)


    def raycasting(self, spielfeld):
        anfangsWinkel = spielfeld.blickrichtung + self.blickfeldWinkelD2
        hoehenListe = []
        for spalte in xrange(self.spaltenZahl):
            winkel = (anfangsWinkel - spalte * self.spaltenWinkel) % 360
            FeldXpos, FeldYpos, entfernung = spielfeld.findeWand(winkel)
            zeichen = spielfeld.zeichen(FeldXpos, FeldYpos)
            hoehe = self.berechneHoehe(entfernung)
            hoehenListe.append((spalte, zeichen, hoehe))
        return hoehenListe


 
def init(breite, hoehe):
    screen = pygame.display.set_mode((breite, hoehe))
    screen.fill((200, 200, 200))
    pygame.display.update()
    return screen


def zeichneSpielfeld(screen, mittellinie, hoehen):
    screen.fill((200, 200, 200))
    for spalte, zeichen, hoehe in hoehen:
        x0, x1 = spalte, spalte
        # Zeichne in der Mitte 
        y0 = mittellinie - hoehe // 2
        y1 = y0 + hoehe
        if zeichen == 'N':
            farbe = (128, 0, 0)
        elif zeichen == 'S':
            farbe = (128, 128, 0)
        elif zeichen == 'O':
            farbe = (213, 85, 0)
        elif zeichen == 'W':
            farbe = (160, 43, 43)
        else:
            farbe = (0, 200, 200)
        pygame.draw.line(screen, farbe, (x0, y0), (x1, y1), 1)
    pygame.display.update()



def hauptschleife(raum, spielfeld, screen, breite, hoehe):
    mussZeichnen = False
    hoehen = raum.raycasting(spielfeld)
    zeichneSpielfeld(screen, raum.mittelLinie, hoehen)
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                mussZeichnen = True
                if event.key == pygame.K_UP:
                    spielfeld.geheVor()
                elif event.key == pygame.K_DOWN:
                    spielfeld.geheZurueck()
                elif event.key == pygame.K_LEFT:
                    spielfeld.dreheLinks()
                elif event.key == pygame.K_RIGHT:
                    spielfeld.dreheRechts()
            if mussZeichnen:
                hoehen = raum.raycasting(spielfeld)
                zeichneSpielfeld(screen, raum.mittelLinie, hoehen)
                mussZeichnen = False


if __name__ == '__main__':
    spielfeld = Spielfeld('spielfeld.txt')
    screen = init(500, 500)
    raum = Raum(60, 500, 500, 64)
    hauptschleife(raum, spielfeld, screen, 500, 500)

VerzerrungskorrekturBearbeiten

Gerade bei großen Blickfeldwinkeln werden die Wände verzerrt dargestellt. die folgenden Abbildungen sollen das verdeutlichen:

 
Verzerrung bei 60°
 
Verzerrung bei 90°
 
Die Randstrahlen des Blickfeldwinkels sind länger als der Strahl der BlickRichtung

Strahlen, die die Spielfigur aussendet, sind selbst dann nicht gleich lang, wenn die Spielfigur vor einer ebenen Wand steht. Diejenigen Strahlen, die im Blickfeldwinkel liegen, treffen alle an unterschiedlichen Stellen auf die Wand. Hätten wir eine kreisförmige Wand, in der Zeichnung punktiert dargestellt, so wären alle Strahlen der Spielfigur gleich lang und die mit dem Raycasting-Verfahren dargestellte Wandhöhe wäre überall gleich. Der rote Ausschnitt aus dem Kreis stellt den Bereich nochmal dar, wo die Wandhöhe innerhalb des Blickfeldwinkels gleich wäre.

Diese unterschiedlichen Wandentfernungen führen zu einer Verzerrung der Darstellung. Gerade Wände erscheinen an den Enden deutlich kleiner als in der Mitte. Um dieses auszugleichen, korrigieren wir die Entfernung um einen Faktor, der abhängig ist vom Winkel.

In der Zeichnung sehen wir sofort:

 


Dieses kann man allgemein für jeden untersuchten Winkel angeben, den man verfolgt. Alle Strahlen, außer dem in Blickrichtung gelegenen, führen zu Verzerrungen. Um nun den korrigierten Abstand (K) zu bestimmen, projiziert man den Strahl, der zu Verzerrungen (V) führt, auf den Blickrichtungsstrahl:

 ,

wobei u gerade die Differenz aus dem aktuellen Winkel und dem Winkel der Blickrichtung ist.

Wir ändern, um die Verzerrungskorrektur nutzen zu können, die Methode raycasting(), die zusätzlich den boolschen Parameter korrektur bekommt. Wenn die Korrektur eingeschaltet ist, dann wird die Winkeldifferenz berechnet und die Entfernung angepasst:

def raycasting(self, spielfeld, korrektur):
    anfangsWinkel = spielfeld.blickrichtung + self.blickfeldWinkelD2
    hoehenListe = []
    for spalte in xrange(self.spaltenZahl):
        winkel = (anfangsWinkel - spalte * self.spaltenWinkel) % 360
        FeldXpos, FeldYpos, entfernung = spielfeld.findeWand(winkel)
        if korrektur:
            w = -winkel + spielfeld.blickrichtung
            entfernung = entfernung * math.cos(math.radians(w))
        zeichen = spielfeld.zeichen(FeldXpos, FeldYpos)
        hoehe = self.berechneHoehe(entfernung)
        hoehenListe.append((spalte, zeichen, hoehe))
    return hoehenListe

Ebenfalls ändern wir die Hauptschleife etwas ab, so dass die Verzerrungskorrektur mit der Taste k eingeschaltet werden kann:

def hauptschleife(raum, spielfeld, screen, breite, hoehe):
    mussZeichnen = False
    korrektur = False
    hoehen = raum.raycasting(spielfeld, korrektur)
    zeichneSpielfeld(screen, raum.mittelLinie, hoehen)
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                mussZeichnen = True
                if event.key == pygame.K_UP:
                    spielfeld.geheVor()
                elif event.key == pygame.K_DOWN:
                    spielfeld.geheZurueck()
                elif event.key == pygame.K_LEFT:
                    spielfeld.dreheLinks()
                elif event.key == pygame.K_RIGHT:
                    spielfeld.dreheRechts()
                elif event.key == pygame.K_k:
                    korrektur = not korrektur
                    print "korrektur: ", korrektur
            if mussZeichnen:
                hoehen = raum.raycasting(spielfeld, korrektur)
                zeichneSpielfeld(screen, raum.mittelLinie, hoehen)
                mussZeichnen = False

Die folgenden Bilder zeigen, wie die Verzerrungskorrektur wirkt. Bei einem Blickfeldwinkel von 60° ist diese Korrektur kaum zu sehen, bei 90° hingegen gleicht sie deutlich aus. Testen Sie beide Varianten mit unterschiedlichen Wandentfernungen, werden Sie aber auch einen Gewinn bei der Darstellung mit einem Blickfeldwinkel von 60° wahrnehmen können.

 
Verzerungskorrektur bei 60°
 
Verzerrungskorrektur bei 90°

MittellinieBearbeiten

Wie oben angedeutet, werden hier einige Details der Mittellinie besprochen. Die Mittellinie führt bisher waagerecht durch die Mitte des Programmfensters. Die Mittellinie kann man heben und senken. Wenn Sie Ihren Bilderrahmen noch einmal vor das Gesicht nehmen, dann können Sie Sie diesen ebenfalls heben und senken - oder sich selbst und den Bilderrahmen auf die Fensterbank stellen. Der visuelle Eindruck ist dabei derselbe. Folgende Abbildungen sollen das verdeutlichen:

 
gesenkte Mittelinie
 
normale Mittellinie
 
gehobene Mittellinie

Diese Darstellungen können Sie selbst ausprobieren, indem Sie folgende geänderte Hauptschleife verwenden. Mit den Tasten 1 bis 3 können Sie die Mittellinie verändern. Die Mittellinien liegen dann auf einem oder zwei Dritteln der Fensterhöhe oder wie gewohnt in der Mitte:

def hauptschleife(raum, spielfeld, screen, breite, hoehe):
    mussZeichnen = False
    hoehen = raum.raycasting(spielfeld)
    zeichneSpielfeld(screen, raum.mittelLinie, hoehen)
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                mussZeichnen = True
                if event.key == pygame.K_UP:
                    spielfeld.geheVor()
                elif event.key == pygame.K_DOWN:
                    spielfeld.geheZurueck()
                elif event.key == pygame.K_LEFT:
                    spielfeld.dreheLinks()
                elif event.key == pygame.K_RIGHT:
                    spielfeld.dreheRechts()
                elif event.key == pygame.K_1:
                    raum.mittelLinie = 2 * hoehe // 3
                elif event.key == pygame.K_2:
                    raum.mittelLinie = hoehe // 2
                elif event.key == pygame.K_3:
                    raum.mittelLinie = hoehe // 3
            if mussZeichnen:
                hoehen = raum.raycasting(spielfeld)
                zeichneSpielfeld(screen, raum.mittelLinie, hoehen)
                mussZeichnen = False

Blauer HimmelBearbeiten

Um die Decke oder den Boden einzufärben, zum Beispiel, um blauen Himmel darzustellen, reicht es aus, in der Methode zeichneSpielfeld() oberhalb der Wandlinien gewünschte farbige Linien einzufügen:

def zeichneSpielfeld(screen, mittellinie, hoehen):
    screen.fill((200, 200, 200))
    for spalte, zeichen, hoehe in hoehen:
        x0, x1 = spalte, spalte
        # Zeichne in der Mitte
        y0 = mittellinie - hoehe // 2
        y1 = y0 + hoehe
        if zeichen == 'N':
            farbe = (128, 0, 0)
        elif zeichen == 'S':
            farbe = (128, 128, 0)
        elif zeichen == 'O':
            farbe = (213, 85, 0)
        elif zeichen == 'W':
            farbe = (160, 43, 43)
        else:
            farbe = (0, 200, 200)
        pygame.draw.line(screen, farbe, (x0, y0), (x1, y1), 1)
        # blauer himmel
        pygame.draw.line(screen, (64, 64, 192), (x0, 0), (x1, y0 - 1))
    pygame.display.update()


AnregungenBearbeiten

Wenn Sie alle bisherigen Kapitel durchgearbeitet haben, dann kennen Sie jetzt Raycasting. In den folgenden Kapiteln werden wir uns mit einigen Details beschäftigen, die den Spieleweltcharakter hervorheben. Um sich noch etwas tiefergehender mit den vorhandenen Methoden zu beschäftigen, können Sie folgende Aufgaben bearbeiten.

  • Stellen Sie nicht nur den Himmel blau dar, sondern auch den Boden grün.
  • Wie müsste man das Programm umschreiben, wenn man als Koordinaten nicht etwa Kästchen wählt, sondern Punkte, so dass man auch auf den einzelnen Kästchen laufen kann?
  • Der Mensch sieht nur in einem kleinen Winkelbereich wirklich scharf, Randbereiche werden unscharf wahrgenommen. Verändern Sie das Programm so, dass Randbereiche im Blickfeldwinkel genähert werden, der Kernbereich aber weiterhin genau berechnet wird. Hierbei geht es darum, im Randbereich nur jeden zweiten oder dritten Spaltenwinkel zu berechnen und alle dazwischenliegenden Werte denselben Abstand zur Wand annehmen zu lassen.
  • Wie könnte man findeWand() verbessern?
  • Färben Sie die Wände so ein, dass sehr weite Wände heller oder dunkler dargestellt werden als sehr nahe Wände. Was finden Sie besser, heller oder dunkler?