Game Engine Math

KWEngine, Teil 27: Einen 3D-Character-Controller erstellen

In diesem Abschnitt geht es darum, einen robusten 3D-Character-Controller für unsere Klasse „Player“ zu erstellen, der auf unebene Kollisionsobjekte (der Art PlaneCollider und ConvexHull) reagieren kann.

Liste der in diesem Artikel verwendeten Engine-Begriffe

BegriffErläuterung
ColliderEin Collider ist ein 3D-Modell, das für die Kollisionsabfrage verwendet wird. Jedes Objekt hat sein eigenes Collider-Modell. Das Standardmodell ist eine konvexe Hülle, die die Maße des importierten 3D-Modells hat.

Es ist aber auch möglich, ein separates 3D-Modell als Collider zu importieren [ KWEngine.LoadCollider(...) ] und dann einer GameObject-Instanz zuzuweisen:
myInstanceName.SetColliderModel("NameDesColliderModells");

Wichtig:
Ein Objekt muss mit IsCollisionObject = true markiert sein, damit dessen Collider überhaupt beachtet wird.
Convex HullEine konvexe Hülle ist die Standardkollisionsform (also ein Collider), die ein Objekt in der KWEngine annehmen kann. Dabei wird beim Import eines 3D-Modells überprüft, welche Maße es hat (Breite, Höhe, Tiefe). Daraus wird dann ein Quader geformt, der diesen Maßen entspricht.
Aus mathematischer Sicht besagt eine konvexe Hülle, dass man von jedem Eckpunkt der Hülle zu jedem anderen Eckpunkt über eine gerade Linie gelangen kann, ohne dass diese Linie außerhalb der Hülle verläuft.

Es ist aber auch möglich, eine eigene konvexe Hülle zu modellieren (z.B. in der Software ‚Blender‚) und dann z.B. als OBJ-3D-Modelldatei zu exportieren und in der KWEngine zu importieren (siehe Code-Beispiel bei Begriff „Collider“).
Plane ColliderKollisionsmodelle können statt konvexer Hüllen auch Ebenen (engl.: plane = Ebene) sein. Ebenen bestehen i.d.R. aus einzelnen Quadraten, die z.B. in Blender modelliert und dann als OBJ-3D-Modelldatei exportiert werden. Diese Modelle können dann als Collider in der KWEngine als PlaneCollider importiert und verwendet werden.

Wichtig:
PlaneCollider-Modelle werden normalerweise nicht beim Aufruf der Instanzmethode GetIntersections() der Klasse GameObject berücksichtigt, weil hierfür eher die die Methode GameObject::RaytraceObjectsBelowPosition() verwendet werden sollte.

Verwendete Assets (Modelle und Texturen)

ModellVorschaubildDateien
Toonhttps://www.kwengine.de/downloads/Toon.glb
3D-Modell
zum Rendern
des Bodens
https://www.kwengine.de/downloads/PlaneRender.obj (Modell)
https://www.kwengine.de/downloads/PlaneRender.mtl (Modell-Zusatz)
https://www.kwengine.de/downloads/plane.png (Textur)
Kollisions-
modell für
den Boden
https://www.kwengine.de/downloads/PlaneCollider.obj

Aufbau der eigentlichen Welt

Dies ist ein Beispielbild der fertigen Testwelt, in der unsere Spielfigur mit Ebenenkollisionsmodellen (PlaneCollider) und mit konvexen Hüllen (ConvexHull) kollideren und entsprechend reagieren kann:

Dafür müssen wir zunächst die Welt (hier: World3DController) mit all ihren Objekten zusammenstellen und die benötigten 3D-Modelle importieren:

using KWEngine3;
using KWEngine3.GameObjects;
using OpenTK.Windowing.GraphicsLibraryFramework;

public class World3DController : World
{
    public override void Act()
    {

    }

