Game Engine Math

KWEngine, Teil 26: Einen 2D-Character-Controller erstellen

In einem klassischen 2D-Platformer-Game muss die Spielfigur auf verschiedene Hindernisse und Zustände reagieren können. Eine Klasse, in der der dafür benötigte Code gesammelt wird, nennt man einen „Character Controller“. Eine Spielerfigur kann sich nämlich meistens nicht nach den typischen Physikgesetzen einer Spielwelt richten, weil sie in der Regel über zusätzliche Funktionen (Fliegen, Doppelsprungfunktion, usw.) verfügt und deshalb gesondert gesteuert werden muss.

Die üblichen Problemfälle bei der Erstellung eines 2D-Character-Controllers sind in der folgenden Grafik abgebildet:

Problemfall #1: Steh‘ ich noch oder fall‘ ich schon?

Das erste Problem bei der Programmierung entsteht, wenn man prüfen muss, ob eine Spielfigur gerade im „stehenden“ Zustand oder im „springenden/fallenden“ Zustand ist. Nehmen wir an, die Spielfigur steht auf festem Boden und ihr Zustand ist demnach „steht“. Jetzt bewegt sich die Spielfigur nach rechts und erreicht das Ende der aktuellen Plattform. Jetzt muss festgestellt werden, ob Teile der Spielfigur noch auf der Plattform sind oder ob die Spielfigur komplett „in der Luft“ steht.
Sollte die Spielfigur bzw. deren Hitbox so wie oben abgebildet teilweise bereits über die Platform hinausragen, so bleibt ihr Zustand dennoch im Status „steht“. Um das zu testen, schießt man zwei Teststrahlen (lila) nach unten und prüft, ob diese auf eine Plattform treffen. Sollte auch nur einer der beiden Strahlen auf eine Plattform treffen, und sollte dann auch noch der Abstand zur Plattform gering genug sein (z.B. kleiner als 0,01 Längeneinheiten), dann können wir davon ausgehen, dass der Zustand weiterhin im Status „steht“ verbleiben darf.
Sollten andernfalls beide Teststrahlen keinen Treffer erzeugen oder die Position der getroffenen Plattform weit unter der Höhe der Spielfigur sein, muss sich der Zustand des Spielers auf „springt/fällt“ ändern.

Problemfall #2: Kollision mit Hindernissen

Das zweite Szenario trifft zu, wenn die Spielfigur auf ein Hindernis (oben in rot dargestellt) trifft. Die Spielfigur wird zunächst ganz normal mit den Richtungstasten bewegt, und erst dann wird geprüft, ob die Spielfigur mit einem Hindernis kollidiert.
Wenn das der Fall ist, dann wird berechnet, wie stark sich die Hitbox der Spielfigur mit der Hitbox des Hindernisses schneidet und die Spielfigur entsprechend zurückgesetzt, damit sie fortan nicht mehr mit dem Hindernis kollidiert.

Problemfall #3: Flieg‘ ich noch oder steh‘ ich schon?

Sollte sich die Spielfigur nach einem Sprung wieder dem Boden annähern, müssen wir – während sich die Spielfigur im Status „springt/fällt“ befindet – permanent messen, wie weit der Boden unter unseren Füßen von uns entfernt ist. Ist die Entfernung klein genug, müssen wir den Status auf „steht“ umschalten.

Code
using System;
using KWEngine3;
using KWEngine3.GameObjects;
using KWEngine3.Helper;
using OpenTK.Windowing.GraphicsLibraryFramework;
using OpenTK.Mathematics;

namespace Tutorial
{
    public class Player : GameObject
    {
        private const float GRAVITY = 0.001f;     // Schwerkraft, die immer gleich groß ist
        private const float JUMPVELOCITY = 0.06f; // Anfängliche Sprungkraft zu Beginn jedes Sprungs
        private string _state = "stand";          // Aktueller Status (beginnend bei "stehend")
        private float _velocityY = 0f;            // Aktuelle Sprungkraft (positiv bei Start des Sprungs
                                                  // und negativ, wenn die Spielfigur fällt)

