Spieler Physik
Autor: Jonas Knerr
Bewegung
Da wir nicht nur einen Ghost
haben wollten, der über die Welt fliegen kann, mussten wir zusätzlich eine Spieler Physik Entwickeln. Grundlegende Dinge, wie Bewegung durch W,A,S,D und die Mausbewegungen waren durch den Ghost
schon gegeben. Diese haben wir dann entsprechen angepasst um eine ordentliche Spieler Physik zu erhalten.
Der Kern der Spieler Physik ist die Update Funktion, diese wird in jeder Iteration aufgerufen. Dort wird dann auf die entsprechenden Tasten, die gedrückt werden reagiert.
Da der Spieler im Gegensatz zum Ghost
eine gewisse Beschleunigung braucht um auf seine Höchstgeschwindigkeit zu bekommen, haben wir eine Exponentialfunktion implementiert. Durch diese erhalten wir eine realitätsnahe Beschleunigung und zusätzlich eine Maximalgeschwindigkeit, die nicht überschritten werden kann.
if self.acceleration.x != 0.0 {
self.velocity.x = self.acceleration.x * delta * delta * self.shift_speed *
(1.0 - (-((self.timer_velx * delta) / (1.0))).exp());
}
Im Eventhandler
wird falls die "W" Taste gedrückt wird die Beschleunigung in X-Richtung auf 100 gesetzt, mit dieser wird dann pro Iteration die entsprechende Geschwindigkeit des Spieler in X-Richtung erhöht. Am Ende jeder Iteration wird dann noch die getestet ob eine Kollision entsteht, falls dies nicht der Fall ist wird die Kamera des Spielers um die Entsprechende Strecke in X- Richtung nach vorne bewegt.
if self.velocity.x > 0.0001 || self.velocity.x < -0.0001 {
self.velocity.x /= 1.4;
} else {
self.velocity.x = 0.0;
}
Falls der Spieler die "W" Taste los lässt wird die Beschleunigung auf 0 gesetzt. Dann wird durch eine angenäherte Reibung die Geschwindigkeit des Spielers langsam verringert, damit kein abruptes abstoppen entsteht. Da wir immer durch 1.4 teilen, wird der Wert nie 0 ergeben, deswegen wird ab einem Wert kleiner als 0.0001 der Wert auf null gesetzt.
Bei der Rückwärtsbewegung wird die Beschleunigung in X-Richtung, auf einen negativen Wert initialisiert. Somit kann man dieselbe Methode wie oben angegeben verwenden um die Rückwärtsbewegung zu berechnen. Zusätzlich ist der Wert bei einer Rückwärtsbewegung nicht ganz so groß, da man sich rückwärts nicht im gleichen Tempo wie vorwärts bewegen kann. Die Rechts/-Linksbewegung wird nach dem gleichen Prinzip realisiert. Statt sich aber in X-Richtung zu bewegen, bewegt sich die Kamera nun in Y-Richtung. Auch hier wird die Beschleunigung des Spieler durch Reibung langsam wieder auf 0 gesetzt. Da X und - Y-Richtung separat behandelt werden ist es auch mögliche sich schräg zu Bewegen.
Höhe
Nun hat der Spieler die Möglichkeit sich in alle Richtungen zu bewegen, aber leider nur auf einer Ebene. Um den Spieler auf den HexPillars
laufen zu lassen, müssen wir uns also die Höhe der HexPillars
holen. Dies geschieht in der get_ground_height_at
Methode. Diese berechnet die Höhe des HexPillars
an der Position des Spielers. Um an die Höhe des HexPillars
zu kommen müssen wir uns einen AxialPoint
berechnen, durch den AxialPoint
bekommt man den entsprechenden HexPillar
auf dem der Spieler steht.
Dadurch können wir die Höhe des Spielers auf die entsprechende Höhe der Welt setzen, da die Welt aber nur in den wenigsten stellen eben ist, muss die Höhe des Spielers immer wieder abgeglichen werden. Zusätzlich wird zur Höhe des Spielers noch ein Wert von 1.75 addiert, damit der Spieler nicht direkt in der Welt steht, sondern "auf" der Welt.
Dadurch ist es aber immer noch nicht möglich sich in bzw. durch Höhlen zu bewegen. Um dies auch noch realisieren zu können muss der Spieler nicht nur auf die höchste Höhe eines HexPillars
gesetzt werden, sondern es muss berechnet werden welche Höhe die richtige ist. Wenn man z.B. abwärts geht und vor einem eine Höhle auftaucht, sollte man weiter in die Höhle gehen und nicht auf der Höhle landen. Deswegen werden die HexPillar
noch auf die unterschiedlichen Höhen untersucht.
Jeder HexPillar
ist in verschieden Sections
aufgeteilt. In den Sections
wird aber nur abgespeichert, wo Material vorhanden ist. Da man sich bei der Spieler-Physik aber die Teile der Sections
wichtig sind, in denen kein Material vorhanden ist, müssen wir alle Sections
durchgehen und diese mit unserer Höhe vergleichen um zu wissen auf welche Höhe wir den Spieler setzen müssen.
if new_pillar_vec[i].top.to_real() < self.cam.position.z &&
self.cam.position.z < new_pillar_vec[i + 1].bottom.to_real() {
height = new_pillar_vec[i].top.to_real();
above = new_pillar_vec[i + 1].bottom.to_real();
break;
}
Dazu überprüfen wir, ob die Höhe des Spielers zwischen height
und above
liegt. Falls dies der Fall ist, liegt ein Loch vor dem Spieler und er kann sich in dieses hinein Bewegen, falls nicht liegt kein Loch vor und der Spieler wird auf die höchste Höhe des HexPillars
gesetzt . Zu der entsprechenden Höhe geben wir die Höhe des anfangs der nächsten Section
zurück(above
), damit der Spieler sich nicht in beliebig kleine Löcher bewegen.
Sprung
if self.velocity.z != 0.0 {
self.velocity.z += (-delta * GRAVITY) * 0.2;
if self.cam.position.z + self.velocity.z > above {
self.velocity.z = 0.0;
} else {
self.cam.move_up(self.velocity.z);
}
Außerdem hat der Spieler ein Sprung implementiert. Um zu Springen muss man die Leertaste betätigen. Dann wird die Beschleunigung des Spielers in Z-Richtung hoch gesetzt. Damit der Spieler nicht in einen HexPillar
rein springt, vergleichen wir die Höhe des Spielers beim Sprung mit dem above
des HexPillars
. Die Beschleunigung des Spielers wird dann durch die Gravitation automatisch verringert. Dies hat auch zur Folge, dass der Spieler auch über längere Distanzen sehr geschmeidig auf den Boden fällt. Dies lässt den Spieler auch angenehm von einem HexPillar
zum anderen Fallen.
Kollision
Autor: Roman Schott
Da nun alle möglichen Bewegungen für den Spieler implementiert sind, fehlt nur noch die Kollision des Spielers an den Wänden. Bevor die Implementierung erfolgen kann, sind gewisse Informationen nötig wie die Position aller umliegenden HexPillar
, dazu auch die Höhe der HexPillar
und auch der Boden des darüber liegenden HexPillar
, falls sich darüber ein HexPillar
befindet. Diese Informationen lassen sich durch die Funktion get_ground_height_at
ermitteln indem nicht die aktuelle Position des Spielers übergeben wird, sondern alle nächstmöglichen Positionen.
Um die nächstmögliche Position zu bekommen, wird der aktuelle Richtungsvektor der Kamera benötigt. Wie auch bei dem Ghost
geschieht dies durch die beiden Variablen theta
und phi
.
let vec_0 = Vector2 {
x: self.cam.phi.cos(),
y: self.cam.phi.sin(),
};
Wobei anzumerken ist, dass das theta
nicht benötigt wird, da die Kollision nur in der horizontalen Richtung erfolgen soll. Der erhaltene Richtungsvektor wird in einer Variable von Typ Vector2
gespeichert. Um auch nun alle anderen Richtungsvektoren zu erhalten, sollte dafür folgende Grafik betrachtet werden:
Wie man an der Grafik deutlich sehen kann befinden sich alle benötigten HexPillar
in einem Winkel von 60° zu dem vorhin ermittelten Richtungsvektor. Dabei ist das egal wo genau der Spieler zu dem HexPillar
schaut oder wo der Spieler sich auf dem HexPillar
befindet. Es sind immer alle umliegenden HexPillar
durch die Vektoren sichtbar. Um auch die zu ermitteln wird folgende Rotationsmatrix verwendet:
let vec_60 = Vector2 {
x: vec_0.x * angle.cos() + vec_0.y * angle.sin(),
y: vec_0.x * (-(angle.sin())) + vec_0.y * angle.cos(),
};
Hier ist ein Beispiel, wie die einzelnen Richtungsvektoren berechnet werden. Dabei wird der Vektor von vorhin verwendet und anhand der Matrix multipliziert. Der Verständlichkeit halber werden sämtliche errechneten Vektoren mit deren Winkel im Variablennamen gespeichert (vec_0
, vec_60
, vec_120
, vec_180
, vec_240
, vec_300
).
Da nun alle Vektoren vorhanden sind, müssen jetzt zu den einzelnen Vektoren die benötigten Daten der HexPillar
bestimmt werden. Darunter die Höhe, die der Spieler erhalten würde wenn er sich in die Richtung bewegen würde sowie, ob sich ein HexPillar
über dem Spieler befinden würde. Diese Informationen erhält man aus den sections
die beim Aufruf der Funktion get_ground_height_at
ermittelt und genutzt werden. Um den Aufruf der Funktion im späteren Verlauf der Kollisionsprüfung zu vermeiden, werden sämtliche, erhaltene Daten in einem Tupel
gespeichert. Diese sind ebenfalls wie die Vektoren mit dem Wert des Winkels abgespeichert.
let pillar_0 = (vec_0, self.get_ground_height_at(vec_0).0.unwrap_or(0.0), self.get_ground_height_at(vec_0).2.unwrap_or(0.0));
Da nun alle Informationen beisammen sind folgt letztendlich die Kollisionsabfrage. Dabei wird überprüft in welche Richtung der Spieler sich bewegt und anhand der Informationen ermittelt, ob sich an der Position ein nicht erreichbarer HexPillar
befindet sei es eine Lücke oder ein zu hoher HexPillar
.
//Movement Forward
if (self.velocity.x > 0.001 &&
(self.get_ground_height_at(Vector2 {
x: vec_0.x - 0.2,
y: vec_0.y - 0.1,
})
.0
.unwrap_or(0.0) > height + self.step_size ||
self.get_ground_height_at(Vector2 {
x: vec_0.x + 0.2,
y: vec_0.y + 0.1,
})
.0
.unwrap_or(0.0) > height + self.step_size ||
pillar_0.1 > height + self.step_size)) || (pillar_0.2 < 3.0) {
if (self.velocity.y > 0.001 && pillar_60.1 > height + self.step_size) ||
pillar_60.2 < 3.0 {
self.velocity.x = 0.0;
self.velocity.y = 0.0;
}
if (self.velocity.y < -0.001 && pillar_300.1 > height + self.step_size) ||
pillar_300.2 < 3.0 {
self.velocity.x = 0.0;
self.velocity.y = 0.0;
} else {
self.velocity.x = 0.0;
}
}
Der Quellcode oben ist ein Beispiel für die Bewegung nach vorne und zusätzliche, seitliche Bewegungen. Zunächst einmal wird untersucht in welche Richtung der Spieler sich bewegt. Danach erfolgt der Test, ob sich in der entsprechenden Richtung ein HexPillar
befindet, dessen Höhe größer ist als die Position des Spielers plus der möglichen Bewegung in die Höhe, dass durch die Variable step_size
gesetzt wird. Wie man aber oben erkennen kann ist dies nicht der einzige Test. Zusätzlich werden noch zwei kleine, leicht dem Richtungsvektor abweichende Vektoren überprüft, um einen speziellen Fall auszuschließen, der in dieser Grafik erläutert wird.
Falls der Spieler sich nicht direkt auf einen HexPillar
zubewegt, sondern an einem entlang läuft (siehe blauer Pfeil), kann es sein, dass der Vektor den falschen HexPillar
auswählt. Dadurch ist es möglich für den Spieler in den an ihm nebenliegenden HexPillar
zu laufen und er würde sich somit in einem HexPillar
befinden, was bei einer Kollision nicht geschehen darf. Daher werden sowohl links als auch rechts von dem Richtungsvektor zusätzlich Vektoren überprüft um diesen Fall auszuschließen. Zu guter Letzt werden auch die Höhenunterschiede überprüft, damit es dem Spieler nicht möglich ist zwischen einen HexPillar
zu laufen, dessen Höhenunterschied kleiner als die Höhe des Spielers ist. Nachdem auch das abgeschlossen ist, kann es durchaus sein, dass der Spieler zusätzlich nach rechts oder nach links läuft. Daher eine zusätzliche Abfrage ob dies der Fall ist durch die in Bewegungsrichtung, anliegenden HexPillar
. Auch hier wird der Höhenunterschied abgefragt. Sobald ein HexPillar
erkannt wird, der die Bewegung des Spielers blockiert, wird die Geschwindigkeit vom Spieler auf 0.0
gesetzt. Dieses Verfahren wird für sämtliche Bewegungsrichtungen des Spielers verwendet.
Erst nachdem der Kollisionstest erfolgt ist, wird am Ende der update
-Funktion die horizontale Bewegung des Spielers durchgeführt durch den Aufruf der Funktionen:
self.cam.move_forward(self.velocity.x);
self.cam.move_right(self.velocity.y);