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

Bewegungen in 3D-Welten

Bewegungsformen und Vektoren

Wenn wir uns in der Spielwelt bewegen, sehen wir alles durch die Kamera, als wären es unsere eigenen Augen – und damit sind wir selbst die Figur. Diese Ich-Perspektive findet Ihr z.B. in Spielen wie Quake, Half-Life oder Unreal.
Ebenso werden wir jetzt durch unsere BSP-Welt wandern. Dabei kommen diese Bewegungsformen infrage:

  • Die Translation ist das Verschieben von Objekten und Räumen. Die Genesis-Methode dazu heißt geXForm3D_Translate.
  • Die Rotation ist das Drehen von Objekten und Räumen. Für jede Achse gibt es eine Methode: geXForm3D_RotateX, geXForm3D_RotateY und geXForm3D_RotateZ.
  • Die Skalierung ist das Vergrößern oder Verkleinern von Objekten, Räumen oder Teilen davon. Die Methode dazu heißt geXForm3D_Scale.

In Wirklichkeit werden nur die Werte einer einzigen Matrix verändert, nämlich XForm. Die Genesis-Engine besorgt den Rest.
Während Verschiebung und Skalierung mit einfachen rechnerischen Mitteln zu regeln sind, benötigen wir für die Rotationen schon ein wenig „Höhere Mathematik“.
Auf eine Skalierung können wir hier sogar ganz verzichten, weil wir die Weltansicht nicht in ihrer Ursprungsgröße ändern wollen. Dass Objekte sich in ihrer Größe ändern, wenn wir uns ihnen nähern oder uns entfernen, regelt Genesis3D automatisch.

  • Verschiebung heißt für uns: Wir gehen vorwärts oder rückwärts oder treten ein paar Schritte zur Seite. Dazu benutzen wir die Pfeiltasten.
  • Drehung heißt für uns: Wir wenden uns nach links oder rechst (oder drehen uns im Kreis). Oder wir schauen nach oben oder unten. Dazu verwenden wir die Maus.

Weil wir beim Bewegen unsere Betrachterposition ändern, müssen wir deren Daten ständig auf dem aktuellen Stand halten. Dazu vereinbaren wir mit ViewVector einen Vektor, der den jeweiligen Koordinatenwert für x, y und z aufnimmt:

ViewVector: geVec3D;  // aktuelle Betrachterposition

Deshalb muss die Unit G4G_Vec3D ebenfalls in die uses-Liste (Änderung gegenüber G4DGAME1.PAS).
Je nach Bewegungsrichtung ändert sich nur ein Wert des Vektors:

ViewVector.x Verschiebung nach links oder rechts
ViewVector.y Verschiebung nach oben oder unten
ViewVector.z Verschiebung nach vorn oder hinten

Die jeweilige Schrittweite wird in drei Konstanten festgelegt, die Ihr an die Leistung Eurer Grafikkarte anpassen müsst:

const xDiff=4.0; yDiff=2.0; zDiff=6.0;  // größer = schneller

Und je nach Richtung wird der entsprechende Diff-Wert addiert oder subtrahiert – z.B. für die Vorwärts- oder Rückwärtsbewegung:

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

Der neue Vektor muss beim Programmstart erst einmal „genullt“ werden. Dazu benutzen wir die Methode geVec3d_Set. Das macht in der Methode TForm1.CreateGame ganz am Schluss eine Ergänzung nötig (Änderung gegenüber G4DGAME1.PAS):

geVec3d_Set(@ViewVector, 0.0, 0.0, 0.0);

Würden wir das Programm so laufen lassen, könnten wir nun zwar die betreffenden Tasten drücken und die Werte von ViewVector würden sich auch entsprechend ändern. Aber zu sehen bekommen wir davon leider nichts. Deshalb hat auch TForm1.RunGame eine Änderung (gegenüber G4DGAME1.PAS) nötig:

geXForm3D_Translate(@XForm, ViewVector.x, ViewVector.y, ViewVector.z);

Denn der ViewVector bietet immer die aktuellen Werte, um die der Betrachter sich weiter bewegt hat.
Außerdem müssen wir ebenfalls in der Wiederholungsschleife von RunGame den Aufruf der Methode unterbringen, die die Tastatur abfragen soll. Ein guter Platz liegt z.B. direkt hinter Application.ProcessMessages. Nennen wir die Methode GetInput, so sieht der betreffende Teil in TForm1.Rungame (gegenüber G4DGAME1.PAS) so aus:

// Neu in TForm1.RunGame
Application.ProcessMessages;
// Tasten abfragen
GetInput;
// Koordinaten und Winkel ausrichten
geXForm3D_SetIdentity (@XForm);