    public override void Prepare()
    {
        // Setze Kameraposition und Schattenfarbe:
        SetCameraPosition(-2.0f, 7.5f, 12.5f);
        SetCameraTarget(-2.0f, 1.0f, 0.0f);
        SetColorAmbient(0.5f, 0.5f, 0.5f);

        // Lade die Modelle für die Spielfigur, 
        // für den Boden und zuletzt noch das 
        // Kollisionsmodell für den Boden (als separates Modell):
        KWEngine.LoadModel("Player", "./App/Models/Toon.glb");
        KWEngine.LoadModel("Plane", "./App/Models/PlaneRender.obj");
        KWEngine.LoadCollider("Plane", "./App/Models/PlaneCollider.obj", ColliderType.PlaneCollider);

        // Erstelle die Spielfigur und konfiguriere sie:
        Player p = new Player();
        p.Name = "Player";
        p.SetModel("Player");
        p.IsCollisionObject = true;
        p.IsShadowCaster = true;
        p.SetPosition(Player.PLAYER_START);
        p.SetScale(0.5f);
        p.SetHitboxScale(0.75f, 1.0f, 1.5f);
        AddGameObject(p);

        // Erstelle den Boden und setze sowohl das Render-
        // als auch das Collider-Modell:
        Immovable i = new Immovable();
        i.Name = "Plane";
        i.SetModel("Plane");
        i.SetColliderModel("Plane");
        i.IsCollisionObject = true;
        i.IsShadowCaster = true;
        AddGameObject(i);

        // Die Klasse Box dient nur dazu ein bewegliches
        // Objekt zu haben, mit dem die Spielfigur interagieren
        // kann. Sie wird zur Laufzeit zwischen zwei Höhenpositionen
        // hin- und herfahren:
        Box box01 = new Box();
        box01.Name = "Box01";
        box01.SetPosition(-0.5f, 3.5f, -2.0f);
        box01.SetScale(1.5f, 0.5f, 1.5f);
        box01.SetColor(1f, 0f, 1f);
        box01.IsCollisionObject = true;
        box01.IsShadowCaster = true;
        AddGameObject(box01);

        // Platziere noch ein (optionales) Sonnenlicht,
        // damit die Szene plastischer wirkt:
        LightObject sun = new LightObject(LightType.Sun, ShadowQuality.Low);
        sun.Name = "Sun";
        sun.SetPosition(-10f, 10f, 0f);
        sun.SetNearFar(1f, 20f);
        sun.SetFOV(16f);
        sun.SetColor(1f, 1f, 1f, 2.5f);
        AddLightObject(sun);
    }
}

Jetzt folgt der Bau der eigentlichen Controller-Klasse Player:

Jetzt müssen wir uns eigentlich nur noch um die Player-Klasse kümmern. Diese Klasse wird in ihrer Act()-Methode mehrere Methoden nacheinander aufrufen, um Bewegungen sowie Kollisionserkennung zu ermöglichen. Zusätzlich zu den Methoden werden wir in der Klasse ein paar Felder anlegen, die den aktuellen Zustand des Player-Objekts abbilden:

