Home » Tutorials » Grafik und Spiele » Bewegungen in 3D-Welten

Bewegungen in 3D-Welten

Orientierungsprobleme?

Ihr ahnt es schon: Wenn da Winkel im Spiel sind, werden die wohl irgendwie auch einen Einfluss auf die Bewegungen haben, die wir über die Pfeiltasten steuern.
So z.B. sehen die ursprünglichen Versionen für die Bewegungen nach links oder rechts, vorwärts oder rückwärts aus:

ViewVector.x := ViewVector.x - xDiff;
ViewVector.x := ViewVector.x + xDiff;
ViewVector.z := ViewVector.z - zDiff;
ViewVector.z := ViewVector.z + zDiff;

Das genügt auch, solange ich mich als Betrachter parallel zur einer der drei Achsen (x, y, z) der aktuellen Spielwelt bewege. Denn dann komme ich noch ohne Winkel aus. Entlang der z-Achse behält ViewVector.x den Wert Null, bewege ich mich parallel zur x-Achse, ist der Wert von ViewVector.z Null.
Für die übrigen Richtungen aber müssen sich beide Werte ändern. Weil es hier um Drehwinkel geht, benötigen wir Funktionen, die mit Winkeln umgehen können – nämlich Sinus und Kosinus.
Jeder Sinus- oder Kosinuswert gehört zu einem ganz bestimmten Winkel, wobei der allerdings nicht im meist im Mathematikunterricht üblichen Gradmaß, sondern im so genannten Bogenmaß gemessen wird, das sich nach dieser Formel umrechnen lässt:

Bogenwert := Gradwert * Pi / 180;

Zum besseren Verständnis sind hier ein paar Vergleichswerte (von Vierteldrehung bis Volldrehung):

Gradmaß Bogenmaß Umrechnung
„nichts“ 0 * Pi / 180
Viertelkreis 90° pi / 2 90 * Pi / 180
Halbkreis 180° pi 180 * Pi / 180
Vollkreis 360° 2 pi 360 * Pi / 180

Und so ergibt sich eine recht komplexe Formel, denn jede Bewegung wirkt sich sowohl auf x als auch auf z aus, wenn sie in jede Richtung möglich sein soll:

// vorwärts
ViewVector.x := ViewVector.x - zDiff * sin(TurnVector.y);
ViewVector.z := ViewVector.z - zDiff * cos(TurnVector.y);
// rückwärts
ViewVector.x := ViewVector.x + zDiff * sin(TurnVector.y);
ViewVector.z := ViewVector.z + zDiff * cos(TurnVector.y);
// nach links
ViewVector.x := ViewVector.x - xDiff * cos(TurnVector.y);
ViewVector.z := ViewVector.z + xDiff * sin(TurnVector.y);
// nach rechts
ViewVector.x := ViewVector.x + xDiff * cos(TurnVector.y);
ViewVector.z := ViewVector.z - xDiff * sin(TurnVector.y);

Je nach Richtung wird der Wert von xDiff bzw. zDiff mit dem Sinus oder Kosinus von TurnVector.y multipliziert. Die Ergebnisse sind dann die Werte, um die sich anschließend ViewVector.x und ViewVector.z ändern müssen.
Die Formeln für den y-Wert dagegen kommen ohne Sinus und Kosinus aus: Eine Drehung nach oben oder unten ist keine Bewegung, sondern ein Umschauen: Die Betrachterposition bleibt wie sie ist, denn sonst würde man beim „Nach-oben-Schauen“ auch nach oben wandern.
Eigentlich erwartet Ihr längst den kompletten Quelltext für die Maus- bzw. die Tastenmethode. Aber es fehlt noch etwas Wichtiges: Damit wir beim Wandern durch eine Spielwelt nicht durch Wände oder Hindernisse einfach so hindurchgehen können, ist eine so genannte Kollisionskontrolle nötig.
Wenn man mit Schwung gegen eine geschlossene Tür oder einen Baum prallt, ist das ein sehr gutes (wenn auch schmerzhaftes) Beispiel für eine Kollision. Und so hat die entsprechende Genesis-Methode auch mit Collision den passenden Namen:

geWorld_Collision (World, @MinVector, @MaxVector, @LookVector,
  @ViewVector, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL, 0, nil,
  nil, @KontaktInfo));

Dass hier eine ganze Kette von Parametern verlangt wird, sollte uns nicht abschrecken. Schauen wir uns das einmal näher an:
Zuerst kommt die Spielwelt (genauer: ein Zeiger mit der Adresse, an der die Weltdaten im Speicher zu finden sind). Mit MinVector und MaxVector vereinbaren wir ein neues Pärchen von Vektoren. Sie bestimmen, in welchem Bereich eines Hindernisses wie z.B. einer Wand für uns als Betrachter eine Kollision vorliegt. Standardmäßig könnte man beide so initialisieren:

geVec3d_Set(@MinVector, 0.0, 0.0, 0.0);
geVec3d_Set(@MaxVector, 0.0, 0.0, 0.0);

