Home » Tutorials » Grafik und Spiele » Figuren in 3D-Welten einsetzen

Figuren in 3D-Welten einsetzen

Freies Bewegen

Bis jetzt können wir Akteure in die Welt einbinden und dort herumstehen lassen. Das würde z.B. gut für Gegenstände passen, die man im Spiel mitnehmen kann. Bei Figuren aber möchte man, dass diese sich bewegen. Im Idealfall tun sie dies sozusagen aus freien Stücken, also ohne dass sie etwa über die Tasten gesteuert werden müssen. Damit unsere Figur also nicht nur eine Statistenrolle einnimmt, müssen wir ihr das Laufen beibringen:

procedure TFigur.Walk;
const xDiff=0.1;
var Richtung: Integer; Winkel: geFloat;
begin
  Richtung := Random(15);
  case Richtung of
    // kurze Drehung
    isLeft : TurnVector.y := TurnVector.y + xDiff;
    isRight: TurnVector.y := TurnVector.y - xDiff;
    // sonst vorwärts/geradeaus
    else
    begin
      LookVector.x := ViewVector.x + MSpeed * sin(TurnVector.y);
      LookVector.z := ViewVector.z + MSpeed * cos(TurnVector.y);
    end;
  end;
  // Neue View-Werte nur, wenn keine Kollision
  if not Collision then
    ViewVector := LookVector
  // sonst Drehung, Schritt zurück
  else
  begin
    Winkel := (Random(7)-3)*Pi/3;     // 60 bis 180 Grad
    TurnVector.y := TurnVector.y + Winkel;
    LookVector.x := ViewVector.x - MSpeed * sin(TurnVector.y);
    LookVector.z := ViewVector.z - MSpeed * cos(TurnVector.y);
  end;
end;

Weil mit MSpeed die Laufgeschwindigkeit bereit gegeben ist, benötigen wir mit xDiff nur eine Konstante. Ansonsten erinnert eine Menge an die GetInput-Methode aus der Hauptunit. Anstelle einer Tastenabfrage wird hier eine zufällige Richtung erzeugt.

Richtung:= random(15);

Die Konstanten für Links und Rechts werden als einfache ganze Zahlen vereinbart:

const isRight = 1; isLeft = 2;

Die erreichbare Zufallszahl ist deshalb hoch, damit die Figur nur ab und zu einen Schwenk nach links oder rechts macht:

case Richtung of
  isLeft : TurnVector.y := TurnVector.y + xDiff;
  isRight: TurnVector.y := TurnVector.y - xDiff;

Ansonsten und meistens geht es vorwärts und geradeaus:

LookVector.x := ViewVector.x + MSpeed * sin(TurnVector.y);
LookVector.z := ViewVector.z + MSpeed * cos(TurnVector.y);

Auch bei der Figur gibt es nur dann neue Werte für ViewVector, wenn es zu keiner Kollision kommt:

if not Collision then ViewVector := LookVector;

Was aber, wenn doch? Um z.B. von einer Wand wegzukommen, hilft am besten eine kräftigere Drehung (um 60 oder mehr Grad) und vielleicht noch ein Schritt zurück:

Winkel := (random(7)-3)*Pi/3;
TurnVector.y := TurnVector.y + Winkel;
LookVector.x := ViewVector.x - MSpeed * sin(TurnVector.y);
LookVector.z := ViewVector.z - MSpeed * cos(TurnVector.y);

Sollte Euch später diese „Kollisionsverarbeitung“ nicht zusagen, heißt es andere Winkel ausprobieren oder auch mal den Schritt zurück weglassen.
In der Hauptunit haben wir uns mit einer recht einfachen Kollisionsfunktion begnügt. Treppen hinaufsteigen oder heruntergehen wurde dabei nicht berücksichtigt. Damit jedoch unsere Figur nicht vor ein paar Stufen schlappmacht, erweitern wir nun die Collision-Methode entsprechend ? erst einmal für TFigur:

function TFigur.Collision: Boolean;
var TempVector: geVec3D; KontaktInfo: GE_Collision;
begin
  Result := Boolean(geWorld_Collision (World, @MinVector, @MaxVector,
    @LookVector, @ViewVector, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL,
    0, nil, nil, @KontaktInfo));
  if not Result then exit;
  // Kontrolle ob Treppe oder "Schräge"
  TempVector   := ViewVector;
  TempVector.y := TempVector.y + StepJump;
  LookVector.y := LookVector.y + StepJump;
  // Test aus höherer Sichtposition
  Result := Boolean(geWorld_Collision (World, @MinVector, @MaxVector,
    @LookVector, @TempVector, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL,
    0, nil, nil, @KontaktInfo));
  // ggf. zurück auf alte "Sichthöhe"
  if Result then LookVector.y := LookVector.y - StepJump;