FeldDatentypArtErläuterung
VELOCITY_JUMPfloatKonstanteBeinhaltet den Wert, der auf die aktuelle vertikale Bewegungsgeschwindigkeit der Spielfigur addiert wird, wenn die Figur zum Sprung ansetzt.
VELOCITY_REDUCTIONfloatKonstanteStellt den Faktor dar, um den die horizontale Bewegungsgeschwindigkeit der Spielfigur pro Simulationsschritt reduziert wird. Dies hat zur Folge, dass die Spielfigur nicht umgehend stillsteht, wenn keine der Bewegungstasten mehr gedrückt wird.
VELOCITY_XZ_SPEEDfloatKontanteBeschreibt die eigentliche Bewegungsgeschwindigkeit, mit der die Spielfigur bewegt wird, wenn Eingabetasten (WASD) verwendet werden.
VELOCITY_XZ_SPEED_SLOPEfloatKontanteBeschreibt den Einfluss, den eine Schräge auf die Bewegungsgeschwindigkeit der Spielfigur hat.
VELOCITY_XZ_MINfloatKontanteBeschreibt die minimale horizontale Geschwindigkeit der Spielfigur. Unterschreitet die aktuelle Geschwindigkeit diesen Wert, wird sie auf 0 gerundet.
VELOCITY_XZ_MAXfloatKontanteBeschreibt die maximale horizontale Geschwindigkeit der Spielfigur. Überschreitet die aktuelle Geschwindigkeit diesen Wert, wird sie auf die Maximalgeschwindigkeit reduziert.
GRAVITYfloatKontanteBeschreibt den Wert, um den die vertikale Geschwindigkeit der Spielfigur pro Simulationsschritt verringert wird, um die Erdanziehungskraft zu simulieren. Wird nur angewendet, wenn sich die Spielfigur in der Luft befindet.
PLAYER_STARTVector3KontanteEnthält die Startposition der Spielfigur [für diese Beispielwelt hat die Startposition die Koordinaten (0|0|0)].
_velocityXZVector2VariabelBeschreibt die aktuelle Geschwindigkeit der Spielfigur auf horizontaler XZ-Ebene. Die erste Koordinate des Vector2-Felds beschreibt die Geschwindigkeit in X-Richtung, die zweite Koordinate die Geschwindigkeit in Z-Richtung.
_velocityYfloatVariabelBeschreibt die aktuelle Geschwindigkeit der Spielfigur auf vertikaler Ebene (Sprung-/Fallgeschwindigkeit).
_currentStateStateVariabelLegt fest, ob eine Spielfigur gerade am Boden oder in der Luft ist. State ist dabei ein enum, das nur die beiden Status OnGround und InAir beinhaltet.
_animationStepfloatVariabelBeschreibt, welchen Prozentteil der aktuell festgelegten 3D-Modellanimation die Spielfigur abbildet.
_animationNeedsToChangeboolVariabelBeschreibt, ob die aktuelle Animation der Spielfigur gewechselt werden musste (z.B. von einer Lauf- zu einer Sprunganimation).

Die Liste der für die Player-Klasse festgelegten Instanzmethoden wird in der folgenden Tabelle dargestellt. Die Reihenfolge der Methoden in der Tabelle entspricht exakt der Reihenfolge, in der diese Methoden in der Act()-Methode der Player-Klasse aufgerufen werden:

Rückgabe-
datentyp
MethodeErläuterung
Vector3HandleGroundDetection()Diese Methode prüft, ob sich unterhalb der Spielerposition ein anderes Objekt befindet.
Befindet sich die Spielfigur im Status „OnGround„, wird die Spielfigur stehts auf die Höhe des entdeckten Objekts gesetzt. Wurde kein Objekt unterhalb des Spielers entdeckt, wechselt der Status der Spielfigur in den Status „InAir„.
Befindet sich die Spielfigur im Status „InAir“ wird geprüft, ob sich ein anderes Objekt unterhalb der Spielfigur befindet, und ob der Abstand dieses Objekts zur Spielfigur gering genug ist, damit sie es mit den Füßen berührt. Ist dies der Fall, wechselt der Status wieder zu „OnGround„.

Bei der Suche nach Objekten unterhalb des Spielers wird ebenfalls ermittelt, in welche Richtung die Oberfläche des gefundenen Objekts zeigt. Die Richtung der Oberfläche wird als Vector3-Objekt angegeben und von der Methode letztendlich zurückgegeben.
Wurde kein Objekt unterhalb des Spielers gefunden, wird ein Vector3-Objekt zurückgegeben, das die Koordinaten (0|1|0) hat.
Vector2HandleMovement()Diese Methode überprüft die Tastatur- und Mauseingaben und ermittelt, wieviel Geschwindigkeit für den aktuellen Simulationsschritt zur aktuellen Geschwindigkeit noch hinzukommt.

Diese durch die Eingabe neu entstehende Geschwindigkeit wird als normalisiertes Vector2-Objekt zurückgegeben (wobei die X-Komponente die Links-/Rechtsgeschwindigkeit und die Y-Komponente des Vektors die Vorne-/Hintengeschwindigkeit beinhaltet).

