Spielewelten mit Raycasting: Frei beweglich
Wir haben uns im letzten Kapitel darauf beschränkt, dass die Spielfigur immer in der Mitte eines Feldes steht. Jetzt erweitern wir die Möglichkeiten dahingehend, dass ein freies Bewegen auch innerhalb der Felder möglich ist. Darüberhinaus werden wir am Ende dieses Kapitels die Wände mit Textur darstellen können.
Freies Gehen auf dem Spielfeld
[Bearbeiten]In diesem Abschnitt üben wir das freie Gehen auf dem Spielfeld. Die schon bekannten Funktionen werden dabei modifiziert.
Spielfeld.weglaenge ist dabei der Weg, den die Spielfigur auf einmal gehen kann. Im folgenden Code sind dafür 10 Pixel vorgesehen. In der Methode Spielfeld.geheVor() wird daher die Richtung der Spielfigur mit der Weglänge multipliziert. Anschließend wird geschaut, ob wir auf eine Wand treffen würden und dann gegangen.
Die Methoden rund um Spielfeld.findeWand() haben wir ebenfalls auf den neusten Stand gebracht. Ein gefundenes Wandelement hat nun eine Position auf der Spielfeldkarte wie auch eine Schnittpunktposition mit genau dieser gefundenen Wand. zeichneSpielfeld() stellt nun nicht nur die gefundene Wand dar, sondern auch den ermittelten Schnittpunkt.
#!/usr/bin/python
# -*- coding: utf-8 -*-
import math
import pygame
import sys
class Spielfeld(object):
def __init__(self, dateiname, feldgroesse, weglaenge):
""" Initialisiert das Spielfeld
dateiname - Dateiname der Spielfeld-Datei
feldgroesse - die Pixelgrösse eines quadratischen Feldes
weglaenge - dieses Stück Pixelweg legt der Spieler bei einem Tastendruck zurück"""
self.felder = []
self.feldgroesse = feldgroesse
self.feldgroesseD2 = feldgroesse // 2
self.weglaenge = weglaenge
self.blickrichtung = 160
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:
# Pixel-Position!!!
self.position = (idx * self.feldgroesse + self.feldgroesseD2, num * self.feldgroesse + self.feldgroesseD2)
self.felder[num] = self.felder[num].replace('#', '.')
break
self.WEITWEG = self.breite * self.hoehe * self.feldgroesse
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 + self.weglaenge * math.cos(math.radians(self.blickrichtung))))
qy = int(round(py - self.weglaenge * math.sin(math.radians(self.blickrichtung))))
zeichen = self.zeichen(qx // self.feldgroesse, qy // self.feldgroesse)
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, winkel, px, py, fx, fy):
"""findet Wände in ausschließlich horizontaler/vertikaler Richtung"""
rDict = {0 : (1, 0), 90 : (0, -1), 180 : (-1, 0), 270 : (0, 1)}
rx, ry = rDict[winkel]
feldX = fx + rx
feldY = fy + ry
feldZeichen = self.zeichen(feldX, feldY)
while not self.istWand(feldZeichen):
feldX = feldX + rx
feldY = feldY + ry
feldZeichen = self.zeichen(feldX, feldY)
if winkel == 0:
sx = feldX * self.feldgroesse
sy = py
entfernung = sx - px
elif winkel == 90:
sx = px
sy = (feldY + 1) * self.feldgroesse
entfernung = py - sy
elif winkel == 180:
sx = (feldX + 1) * self.feldgroesse
sy = py
entfernung = px - sx
else: # winkel == 270
sx = px
sy = feldY * self.feldgroesse
entfernung = sy - py
return (feldX, feldY, feldZeichen, sx, sy, entfernung)
def findeWandDiagonal(self, px, py, dx1, dy1, dx2, dy2, (kx, ky)):
# Findet Wände in diagonaler Richtung
sx = px + dx1
sy = py + dy1
feldX = int(sx / self.feldgroesse) + kx
feldY = int(sy / self.feldgroesse) + ky
if (0 <= feldX < self.breite) and (0 <= feldY < self.hoehe):
feldZeichen = self.zeichen(feldX, feldY)
else:
return (0, 0, '0', 0, 0, self.WEITWEG)
while not self.istWand(feldZeichen):
sx = sx + dx2
sy = sy + dy2
feldX = int(sx / self.feldgroesse) + kx
feldY = int(sy / self.feldgroesse) + ky
if (0 <= feldX < self.breite) and (0 <= feldY < self.hoehe):
feldZeichen = self.zeichen(feldX, feldY)
else:
return (0, 0, '0', 0, 0, self.WEITWEG)
# wir haben die Wand gefunden
ex = sx - px
ey = sy - py
entfernung = math.sqrt(ex * ex + ey * ey)
return (feldX, feldY, feldZeichen, sx, sy, entfernung)
def findeWand(self, winkel):
winkel = winkel % 360
px, py = self.position
fx = px // self.feldgroesse
fy = py // self.feldgroesse
# Spezialfall wegen Tangens
if winkel % 90 == 0:
return self.findeWandHV(winkel, px, py, fx, fy)
else: # alle anderen Winkel
rdict = {0 : ((0, -1), (0, 0)),
1 : ((0, -1), (-1, 0)),
2 : ((0, 0), (-1, 0)),
3 : ((0, 0), (0, 0))}
quadrant = int(winkel) // 90
tangens = math.tan(math.radians(winkel))
korr1, korr2 = rdict[quadrant]
if quadrant == 0:
# - Schnittpunkte mit horiz. Linien
dy1 = py % self.feldgroesse
dx1 = dy1 / tangens
dy2 = self.feldgroesse
dx2 = dy2 / tangens
rx = 1.0
ry = -1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand1 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr1)
# - Schnittpunkte mit vert. Linien
dx1 = self.feldgroesse - px % self.feldgroesse
dy1 = tangens * dx1
dx2 = self.feldgroesse
dy2 = tangens * dx2
rx = 1.0
ry = -1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand2 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr2)
elif quadrant == 1:
# - Schnittpunkte mit horiz. Linien
dy1 = py % self.feldgroesse
dx1 = dy1 / tangens
dy2 = self.feldgroesse
dx2 = dy2 / tangens
rx = 1.0
ry = -1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand1 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr1)
# - Schnittpunkte mit vert. Linien
dx1 = px % self.feldgroesse
dy1 = tangens * dx1
dx2 = self.feldgroesse
dy2 = tangens * dx2
rx = -1.0
ry = 1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand2 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr2)
elif quadrant == 2:
# - Schnittpunkte mit horiz. Linien
dy1 = self.feldgroesse - py % self.feldgroesse
dx1 = dy1 / tangens
dy2 = self.feldgroesse
dx2 = dy2 / tangens
rx = -1.0
ry = 1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand1 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr1)
# - Schnittpunkte mit vert. Linien
dx1 = px % self.feldgroesse
dy1 = tangens * dx1
dx2 = self.feldgroesse
dy2 = tangens * dx2
rx = -1.0
ry = 1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand2 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr2)
elif quadrant == 3:
# - Schnittpunkte mit horiz. Linien
dy1 = self.feldgroesse - py % self.feldgroesse
dx1 = dy1 / tangens
dy2 = self.feldgroesse
dx2 = dy2 / tangens
rx = -1.0
ry = 1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand1 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr1)
# - Schnittpunkte mit vert. Linien
dx1 = self.feldgroesse - px % self.feldgroesse
dy1 = tangens * dx1
dx2 = self.feldgroesse
dy2 = tangens * dx2
rx = 1.0
ry = -1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand2 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr2)
if wand1[5] < wand2[5]:
return wand1
else:
return wand2
def init(breite, hoehe):
screen = pygame.display.set_mode((breite, hoehe))
screen.fill((200, 200, 200))
pygame.display.update()
return screen
def zeichneSpielfeld(screen, breite, hoehe, spielfeld, wandpos):
feldbreite = breite // spielfeld.breite
feldhoehe = hoehe // spielfeld.hoehe
# Spielfeld zeichnen
for y in xrange(spielfeld.hoehe):
for x in xrange(spielfeld.breite):
zeichen = spielfeld.zeichen(x, y)
rechteck = (x * feldbreite, y * feldhoehe, feldbreite, feldhoehe)
if zeichen == '.':
pygame.draw.rect(screen, (0, 0, 0), rechteck)
elif zeichen == 'N':
pygame.draw.rect(screen, (128, 0, 0), rechteck)
elif zeichen == 'W':
pygame.draw.rect(screen, (160, 43, 43), rechteck)
elif zeichen == 'S':
pygame.draw.rect(screen, (128, 128, 0), rechteck)
elif zeichen == 'O':
pygame.draw.rect(screen, (213, 85, 0), rechteck)
# Spielfigur zeichnen
pos = (int (1.0 * spielfeld.position[0] / spielfeld.feldgroesse * feldbreite), int(1.0 * spielfeld.position[1] / spielfeld.feldgroesse * feldhoehe))
pygame.draw.circle(screen, (255, 255, 255), pos, 10)
# grid zeichnen
for x in xrange(1, spielfeld.breite):
start = (x * feldbreite, 0)
ende = (start[0], hoehe - 1)
pygame.draw.line(screen, (128, 128, 128), start, ende)
for y in xrange(1, spielfeld.hoehe):
start = (0, y * feldhoehe)
ende = (breite - 1, start[1])
pygame.draw.line(screen, (128, 128, 128), start, ende)
# Blickrichtung zeichnen
qx = int(pos[0] + 2 * feldbreite * math.cos(math.radians(spielfeld.blickrichtung)))
qy = int(pos[1] - 2 * feldhoehe * math.sin(math.radians(spielfeld.blickrichtung)))
pygame.draw.line(screen, (0, 255, 0), pos, (qx, qy))
# gefundene Wand zeichnen
wx, wy, zeichen, sx, sy, entfernung = wandpos
wx = wx * feldbreite + feldbreite // 2
wy = wy * feldhoehe + feldhoehe // 2
spos = (int (1.0 * sx / spielfeld.feldgroesse * feldbreite), int(1.0 * sy / spielfeld.feldgroesse * feldhoehe))
pygame.draw.line(screen, (0, 0, 255), pos, spos)
pygame.draw.circle(screen, (0, 0, 255), spos, 5)
pygame.draw.circle(screen, (0, 0, 255), (wx, wy), min(feldhoehe, feldbreite) // 5)
pygame.display.update()
def hauptschleife(spielfeld, screen, breite, hoehe):
wand = spielfeld.findeWand(spielfeld.blickrichtung)
zeichneSpielfeld(screen, breite, hoehe, spielfeld, wand)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
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()
wand = spielfeld.findeWand(spielfeld.blickrichtung)
zeichneSpielfeld(screen, breite, hoehe, spielfeld, wand)
if __name__ == '__main__':
spielfeld = Spielfeld('spielfeld.txt', 64, 10)
screen = init(500, 500)
hauptschleife(spielfeld, screen, 500, 500)
Wandsuche
[Bearbeiten]Die auffälligste Neuerung an diesem Code ist Spielfeld.findeWand(). Wir bedienen uns hier kleinen Tricks, um die Wandsuche im Vergleich zum letzten Kapitel qualitativ zu verbessern.
Wandsuche horizontal/vertikal
[Bearbeiten]Für die Spezialfälle, in denen der aktuell zu untersuchende Winkel ein vielfaches von 90° ist, wird Spielfeld.findeWandHV() aufgerufen. Die Parameter sind die tatsächliche Pixelposition und die aktuelle Feldposition, auf der sich die Spielfigur befindet. In Abhängigkeit vom Winkel wird im Dictionary rDict eine Richtung vorgegeben, in der die zu suchende Wand liegt. Bei einem Winkel von 0° beispielsweise wird in X-Richtung gesucht und die Y-Richtung beibehalten. Die Notation ist dann (1, 0). Für 180° müssen wir allerdings nach links blicken und deswegen jeweils -1 in X-Richtung suchen, die Y-Richtung bleibt erhalten, also (-1, 0). So kommen die Einträge innerhalb dieses Dictionaries zustande. In der Schleife schauen wir uns also immer das nächste Feld an und testen darauf, ob es ein Wandzeichen ist. Ist es so, dann haben wir die Wand gefunden.
Im Fall von 0° ist die X-Position, an der das Wandelement gefunden wurde, einfach feldX * feldgroesse. Im Fall von 180° jedoch finden wir beispielsweise die Wand an der X-Position 0. Der Abstand zu dieser Wand ist dann allerdings zu korrigieren, da wir die Wand ja nicht an der linken, sondern an der rechten Seite schneiden. Finden wir also eine Wand in der Spalte 0, so müssen wir feldgroesse hinzuzählen. Allgemein also (feldX + 1) * self.feldgroesse.
Wandsuche Diagonal
[Bearbeiten]Alle anderen Fälle werden von Spielfeld.findeWandDiagonal(), verarbeitet, die ihrerseits sehr viel Vorbereitung bedarf. Wir übergeben der Methode Spielfeld.findeWandDiagonal() die augenblickliche Pixel-Position, den Abstand, wo diese Methode zuerst suchen soll, also die erste Schrittweite (dx1, dy1), den Abstand, wo danach gesucht werden soll (dx2, dy2) und einen Korrekturwert, auf den wir gleich noch zu sprechen kommen.
Innerhalb von Spielfeld.findeWandDiagonal() wird zuerst die erste Schrittweite (dx1, dy1) gegangen, dann wird geschaut, ob dort eine Wand liegt. Korrekturwerte werden jeweils hinzugezählt. Falls an dieser Stelle die zu untersuchende Wand außerhalb des Spielfeldes liegt, wird einfach angenommen, die Wand ist sehr weit entfernt und ein entsprechender Wert (Spielfeld.WEITWEG) zurückgeliefert. Dann wird unter berücksichtigung der Korrekturwerte das zweite Paar Schrittweiten ((dx1, dy1)) genommen, um Felder zu untersuchen.
Die Vorbereitung von Spielfeld.findeWandDiagonal() innerhalb von Spielfeld.findeWand() nimmt sehr viel Raum ein. Wir rufen Spielfeld.findeWandDiagonal() auch zweimal auf, einmal für die Schnittpunkte mit horizontalen und einmal für die Schnittpunkte mit vertikalen Linien. Dies kennen sie schon aus dem letzten Kapitel. Neu ist, dass wir mit quadrant das Spielfeld von der Spielfigur aus in vier Teile teilen. Der Quadrant 0 enthält alle Winkel, die größer als 0° und kleiner als 90° sind.
Die erste Schrittweite ist bestimmt durch den Abstand zum nächsten Feld. Es wird also der erste mögliche Schnittpunkt mit einer waagerechten und senkrechten Feldbegrenzung (im Anwendungsbild eine Gitterline) gesucht. Die nächsten Schrittweiten werden bestimmt durch die Feldgröße, da Feldbegrenzungen immer Feldgrößen voneinander entfernt sind. dy1 = py % self.feldgroesse hat hier die Bedeutung, die wir im mathematischen Kapitel erläutert haben, nämlich den Abstand der Spielfigur vom oberen Rand zu bestimmen.
Der Tangens ist im Quadrant 0 positiv, die Y-Richtung soll aber negativ sein - wir erinnern uns, dass der Wert (0 ; 0) in der oberen linken Ecke liegt, man also nach oben in den negativen Bereich schaut. Folglich müssen wir alle gefundenen Werte für dy1 und dy2 mit -1.0 multiplizieren. Dafür dient die etwas längliche aber immer gleiche Rechnung, die rx und ry enthält.
Die Korrekturwerte im rdict kommen daher zustande, dass man manchmal das falsche Feld untersucht. Sucht man beispielsweise im Quadrant 0 einen Schnittpunkt mit horizontalen Linien, so rechnet man immer ein Feld zu tief. Die Korrektur von (0, -1) für diesen Fall berichtigt das. Da meistens alles gut läuft, man also nur wenige solcher quadrantabhängigen Korrekturen braucht, ist rdict zumeist mit Nullen besetzt. Übrigens könnte man auch die oben genannte rx- und ry-Werte dort mit eintragen.
Spielfeld zeichnen
[Bearbeiten]Das Spielfeld wird mit der Funktion zeichneSpielfeld() neu gezeichnet. Diese Funktion wird mit der echten Fenstergröße und einigen weiteren Parametern aufgerufen. Da diese echte Fenstergröße nicht unbedingt genau so groß sein muss, wie das Spielfeld, benutzen wir die aus dem mathematischen Kapitel bekannte Methode, um das Spielfeld maßstabsgetreu zu verkleinern. Wenn also die echte Spielfeldbreite in Feldern gemessen spielfeld.breite ist und die echte Feldbreite in Pixeln gemessen spielfeld.feldgroesse ist, dann ist das gesamte echte Spielfeld in Pixeln gemessen gerade spielfeld.breite * spielfeld.feldgroesse breit. Diese Breite muss aber nun in das Programmfenster mit der Breite breite gequetscht werden. Hierbei hilft uns die Variable feldbreite, die die Größe eines einzelnen Feldes auf dem Programmfenster berechnet.
Hat man dann eine Pixel-Position auf dem Spielfeld, kann man mit spielfeld.position[0] / spielfeld.feldgroesse * feldbreite die X-Position im Programmfenster bestimmen. Der Quotient feldbreite / spielfeld.feldgroesse, der in all diesen Berechnungen steckt, ist dann genau das Maß, um das wir alle X-Positionsangaben verkleinern müssen, um eine maßstabsgetreue Positionierung auf dem Fenster zu bekommen.
Freies Gehen im Raum
[Bearbeiten]Das nun folgende Beispiel benutzt die jetzt bekannte Spielfeldklasse, um eine dreidimensionale Raumdarstellung zu erzeugen.
Anmerkung:
- Die Klasse Spielfeld ist hierbei die gleiche wie oben schon beschrieben, wir haben sie weggelassen, um den Quelltext nicht unnötig lang werden zu lassen. Probieren Sie dieses Beispiel aus, dann kopieren Sie sich bitte die oben angeführte Klasse einfach in den Quelltext.
Im Vergleich zu obigem Programm fällt auf, dass wir lediglich die Raumklasse hinzugefügt haben und die Funktion zeichneSpielfeld() deutlich kleiner ausfällt:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import math
import pygame
import sys
class Spielfeld(object):
# Aus Gründen der Übersichtlichkeit weggelassen, Wie oben
pass
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.pHoehe = pHoehe
self.mittelLinie = self.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.
Da Entfernung auch 0 sein kann, erfolgt hier Anpassung"""
if entfernung > 0.0:
h = self.wandHoeheEcht * self.projektionsAbstand / entfernung
else:
h = self.pHoehe
return int(h)
def raycasting(self, spielfeld):
anfangsWinkel = spielfeld.blickrichtung + self.blickfeldWinkelD2
hoehenListe = []
for spalte in xrange(self.spaltenZahl):
winkel = anfangsWinkel - spalte * self.spaltenWinkel
winkel = winkel % 360
feldXpos, feldYpos, feldZeichen, schnittpunktX, schnittpunktY, entfernung = spielfeld.findeWand(winkel)
hoehe = self.berechneHoehe(entfernung)
hoehenListe.append((spalte, feldZeichen, 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)
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', 64, 10)
screen = init(500, 500)
raum = Raum(60, 500, 500, 64)
hauptschleife(raum, spielfeld, screen, 500, 500)
Die Methode berechneHoehe() berechnet wieder die Höhe mit Hilfe des Strahlensatzes, allerdings haben wir es hier mit Pixel-Entferungen zu tun, und müssen deswegen wandHoeheEcht miteinbeziehen. Den Fall, dass der Abstand 0 wird, berücksichtigen wir in der Methode ebenfalls.
Die Modulo-Operation beim Winkel haben wir statt in spielfeld.findeWand() nun direkt in der Methode raycasting() vorgenommen. In den späteren Beispielen werden wir sie dort gebrauchen.
Ansonsten ist die Ausgabe wie im Beispielprogramm im Kapitel "Darstellung des Raumes", allerdings kommen Sie den Wänden wesentlich näher.
Wand mit Textur
[Bearbeiten]Nachdem wir uns nun frei auf dem Spielfeld bewegen können, werden wir uns dem Thema Texturen widmen. Auf der nebenstehenden Abbildung sehen Sie schon, worum es geht: Die Nordwand wird mit einer Textur versehen. Hierzu benötigen wir eine Grafikdatei mit dem Namen wand2N.png, diese sollte so groß sein wie die Feldgröße, also 64 Pixel in diesen Beispielprogrammen.
Das Beispielprogramm enthält einige neue Funktionen, nämlich ladeBild() und bildspalteZoom(). Den Kern dieser Funktionen haben wir Ihnen im Kapitel Grundlagen der Programmierung vorgestellt.
#!/usr/bin/python
# -*- coding: utf-8 -*-
import math
import pygame
import sys
class Spielfeld(object):
# Wie im ersten Beispielprogramm
pass
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.pHoehe = pHoehe
self.mittelLinie = self.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.
Da Entfernung auch 0 sein kann, erfolgt hier Anpassung"""
if entfernung > 0.0:
h = self.wandHoeheEcht * self.projektionsAbstand / entfernung
else:
h = self.pHoehe
return int(h)
def raycasting(self, spielfeld):
anfangsWinkel = spielfeld.blickrichtung + self.blickfeldWinkelD2
hoehenListe = []
for spalte in xrange(self.spaltenZahl):
winkel = anfangsWinkel - spalte * self.spaltenWinkel
winkel = winkel % 360
feldXpos, feldYpos, feldZeichen, schnittpunktX, schnittpunktY, entfernung = spielfeld.findeWand(winkel)
# Abstand, an dem der momentane Winkel eine Wand trifft vom linken Rand der Wand
wandort = int( (schnittpunktX + schnittpunktY) % spielfeld.feldgroesse )
hoehe = self.berechneHoehe(entfernung)
hoehenListe.append((spalte, feldZeichen, hoehe, wandort))
return hoehenListe
def init(breite, hoehe):
screen = pygame.display.set_mode((breite, hoehe))
screen.fill((200, 200, 200))
pygame.display.update()
return screen
def ladeBild(dateiname):
tmp = pygame.image.load(dateiname)
surface = tmp.convert()
return surface
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))
def zeichneSpielfeld(screen, mittellinie, hoehen, bild):
screen.fill((200, 200, 200))
for spalte, zeichen, hoehe, abstand in hoehen:
x0, x1 = spalte, spalte
# Zeichne in der Mitte
y0 = mittellinie - hoehe // 2
y1 = y0 + hoehe
if zeichen == 'N':
bildspalteZoom(abstand, bild, screen, (x0, y0), hoehe)
elif zeichen == 'S':
farbe = (128, 128, 0)
pygame.draw.line(screen, farbe, (x0, y0), (x1, y1), 1)
elif zeichen == 'O':
farbe = (213, 85, 0)
pygame.draw.line(screen, farbe, (x0, y0), (x1, y1), 1)
elif zeichen == 'W':
farbe = (160, 43, 43)
pygame.draw.line(screen, farbe, (x0, y0), (x1, y1), 1)
pygame.display.update()
def hauptschleife(raum, spielfeld, screen, breite, hoehe):
mussZeichnen = False
wandbild = ladeBild('wand2N.png')
hoehen = raum.raycasting(spielfeld)
zeichneSpielfeld(screen, raum.mittelLinie, hoehen, wandbild)
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, wandbild)
mussZeichnen = False
if __name__ == '__main__':
spielfeld = Spielfeld('spielfeld.txt', 64, 10)
screen = init(500, 500)
raum = Raum(60, 500, 500, 64)
hauptschleife(raum, spielfeld, screen, 500, 500)
Korrekte Spalte
[Bearbeiten]Hat man in der Methode raycasting() den Schnittpunkt eines Strahles mit einer Wand bestimmt (schnittpunktX, schnittpunktY), dann ist ein Teil dieses Schnittpunktes genau auf einer Linie zwischen zwei Feldern. An dieser Stelle ist dann entweder schnittpunktX oder schnittpunktY ein Vielfaches von der Feldbreite. Da man gar nicht so genau wissen möchte, an welcher Seite einer Mauer man schneidet, sondern nur, wie groß der Abstand zur Wand ist, reicht (schnittpunktX + schnittpunktY) % spielfeld.feldgroesse aus, um diesen Abstand zu bestimmen. Man weiss dann also den Abstand des Schnittpunktes zu einer Seite der Mauer. Diesen Abstand nennen wir wandort, er ist gleich der Spalte in unserer Textur.
Innerhalb von zeichneSpielfeld() benutzen wir diesen Wandort, um bei Nordwänden mit bildspalteZoom() die passende Spalte aus der Textur auszuwählen und auf die richtige Größe zu zoomen.
Probleme
[Bearbeiten]Leider ist das beschriebene Verfahren, um den Wandort zu bestimmen, nicht gut genug, was man an folgenden Bildern sieht. Hier wird eine Situation auf dem Spielfeld wie auch im Raum gezeigt, bei der die falschen Spalten aus der Textur ausgewählt werden.
Ansicht Spielfeld | Ansicht 3D |
---|---|
Blickt man nämlich nach unten auf eine Textur, wird der falsche Wandort bestimmt. Wir müssen die Texturspalte beim Blick nach unten von rechts auswählen, die oben beschriebene Modulo-Operation sorgt allerdings dafür, dass die Texturspalten von links ausgewählt werden. Wenn Sie einmal mit dem Programm durch den Raum wandern, werden Sie weitere Stellen sehen können, bei denen das nicht passt.
Um dieses Problem kümmern wir uns im nächsten Abschnitt und zeigen darüberhinaus, wie man alle Wände mit Textur belegt.
Alle Wände mit Textur
[Bearbeiten]Das folgende Beispielprogramm ist eine löst das oben beschriebene Problem innerhalb von Raum.raycasting(), in dem alle Fälle, bei denen die Wand falsch gezeichnet würde abfängt und die Spalten korrekt von der anderen Seite aus bestimmt:
wandort = int( (schnittpunktX + schnittpunktY) % spielfeld.feldgroesse )
if (270 > winkel > 90 and schnittpunktX % spielfeld.feldgroesse == 0) or \
(360 > winkel > 180 and schnittpunktY % spielfeld.feldgroesse == 0):
wandort = spielfeld.feldgroesse - wandort - 1
Es wird also bei Winkeln zwischen 90° und 270° geschaut, ob man einen Schnittpunkt mit vertikalen Feldbegrenzungen hat und bei Winkeln zwischen 180° und 360°, ob man hier einen Schnittpunkt mit horizontalen Feldbegrenzungen vorfindet. In diesen Fällen muss von der anderen Seite der Wand die Texturspalte bestimmt werden.
Ebenfalls zeigen wir eine Möglichkeit, wie man mit mehreren Texturen umgeht. Hierzu benötigen wir Texturdateien wand2N.png, wand2S.png, wand2O.png und wand2W.png, die wir in der Hauptschleife laden. Diese Bilddateien müssen wieder so groß sein wie die Feldgröße, in unseren Beispielen 64 Pixel.
#!/usr/bin/python
# -*- coding: utf-8 -*-
import math
import pygame
import sys
class Spielfeld(object):
def __init__(self, dateiname, feldgroesse, weglaenge):
""" Initialisiert das Spielfeld
dateiname - Dateiname der Spielfeld-Datei
feldgroesse - die Pixelgrösse eines quadratischen Feldes
weglaenge - dieses Stück Pixelweg legt der Spieler bei einem Tastendruck zurück"""
self.felder = []
self.feldgroesse = feldgroesse
self.feldgroesseD2 = feldgroesse // 2
self.weglaenge = weglaenge
self.blickrichtung = 160
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:
# Pixel-Position!!!
self.position = (idx * self.feldgroesse + self.feldgroesseD2, num * self.feldgroesse + self.feldgroesseD2)
self.felder[num] = self.felder[num].replace('#', '.')
break
self.WEITWEG = self.breite * self.hoehe * self.feldgroesse
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 + self.weglaenge * math.cos(math.radians(self.blickrichtung))))
qy = int(round(py - self.weglaenge * math.sin(math.radians(self.blickrichtung))))
zeichen = self.zeichen(qx // self.feldgroesse, qy // self.feldgroesse)
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, winkel, px, py, fx, fy):
"""findet Wände in ausschließlich horizontaler/vertikaler Richtung"""
rDict = {0 : (1, 0), 90 : (0, -1), 180 : (-1, 0), 270 : (0, 1)}
rx, ry = rDict[winkel]
feldX = fx + rx
feldY = fy + ry
feldZeichen = self.zeichen(feldX, feldY)
while not self.istWand(feldZeichen):
feldX = feldX + rx
feldY = feldY + ry
feldZeichen = self.zeichen(feldX, feldY)
if winkel == 0:
sx = feldX * self.feldgroesse
sy = py
entfernung = sx - px
elif winkel == 90:
sx = px
sy = (feldY + 1) * self.feldgroesse
entfernung = py - sy
elif winkel == 180:
sx = (feldX + 1) * self.feldgroesse
sy = py
entfernung = px - sx
else: # winkel == 270
sx = px
sy = feldY * self.feldgroesse
entfernung = sy - py
return (feldX, feldY, feldZeichen, sx, sy, entfernung)
def findeWandDiagonal(self, px, py, dx1, dy1, dx2, dy2, (kx, ky)):
# Findet Wände in diagonaler Richtung
sx = px + dx1
sy = py + dy1
feldX = int(sx / self.feldgroesse) + kx
feldY = int(sy / self.feldgroesse) + ky
if (0 <= feldX < self.breite) and (0 <= feldY < self.hoehe):
feldZeichen = self.zeichen(feldX, feldY)
else:
return (0, 0, '0', 0, 0, self.WEITWEG)
while not self.istWand(feldZeichen):
sx = sx + dx2
sy = sy + dy2
feldX = int(sx / self.feldgroesse) + kx
feldY = int(sy / self.feldgroesse) + ky
if (0 <= feldX < self.breite) and (0 <= feldY < self.hoehe):
feldZeichen = self.zeichen(feldX, feldY)
else:
return (0, 0, '0', 0, 0, self.WEITWEG)
# wir haben die Wand gefunden
ex = sx - px
ey = sy - py
entfernung = math.sqrt(ex * ex + ey * ey)
return (feldX, feldY, feldZeichen, sx, sy, entfernung)
def findeWand(self, winkel):
px, py = self.position
fx = px // self.feldgroesse
fy = py // self.feldgroesse
# Spezialfall wegen Tangens
if winkel % 90 == 0:
return self.findeWandHV(winkel, px, py, fx, fy)
else: # alle anderen Winkel
rdict = {0 : ((0, -1), (0, 0)),
1 : ((0, -1), (-1, 0)),
2 : ((0, 0), (-1, 0)),
3 : ((0, 0), (0, 0))}
quadrant = int(winkel) // 90
tangens = math.tan(math.radians(winkel))
korr1, korr2 = rdict[quadrant]
if quadrant == 0:
# - Schnittpunkte mit horiz. Linien
dy1 = py % self.feldgroesse
dx1 = dy1 / tangens
dy2 = self.feldgroesse
dx2 = dy2 / tangens
rx = 1.0
ry = -1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand1 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr1)
# - Schnittpunkte mit vert. Linien
dx1 = self.feldgroesse - px % self.feldgroesse
dy1 = tangens * dx1
dx2 = self.feldgroesse
dy2 = tangens * dx2
rx = 1.0
ry = -1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand2 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr2)
elif quadrant == 1:
# - Schnittpunkte mit horiz. Linien
dy1 = py % self.feldgroesse
dx1 = dy1 / tangens
dy2 = self.feldgroesse
dx2 = dy2 / tangens
rx = 1.0
ry = -1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand1 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr1)
# - Schnittpunkte mit vert. Linien
dx1 = px % self.feldgroesse
dy1 = tangens * dx1
dx2 = self.feldgroesse
dy2 = tangens * dx2
rx = -1.0
ry = 1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand2 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr2)
elif quadrant == 2:
# - Schnittpunkte mit horiz. Linien
dy1 = self.feldgroesse - py % self.feldgroesse
dx1 = dy1 / tangens
dy2 = self.feldgroesse
dx2 = dy2 / tangens
rx = -1.0
ry = 1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand1 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr1)
# - Schnittpunkte mit vert. Linien
dx1 = px % self.feldgroesse
dy1 = tangens * dx1
dx2 = self.feldgroesse
dy2 = tangens * dx2
rx = -1.0
ry = 1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand2 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr2)
elif quadrant == 3:
# - Schnittpunkte mit horiz. Linien
dy1 = self.feldgroesse - py % self.feldgroesse
dx1 = dy1 / tangens
dy2 = self.feldgroesse
dx2 = dy2 / tangens
rx = -1.0
ry = 1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand1 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr1)
# - Schnittpunkte mit vert. Linien
dx1 = self.feldgroesse - px % self.feldgroesse
dy1 = tangens * dx1
dx2 = self.feldgroesse
dy2 = tangens * dx2
rx = 1.0
ry = -1.0
dx1 = rx * dx1
dy1 = ry * dy1
dx2 = rx * dx2
dy2 = ry * dy2
wand2 = self.findeWandDiagonal(px, py, dx1, dy1, dx2, dy2, korr2)
if wand1[5] < wand2[5]:
return wand1
else:
return wand2
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.pHoehe = pHoehe
self.mittelLinie = self.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.
Da Entfernung auch 0 sein kann, erfolgt hier Anpassung"""
if entfernung > 0.0:
h = self.wandHoeheEcht * self.projektionsAbstand / entfernung
else:
h = self.pHoehe
return int(h)
def raycasting(self, spielfeld):
anfangsWinkel = spielfeld.blickrichtung + self.blickfeldWinkelD2
hoehenListe = []
for spalte in xrange(self.spaltenZahl):
winkel = anfangsWinkel - spalte * self.spaltenWinkel
winkel = winkel % 360
feldXpos, feldYpos, feldZeichen, schnittpunktX, schnittpunktY, entfernung = spielfeld.findeWand(winkel)
# Abstand, an dem der momentane Winkel eine Wand trifft vom linken Rand der Wand
wandort = int( (schnittpunktX + schnittpunktY) % spielfeld.feldgroesse )
if (270 > winkel > 90 and schnittpunktX % spielfeld.feldgroesse == 0) or \
(360 > winkel > 180 and schnittpunktY % spielfeld.feldgroesse == 0):
wandort = spielfeld.feldgroesse - wandort - 1
hoehe = self.berechneHoehe(entfernung)
hoehenListe.append((spalte, feldZeichen, hoehe, wandort))
return hoehenListe
def init(breite, hoehe):
screen = pygame.display.set_mode((breite, hoehe))
screen.fill((200, 200, 200))
pygame.display.update()
return screen
def ladeBild(dateiname):
tmp = pygame.image.load(dateiname)
surface = tmp.convert()
return surface
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))
def zeichneSpielfeld(screen, mittellinie, hoehen, bildN, bildS, bildO, bildW):
screen.fill((200, 200, 200))
for spalte, zeichen, hoehe, abstand in hoehen:
x0, x1 = spalte, spalte
# Zeichne in der Mitte
y0 = mittellinie - hoehe // 2
y1 = y0 + hoehe
if zeichen == 'N':
bildspalteZoom(abstand, bildN, screen, (x0, y0), hoehe)
elif zeichen == 'S':
bildspalteZoom(abstand, bildS, screen, (x0, y0), hoehe)
elif zeichen == 'O':
bildspalteZoom(abstand, bildO, screen, (x0, y0), hoehe)
elif zeichen == 'W':
bildspalteZoom(abstand, bildW, screen, (x0, y0), hoehe)
pygame.display.update()
def hauptschleife(raum, spielfeld, screen, breite, hoehe):
mussZeichnen = False
wandbildN = ladeBild('wand2N.png')
wandbildS = ladeBild('wand2S.png')
wandbildO = ladeBild('wand2O.png')
wandbildW = ladeBild('wand2W.png')
hoehen = raum.raycasting(spielfeld)
zeichneSpielfeld(screen, raum.mittelLinie, hoehen, wandbildN, wandbildS, wandbildO, wandbildW)
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, wandbildN, wandbildS, wandbildO, wandbildW)
mussZeichnen = False
if __name__ == '__main__':
spielfeld = Spielfeld('spielfeld.txt', 64, 10)
screen = init(500, 500)
raum = Raum(60, 500, 500, 64)
hauptschleife(raum, spielfeld, screen, 500, 500)