end;

Zuerst erfolgt die normale Kollisionskontrolle (die sich für Wände bereits bewährt hat). Als nächstes heben wir die Figurposition ein bisschen an, um von hier aus zu überprüfen, ob wir auf ein Hindernis stoßen (würden). Ein temporärer Hilfsvektor dient zur Zwischenspeicherung des „Versuchswertes“:

TempVector   := ViewVector;
TempVector.y := TempVector.y + StepJump;
LookVector.y := LookVector.y + StepJump;
Result := Boolean(geWorld_Collision (World, @MinVector, @MaxVector,
  @LookVector, @TempVector, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL,
  0, nil, nil, @KontaktInfo));

Sollte das zu einer Kollision führen, kehren wir zurück zur alten Sichthöhe:

if Result then LookVector.y := LookVector.y - StepJump;

StepJump wir als Konstante vereinbart, z.B. mit diesem Wert:

const StepJump = 50;

Mit der erweiterten Kollisionsmethode ist es unserer Figur möglich, die Treppe zu erklimmen ? aber nun kommt sie nicht mehr herunter. Ein weitere Fall für die Kollisionskontrolle also: Eine neue Methode soll sich um die Gravitation kümmern, also dafür sorgen, dass die Figur ständig auf dem Boden bleibt. Ein passender Name ist mit CheckGravity auch gleich gefunden:

procedure TFigur.CheckGravity;
var TempVector: geVec3D; KontaktInfo: GE_Collision; isFalling: Boolean;
begin
  // Kontrolle, ob noch "festen Boden unter Füßen"
  TempVector := ViewVector;
  TempVector.y := TempVector.y - StepJump;
  isFalling := Boolean(geWorld_Collision (World, @MinVector, @MaxVector,
    @ViewVector, @TempVector, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL,
    0, nil, nil, @KontaktInfo));
  // ggf. zurück auf Bodenhöhe
  if isFalling then TempVector.y := KontaktInfo.Impact.y;
  LookVector := TempVector;
  ViewVector := LookVector;
end;

Auch hier arbeiten wir wieder mit einem temporären Hilfsvektor, dem wir gleich die Werte von ViewVector zuweisen. Anschließend wird ein Konstantenwert subtrahiert:

TempVector := ViewVector;
TempVector.y := TempVector.y - StepJump;

Nun probieren wir aus, ob wir die Figur ein Stückchen nach unten sacken lassen könnten:

isFalling := Boolean(geWorld_Collision (World, @MinVector, @MaxVector,
  @ViewVector, @TempVector, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL,
  0, nil, nil, @KontaktInfo));

Sollte das nicht klappen, befindet sich die Figur offensichtlich auf festem Grund. Andernfalls muss sie sich auf den Boden fallen lassen:

if isFalling then TempVector.y := KontaktInfo.Impact.y;

Der Vektor Impact in der KontaktInfo-Struktur enthält die aktuellen noch erlaubten „Berührungspunkte“ für alle Achsen. Weil das Fallen entlang der y-Achse verläuft, wird hier also Impact.y benötigt, womit TempVector.y auf Bodenhöhe gesetzt wird. (Dieses Kontrollprinzip funktioniert nicht nur für Treppen, wenn man sie hinuntergehen will: Dort lässt man sich sozusagen von Stufe zu Stufe fallen. Auch ein Hinunterplumpsen in ein Loch oder einen Abgrund wäre jetzt möglich. Und sollte es auch noch so tief hinuntergehen, man kommt immer heil unten an. Allerdings nur, wenn irgendwo ein Stückchen fester Boden ist. Andernfalls wird der Fall endlos.)
Als (vorläufig) letzte Methode bekommt die Unit G4DFigur noch eine Prozedur zur Demontage des Akteurs:

destructor TFigur.Destroy;
begin
  if ActorDef <> nil then geActor_DefDestroy(@ActorDef);
  if Actor <> nil then geActor_Destroy(@Actor);
  inherited Destroy;
end;

Damit die Freigabe reibungslos klappt, werden hier über zwei Genesis-Methode die Definitionsdaten und dann der Actor selbst entfernt.