Zusätzlich prüft die Methode, ob die Taste für den Sprungbefehl gedrückt wurde. Ist dies der Fall, wechselt die Spielfigur in den Status „InAir“ und bekommt eine positive vertikale Geschwindigkeit (_velocityY), damit der Sprung gestartet wird.
voidApplyVelocity(
Vector2 inputVelocity,
Vector3 currentSlopeNormal)
Diese Methode verrechnet die durch die Eingabetasten entstandene Geschwindigkeit mit dem Winkel der aktuellen Oberfläche um die tatsächlich entstehende Geschwindigkeit zu berechnen. Diese wird dann zur bisherigen Geschwindigkeit addiert. Dabei wird aber auch darauf geachtet, dass die Maximalgeschwindigkeit der Spielfigur nicht überschritten wird.
voidHandleIntersectionWithObjects()Diese Methode prüft, ob die Spielfigur mit anderen Hindernissen (alles außer dem Boden) kollidiert und setzt die Spielfigur dementsprechend zurück, so dass die Kollision ungeschehen gemacht wird.
Sollte die Spielfigur während eines Sprungs nach oben gegen mit einer Decke kollidieren, wird die Aufwärtsgeschwindigkeit der Spielfigur auf zurück auf 0 gesetzt, um den Sprung zu bremsen.
voidHandleAnimations()Diese Methode spielt je nach aktuellem Zustand die entsprechende Modellanimation ab.
void HandlePlayerFalloff()Diese Methode prüft, ob die Spielfigur in ihrer Höhe einen bestimmten Grenzwert unterschreitet. Ist dies der Fall, wird die Figur via Reset() zurückgesetzt.
voidReset()Setzt die Spielfigur auf den Startpunkt zurück und löscht alle bestehenden Geschwindigkeiten.

Code der einzelnen Methoden

Act():

public override void Act()
{
    // Prüfe, ob Kontakt zum Boden besteht und setze
    // _currentState dementsprechend auf den korrekten
    // Wert:
    Vector3 currentSlopeNormal = HandleGroundDetection();

    // Prüfe, welche Steuerungstasten gedrückt wurden und
    // bestimme so die durch die Eingabetasten entstehende
    // Bewegung:
    Vector2 inputVelocity = HandleMovement();

    // Wende die durch die Eingabe entstandene Geschwindigkeit
    // auf die aktuell für die Spielfigur gültige 
    // Geschwindigkeit an:
    ApplyVelocity(inputVelocity, currentSlopeNormal);

    // Prüfe, ob interaktive Objekte (Gegenstände, bewegliche
    // Plattformen, usw.) berührt werden:
    HandleIntersectionsWithObjects();

    // Passe die Animationen der Spielfigur dem aktuellen
    // Status (_currentState) an:
    HandleAnimations();

    // Falls die Spielfigur vom Rand der Welt fällt, 
    // setze sie wieder zurück auf den Ausgangspunkt:
    HandlePlayerFallOff();
}


HandleGroundDetection():