Für die Drehung benötigen wir einen weiteren Vektor, den wir TurnVector nennen:

TurnVector: geVec3D;  // Drehwinkel

Dort sollen die Winkel stehen, um die der Betrachter sich dreht bzw. sich umschaut. Dabei haben die Vektorelemente diese Bedeutung:

TurnVector.x Drehung nach oben oder unten
TurnVector.y Drehung nach links oder rechts
TurnVector.z bleibt (normalerweise) immer null

Wie „fangen“ wir die Mausbewegungen ein? Nahe liegend wäre es, sich am Ereignis OnMouseMove zu orientieren. Weil der Mauszeiger sich dabei mitbewegt, klappt das nur so lange, bis der Mauszeiger irgendwo am Rand des Bildschirms angekommen ist. Dann müsste er wieder in die Mitte des Bildschirms (oder Fensters) geschubst werden. Das Verändern der Koordinaten des Mauszeigers führt jedoch innerhalb der MouseMove-Methode zu Konflikten. Deshalb müssen wir uns eine eigene Methode stricken.
Dort ermitteln wir zuerst die aktuelle Position des Mauszeigers:

GetCursorPos(MousePos);

MousePos ist vom Typ TPoint, eine Struktur mit den Koordinatenwerten TPoint.x und TPoint.y (also ein 2D-Vektor).
Bei unseren Bewegungen gehen wir davon aus, dass sich der Mauszeiger immer möglichst in der Mitte der Anzeigefläche (Screen) befindet. Deshalb berechnen wir, wie weit die aktuelle Position davon abweicht:

xMouse := (MousePos.x - Screen.Width  div 2);
yMouse := (MousePos.y - Screen.Height div 2);

Die Werte von xMouse und yMouse bestimmen dann, in welche Richtung wir uns drehen (müssen). Bei Änderungen der x-Koordinaten des Mauszeigers drehen wir uns um die y-Achse:

if xMouse < 0 then TurnVector.y := TurnVector.y + xDiff;
if xMouse > 0 then TurnVector.y := TurnVector.y - xDiff;

Ändern sich die y-Koordinaten des Mauszeigers, dann schauen wir nach oben oder unten (was einer Drehung um die x-Achse entspricht):

if yMouse < 0 then TurnVector.x := TurnVector.x + yDiff;
if yMouse > 0 then TurnVector.x := TurnVector.x - yDiff;

Allerdings gibt es hier eine wichtige Einschränkung: Damit wir beim Blick nach oben oder unten nicht nach hinten kippen oder uns überschlagen, beschränken wir hier den Blickwinkel auf den Wert +1 bzw. -1:

if yMouse < 0 then
  if TurnVector.x < 1 then TurnVector.x := TurnVector.x + yDiff;
if yMouse > 0 then
  if TurnVector.x > -1 then TurnVector.x := TurnVector.x - yDiff;

Damit der Mauszeiger nicht mit der Drehung mitwandert, sondern schön in der Mitte bleibt, müssen die Koordinaten ständig nachjustiert werden:

SetCursorPos(Screen.Width div 2, Screen.Height div 2);

Auch hier muss die Methode TForm1.CreateGame für das „Nullen“ von TurnVector (gegenüber G4DGAME1.PAS) um eine weitere Zeile ergänzt werden:

geVec3d_Set(@TurnVector, 0.0, 0.0, 0.0);

Und damit unsere Maussteuerung im Programm seine Wirkung zeigen kann, ist auch ein Aufruf der Methode zum Abfragen der Mausposition in der Wiederholungsschleife von RunGame nötig – am besten gleich hinter GetInput. Nennen wir die Methode GetMousePos, so sieht der betreffende Teil in TForm1.Rungame (gegenüber G4DGAME1.PAS) so aus:

// Neu in TForm1.RunGame
Application.ProcessMessages;
// Tasten und Maus abfragen
GetInput;
GetMousePos;
// Koordinaten und Winkel ausrichten
geXForm3D_SetIdentity (@XForm);

Außerdem müssen die aktuellen Werte von TurnVector irgendwo ankommen. Dazu bemühen wir ebenfalls in TForm1.RunGame die drei Rotate-Methoden:

geXForm3D_RotateX(@XForm, TurnVector.x);
geXForm3D_RotateY(@XForm, TurnVector.y);
geXForm3D_RotateZ(@XForm, TurnVector.z);

Dass hier auch RotateZ bedacht wird, obwohl dort der Wert immer null bleibt, macht durchaus Sinn – vielleicht möchtet Ihr ja später mal eine Rolle seitwärts in die Bewegungen einbauen.