So kommt es zu einer Kollision, sobald wir direkt auf ein Hindernis stoßen (z.B. mit der Stirn an eine Wand geraten). Besser aber ist es, schon eine Kollision auszulösen, wenn wir uns einem Hindernis zu sehr nähern. Deshalb lassen sich die Grenzen über die Werte von MinVector und MaxVector verstellen:

geVec3d_Set(@MinVector,-20.0,-20.0,-20.0);
geVec3d_Set(@MaxVector, 20.0, 20.0, 20.0);

So bekommt die Kamera einen (unsichtbaren) Quader als Körper – englisch auch Bounding Box genannt. Wären alle Werte Null, so wäre die Kamera nur ein Punkt. Wir aber wollen ja so tun, als wäre die mit den Pfeiltasten bewegte und der Maus gedrehte Kamera ein Spieler – nämlich wir selbst, die wir durch die Kamera schauen. (Statt Kameraposition sagt man ja auch Betrachterposition.)
Beim nächsten Parameterpaar treffen wir zunächst mit ViewVector einen alten Bekannten. Neu dagegen ist LookVector: Während in ViewVector der aktuelle Standpunkt des Betrachters gespeichert ist, erhält LookVector die (mögliche) Zielposition:
Genesis3D muss bei einer Kollisionsabfrage ja wissen, wo wir als Betrachter gerade stehen und wo wir hingehen wollen, um beurteilen zu können, ob wir gleich vor eine Mauer laufen. Und erst, wenn es am gesetzten Ziel kein Hindernis gibt, wird der Weg dorthin freigegeben.
In den folgenden Parametern geht es um so genannte Flags. Das sind vereinfacht gesagt Schalter u.a. für die Art des Hindernisses (z.B. Person, Gegenstand) und seine Beschaffenheit (z.B. Stein, Wasser, Feuer). In unserem Fall werden alle Kollisionen mit festen Gegenständen (wie z.B. Wänden) kontrolliert:

GE_CONTENTS_SOLID_CLIP  // aus "festem" Material
GE_COLLIDE_ALL          // Kollision mit allen Hindernis-Arten

Der letzte Parameter schließlich ist eine Art Unfallbericht: Ich nenne diese Variable KontaktInfo, weil sie Informationen über den „Kontakthergang“ und die beteiligten Akteure bzw. Hindernisse enthält. Diese Struktur lässt sich später auswerten, wenn sich in einem Spiel verschiedene andere Wesen tummeln und dem Spieler allerlei Dinge begegnen, von denen er ja möglicherweise auch einige aufsammeln möchte.
Vereinbart werden alle neuen Vektoren wie die alten:

LookVector: geVec3D;  // neue Betrachterposition
MinVector : geVec3D;  // min. Kollisionsgrenze
MaxVector : geVec3D;  // max. Kollisionsgrenze

Bevor der Kollisionstest ausgeführt werden kann, müssen alle Vektoren die passenden Werte enthalten. Bei MinVector und MaxVector ist das ohnehin der Fall, nachdem sie in CreateGame initialisiert wurden. ViewVector enthält immer den aktuellen Standpunkt. LookVector wird erst einmal in CreateGame mit den Startwerten von ViewVector versehen. Das führt uns zu folgender Erweiterung gegenüber G4DGAME1.PAS:

geVec3d_Set(@ViewVector, 0.0, 0.0, 0.0);
LookVector := ViewVector;
geVec3d_Set(@TurnVector, 0.0, 0.0, 0.0);
geVec3d_Set(@MinVector,-20.0,-20.0,-20.0);
geVec3d_Set(@MaxVector, 20.0, 20.0, 20.0);

Als Nächstes benötigen wir eine eigene Kollisionsfunktion als Methode von TForm1:

function TForm1.Collision: Boolean;
var KontaktInfo: GE_Collision;
begin
  Result := Boolean(geWorld_Collision (World, @MinVector, @MaxVector,
    @LookVector, @ViewVector, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL,
    0, nil, nil, @KontaktInfo));
end;

Warum eigentlich eine eigene Methode, in der doch nur der Aufruf von geWorld_Collision steht? Ganz einfach: Es gibt noch eine ganze Reihe von Fällen, in denen eine Kollision näher untersucht werden muss. Dazu gehören Treppen, die man hinauf- oder hinunter gehen will. Oder Türen, die sich öffnen, wenn man sich ihnen nähert.
Grundsätzlich müssen ViewVector und LookVector beim Start dieselben Werte haben – daher auch die direkte Zuweisung. Wenn Ihr eigene Welten ausprobiert, kann es durchaus passieren, dass Eure Position außerhalb der Raumwände liegt und Ihr nicht mehr hineinkommt. Möglicherweise ist auch die Stelle so ungünstig, dass Ihr Euch nicht mal mehr bewegen könnt. Da hilft nur Experimentieren mit den Werten von ViewVector.