private Vector3 HandleGroundDetection()
{
    // Gehe zunächst von einer ebenen Fläche
    // (deren Ebenenvektor gerade nach oben zeigt) aus:
    Vector3 slopeNormal = Vector3.UnitY;

    // Ist die Spielfigur gerade im Zustand "am Boden"...
    if (_currentState == State.OnGround)
    {
        // ...prüfe, ob sich Objekte des Typs Immovable oder Box weniger als 0.1f Einheiten unterhalb 
        // der Spielfigur befinden:
        RayIntersectionExtSet set = RaytraceObjectsBelowPosition(
            RayMode.FourRaysY, 
            0.75f, 
            -1.0f, 
            0.1f, 
            typeof(Immovable), typeof(Box)
            );

        // Enthält die Ergebnismenge (ein Set = Menge an Ergebnissen) valide Daten,
        // wurde mindestens ein Objekt gefunden, das so nah unterhalb des Players liegt:
        if (set.IsValid)
        {
            // Überschreibe die Steigung der aktuellen Ebene mit der gemessenen Steigung:
            slopeNormal = set.SurfaceNormalAvg;

            // Setze die Spielfigur direkt auf den Punkt, der von ihrer Mitte aus unterhalb 
            // gemessen wurde:
            this.SetPositionY(set.IntersectionPointCenter.Y, PositionMode.BottomOfAABBHitbox);
        }
        else
        {
            // Wenn die Ergebnismenge kein valides Ergebnis hat, dann wechsele den
            // aktuellen Zustand auf "in der Luft", weil die Spielfigur dann nämlich
            // in der Luft ist:
            _currentState = State.InAir;
            _animationNeedsToChange = true;
        }
    }
    else
    {
        // Ist die Spielfigur in der Luft, wird von ihrer aktuellen 
        // vertikalen Geschwindigkeit immer wieder ein Stück der aktuellen
        // Gravitation abgezogen, so dass sich die Fallgeschwindigkeit pro
        // Frame erhöht. In diesem Beispiel wird die Fallhöhe dann aber auf
        // -0.5f begrenzt, damit die Spielfigur nicht zu schnell fällt:
        _velocityY = _velocityY - GRAVITY;
        if (_velocityY < -0.5f)
            _velocityY = -0.5f;

        // Auch in der Luft muss geprüft werden, ob sich ein Bodenobjekt nahe den Füßen
        // der Spielfigur befindet. Hier muss ein Objekt sich aber näher als 0.1f Einheiten
        // unterhalb der Figur befinden, damit die Figur dies als gültigen Boden erkennt:
        RayIntersectionExtSet set = RaytraceObjectsBelowPosition(
            RayMode.FourRaysY, 
            0.75f, 
            -0.5f, 
            0.1f, 
            typeof(Immovable), typeof(Box)
            );

        // Wurde ein Boden gefunden, ist die Ergebnismenge "valide":
        if (set.IsValid == true)
        {
            // Ist dann die vertikale Geschwindigkeit auch noch kleiner/gleich 0
            // kann die Spielfigur "landen". Würden wir hier nicht auf <= 0 prüfen
            // würde die Spielfigur sofort ihre Aufwärtsbewegung stoppen, wenn sie 
            // eine bodenähnliche Oberfläche streift:
            if (_velocityY <= 0)
            {
                _currentState = State.OnGround;
                this.SetPositionY(set.IntersectionPointCenter.Y, PositionMode.BottomOfAABBHitbox);
                _animationNeedsToChange = true;
                _velocityY = 0f;
            }
        }
    }
    return slopeNormal;
}


HandleMovement():

private Vector2 HandleMovement()
{
    // Hier wird gemessen, wieviel zusätzliche Geschwindigkeit
    // die Spielfigur durch die aktuell gedrückten Tasten erhält:
    Vector2 inputVelocity = Vector2.Zero;
    if (Keyboard.IsKeyDown(Keys.A))
    {
        inputVelocity -= CurrentWorld.CameraLookAtVectorLocalRightXZ.Xz;
    }
    if (Keyboard.IsKeyDown(Keys.D))
    {
        inputVelocity += CurrentWorld.CameraLookAtVectorLocalRightXZ.Xz;
    }
    if (Keyboard.IsKeyDown(Keys.W))
    {
        inputVelocity += CurrentWorld.CameraLookAtVectorXZ.Xz;
    }
    if (Keyboard.IsKeyDown(Keys.S))
    {
        inputVelocity -= CurrentWorld.CameraLookAtVectorXZ.Xz;
    }

    // Hier wird ermittelt, ob die Eingabegeschwindigkeit überhaupt > 0 ist:
    float inputVelocityLengthSq = inputVelocity.LengthSquared;
    if (inputVelocityLengthSq > 0)
    {
        // Wenn sie >0 ist, dann wird die inputVelocity normalisiert (das bedeutet,
        // dass die Länge dieses Vektors auf 1 gekürzt/verstärkt wird):
        inputVelocity.NormalizeFast();
    }

    // Ist die Leertaste oder die rechte Maustaste gedrückt während sich der Spieler 
    // am Boden befindet, so wird der Status in "in air" gewechselt:
    if ((Keyboard.IsKeyPressed(Keys.Space) || Mouse.IsButtonPressed(MouseButton.Right)) && _currentState == State.OnGround)
    {
        _currentState = State.InAir;

        // Erhöhe die Aufwärtsgeschwindigkeit um die für die Spielfigur
        // festgelegte Menge:
        _velocityY += VELOCITY_JUMP;
        _animationNeedsToChange = true;
    }

    return inputVelocity;
}


ApplyVelocity(Vector2, Vector3):