        public override void Act()
        {
            // Abfrage der Links-/Rechtsbewegung:
            if (Keyboard.IsKeyDown(Keys.A))
            {
                MoveOffset(-0.01f, 0f, 0f);
            }
            if (Keyboard.IsKeyDown(Keys.D))
            {
                MoveOffset(+0.01f, 0f, 0f);
            }

            // Ggf. den Status in "springt/fällt" ändern, wenn die Sprungtaste gedrückt wird:
            if(_state == "stand" && Keyboard.IsKeyPressed(Keys.Space))
            {
                _state = "in air";
                _velocityY = JUMPVELOCITY;
            }

            // Wenn sich die Spielfigur in der Luft befindet, dann ziehe in jedem Frame
            // den Gravitationswert von der Geschwindigkeit ab. Dies bewirkt, dass der Zuwachs
            // an Höhe auf der Y-Achse jedes Mal ein wenig abnimmt. Solange _velocityY positiv ist,
            // ist die Spielfigur in der Phase "springt hoch". Sobald _velocityY einen Wert <= 0
            // annimmt, "fällt" die Spielfigur.
            // Der Wert in _velocityY auf ein Minimum beschränkt werden, so dass
            // die Fallgeschwindigkeit nicht zu hoch wird.
            if(_state == "in air")
            {
                _velocityY = Math.Max(_velocityY - GRAVITY, -0.5f); // Fallgeschwindigkeit auf -0.5f begrenzt
                MoveOffset(0f, _velocityY, 0f);
            }

            // Problemfälle #1 und #3:
            // -----------------------
            // Prüfen, ob die Spielfigur gerade steht und dann entscheiden, ob der Status
            // auf "springend/fallend" geändert werden muss:
            RayIntersectionExtSet result = RaytraceObjectsBelowPosition(
                RayMode.TwoRays2DPlatformerY,  // Schieße zwei Strahlen auf der linken und rechten Seite nach unten

                1f,                            // Skalierung der Strahlenpositionen in Relation zur Hitbox 
                                               // (in der Regel auf 1f setzen)

                -0.05f,                        // Strahlenmessungen ignorieren, bei denen die Spielerhitbox vertikal
                                               // bereits mehr als 0,05 Einheiten in dem Objekt steckt

                0.025f,                        // Strahlenmessungen ignorieren, wenn das getroffene Objekt noch weiter 
                                               // als 0,025 Einheiten von den Füßen der Spielfigur entfernt ist

                typeof(GameObject)             // Welche Art(en) von Objekten soll getestet werden?
                                               // z.B. typeof(Floor), typeof(Ground), usw. (mehrere Typen kommasepariert möglich)

                                               // HINWEIS: typeof(GameObject) sollte prinzipiell NICHT verwendet werden, da dann
                                               // wirklich ALLE Objektarten geprüft werden (also z.B. auch Wolken am Himmel),
                                               // was sich negativ auf die CPU-Performance auswirkt
                );
             
            // Falls überhaupt einer der Strahlen ein Objekt getroffen hat, gilt die Messung als "gültig".
            // (Ist die Messung hingegen NICHT gültig, dann können wir davon ausgehen, dass die Spielfigur 
            // in der Luft schwebt)
            if(result.IsValid)
            {
                // Wenn Objekte von den Strahlen getroffen wurden, muss jetzt je nach aktuellem Status
                // unterschiedlich reagiert werden:
                // Wenn sich die Spielfigur gerade im Zustand "steht" befindet, ...
                if(_state == "stand")
                {
                    // ...muss einfach nur die Position der Spielfigur in ihrer Höhe (Y-Achse) auf die 
                    // Stelle gesetzt werden, an der der Teststrahl ein Objekt getroffen hat.
                    // Damit "klebt" die Spielfigur am Boden:
                    SetPositionY(result.IntersectionPointNearest.Y, PositionMode.BottomOfAABBHitbox);
                }
                else
                {
                    // Andernfalls muss zusätzlich zur Positionsänderung noch der Status zurück auf 
                    // "steht" geändert und die aktuelle Sprung-/Fallgeschwindigkeit gelöscht werden:
                    SetPositionY(result.IntersectionPointNearest.Y, PositionMode.BottomOfAABBHitbox);
                    _state = "stand";
                    _velocityY = 0f;
                }
            }
            else
            {
                // Wenn unser Programm diesen else-Block erreicht, haben die Teststrahlen kein Objekt 
                // unterhalb des Spielers getroffen - also muss der Status in "springt/fällt" geändert werden:
                if(_state == "stand")
                { 
                    _state = "in air";
                }
            }

            // Problemfall #2:
            // -----------------------
            // Die Spielfigur kollidiert mit einem Hindernis, das nicht zwingend ein Bodenobjekt ist
            // und muss sich so ausrichten, dass es nicht mehr mit diesem Objekt kollidiert.
            // Dafür holt sich unsere Spielfigur eine Liste aller für sie geltenden Kollisionen. 
            // Kollidiert sie mit keinem Objekt hat die Liste eine Länge (Count) von 0.
            List<Intersection> intersections = GetIntersections();
            
            // Durchlaufe alle Kollisionen in einer Schleife:
            foreach(Intersection i in intersections)
            {
                // Beispiel: Sollte das in 
                if(i.Object is PowerUp)
                {
                    // Wenn es sich bei dem Kollisionsobjekt um ein Objekt der Art "PowerUp" handelt,
                    // soll sich die Spielfigur nicht neu ausrichten, aber hier könnte Code stehen,
                    // der das Aufnehmen des PowerUps behandelt.
                }
                else
                {
                    // Andernfalls bietet jede Kollision (Intersection) das Feld "MTV" an.
                    // MTV steht für "minimum-translation-vector" und beinhaltet die Bewegung
                    // die die Spielfigur machen müsste, um nicht mehr zu kollidieren:
                    MoveOffset(i.MTV);

                    // Sonderfall:
                    // Wenn die Spielfigur gerade in der Luft ist und die Y-Komponente
                    // des MTV-Vektors negativ (kleiner 0) ist, dann bedeutet das, dass die
                    // Korrektur nach unten erfolgt. Daraus können wir schließen, dass die 
                    // Spielfigur mit einem Objekt von unten kollidiert ist (z.B. mit einer Raumdecke).
                    // In diesem Fall müssen wir die Sprunggeschwindigkeit auf 0 setzen, damit unser
                    // Spieler nicht mehr in der Höhe aufsteigt sondern zu fallen beginnt:
                    if (_state == "in air" && i.MTV.Y < 0f)
                    {
                        _velocityY = 0f;
                    }
                }
            }
        }
    }
}

Ergebnis


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.