private void ApplyVelocity(Vector2 inputVelocity, Vector3 currentSlopeNormal)
{
    // Um zu bestimmen, was die letztendliche Bewegungsgeschwindigkeit für
    // den Frame ist, müssen die Eingabegeschwindigkeit (inputVelocity) und die
    // Lage der aktuellen Oberfläche (currentSlopeNormal) betrachtet werden.
    // Zeigt die Eingaberichtung z.B. an, dass die Spielfigur nach rechts laufen will
    // aber die aktuelle Oberfläche ist nach links geneigt, soll es für die Spielfigur
    // schwerer sein, sich auf dieser Oberfläche zu bewegen.

    // Als erstes berechnen wir, in welchem Verhältnis die Bewegungsrichtung des Spielers
    // und die Neigung der aktuellen Ebene (auf dem er steht) zueinander stehen.
    // Das Skalarprodukt (dot product) ist größer 0, wenn die Richtungen grob übereinstimmen.
    // Die Neigungsrichtung (und -stärke) ist im Vector3-Objekt 'currentSlopeNormal' gespeichert.
    // Aus diesem Grund muss die Bewegungsrichtung (die eigentlich in einem Vector2-Objekt liegt)
    // in ein Vector3-Objekt umgerechnet werden.
    Vector3 velocityXYZ = Vector3.NormalizeFast(new Vector3(_velocityXZ.X, 0, _velocityXZ.Y));

    // Wenn man jetzt beide Vektoren hat und sicher ist, dass sie normalisiert sind (der Ebenenvektor
    // ist immer normalisiert), wird das Skalarprodukt (dot product) der beiden Vektoren gebildet.
    // Das Skalarprodukt kann Werte zwischen -1 und +1 annehmen. Der Wert ist positiv, wenn die 
    // die Vektoren (grob) in die gleiche Richtung zeigen. Der Wert ist negativ, wenn die 
    // Vektoren (grob) in die entgegensetzte Richtung zeigen.
    // Da die Spielfigur in diesem Beispiel nicht schneller werden soll, wenn ihre Richtung mit
    // der Ebenenrichtung übereinstimmt (z.B. wenn sie bergab läuft), begrenzen wir mithilfe
    // von Math.Min() den Wert auf maximal 0. Dann hat die Schräge nur dann einen Effekt
    // wenn sie der Bewegungsrichtung entgegensetzt steht:
    float dotVelocitySlope = Math.Min(0, Vector3.Dot(velocityXYZ, currentSlopeNormal));

    // Auf die aktuelle Geschwindigkeit wird dann die Eingabegeschwindigkeit (verrechnet
    // mit der Geschwindigkeitsminderung) addiert und ergibt die für den aktuellen Frame
    // gültige Endgeschwindigkeit:
    _velocityXZ += inputVelocity * VELOCITY_XZ_SPEED + inputVelocity * dotVelocitySlope * VELOCITY_XZ_SPEED_SLOPE;

    // Wenn die Eingabegeschwindigkeit größer 0 ist (der Spieler also irgendeine der 
    // Bewegungstasten gedrückt hat), dann wird die Spielfigur auch in die entsprechende
    // Richtung gedreht:
    if (inputVelocity.LengthSquared > 0)
    {
        TurnTowardsXZ(this.Position + new Vector3(_velocityXZ.X, 0, _velocityXZ.Y));
    }

    // Sollte die berechnete Bewegungsgeschwindigkeit höher als die in _playerSpeed
    // definierte Maximalgeschwindigkeit sein, begrenze den Geschwindigkeitsvektor
    // (_velocityXZ) auf die Maximalgeschwindigkeit:
    float velocitySum = _velocityXZ.LengthFast;
    if (velocitySum > VELOCITY_XZ_MAX)
    {
        _velocityXZ = Vector2.NormalizeFast(_velocityXZ) * VELOCITY_XZ_MAX;
    }

    // Wende die berechnete Geschwindigkeit des aktuellen Frames auf die Spielfigur an:
    MoveOffset(new Vector3(_velocityXZ.X, _velocityY, _velocityXZ.Y));

    // Reduziere die Bewegungsgeschwindigkeit für den nächsten Frame um einen gewissen
    // Anteil, damit die Spielfigur langsam zum Stillstand kommt, wenn keine Bewegungs-
    // tasten mehr gedrückt werden:
    _velocityXZ /= VELOCITY_REDUCTION;
    if (_velocityXZ.LengthSquared < VELOCITY_XZ_MIN)
        _velocityXZ = Vector2.Zero;
}


HandleIntersectionsWithObjects():

private void HandleIntersectionsWithObjects()
{
    // Die Kollisionsprüfung wird ausschließlich für Kollisionsobjekte des Typs Box und 
    // dem Hitboxmodus "Convex Hull" getätigt.
    List<Intersection> intersections = GetIntersections<Box>(IntersectionTestMode.CheckConvexHullsOnly);

    // Dann wird durch die Liste aller entdeckten Kollisionen iteriert:
    foreach (Intersection i in intersections)
    {
        // Die Spielfigur bewegt sich gemäß der gemessenen Kollision, um die Kollision
        // ungeschehen zu machen:
        MoveOffset(i.MTV);

        // Sonderfall! Die Spielfigur kollidiert mit etwas, während sie in der Luft ist:
        if (_currentState == State.InAir)
        {
            // Wenn der Korrekturvektor (MTV) und die Welt-Y-Achse 
            // ein stark negatives Skalarprodukt (dot product) haben, dann zeigt
            // der Korrekturvektor nach unten. Das bedeutet, dass die Spielfigur
            // ein Objekt von unten berührt. In diesem Fall muss die Aufwärts-
            // geschwindigkeit (_velocityY) zurückgesetzt werden:
            if (_velocityY > 0f && Vector3.Dot(Vector3.NormalizeFast(i.MTV), KWEngine.WorldUp) < -0.5f)
            {
                _velocityY = 0f;
            }

            // Sind die XZ-Komponenten des Korrekturvektors != 0, so ist die Spielfigur
            // scheinbar seitlich gegen eine Wand geprallt. In diesem Fall muss die Bewegungs-
            // geschwindigkeit der Spielfigur auf der XZ-Achse auf (0|0) zurückgesetzt werden.
            if (i.MTV.Xz.LengthSquared > 0)
            {
                _velocityXZ = Vector2.Zero;
            }
        }
    }
}


HandleAnimations():

private void HandleAnimations()
{
    // Setze die Animationen gemäß des aktuellen Zustands, wenn das 3D-Modell
    // der Spielfigur über Animationen verfügt:
    if (HasAnimations)
    {
        if (_animationNeedsToChange)
        {
            _animationStep = 0f;
            SetAnimationPercentage(_animationStep);
        }
        if (_currentState == State.OnGround)
        {
            if (_velocityXZ.LengthFast > 0.001f)
            {
                SetAnimationID(11); // Walk
                SetAnimationPercentageAdvance(0.005f);
            }
            else
            {
                SetAnimationID(3); // Idle
                SetAnimationPercentageAdvance(0.001f);
            }
        }
        else
        {
            if (_animationNeedsToChange)
            {
                SetAnimationID(6);
                _animationStep = 0.10f;
                _animationNeedsToChange = false;
            }

            if (AnimationID == 6)
            {
                SetAnimationPercentage(_animationStep);
                if (_animationStep >= 1)
                {
                    SetAnimationID(7);
                    _animationStep = 0f;
                    SetAnimationPercentage(_animationStep);
                }
                else
                {
                    _animationStep += 0.006f;
                }
            }
            else if (AnimationID == 7)
            {
                SetAnimationPercentageAdvance(0.01f);
            }
        }
        _animationNeedsToChange = false;
    }
}


HandlePlayerFallOff():

private void HandlePlayerFallOff()
{
    // Sollte die Spielfigur vom Rand der Szene fallen,
    // wird ihre Position zurückgesetzt, wenn sie eine negative
    // Höhe von -20 unterschreitet:
    if (Position.Y < -20)
    {
        Reset();
    }
}


Reset():

private void Reset()
{
    SetPosition(Player.PLAYER_START);
    _velocityY = 0f;
    _velocityXZ = Vector2.Zero;
    _currentState = State.OnGround;
    _animationNeedsToChange = true;
}

Beitrag veröffentlicht

in

von

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.