Game Engine Math

KWEngine, Teil 25: Objektnavigation mit Flowfields

  • Ein Flowfield ist eine zweidimensionale Zellstruktur und hat eine frei wählbare Anzahl von Zellen, die alle einen frei wählbaren Zellradius besitzen.
  • Ein Flowfield ist unsichtbar (und nur für dieses Tutorial visualisiert).
  • Es scannt alle innerhalb des Feldes liegenden Objekte und notiert sich je Objekt die „Kosten“ (zwischen 1 und 255). Je „teurer“ ein Objekt, desto eher wird das Flowfield versuchen, einen Weg zum Ziel zu generieren, der an diesem Objekt vorbeiführt.
  • Wird eine der Zellen als Ziel markiert, berechnet das Flowfield pro Zelle die bestmögliche Wegrichtung, die ein Objekt einschlagen müsste, um zu diesem Ziel zu gelangen.
  • Jedes in der Welt befindliche Objekt kann nun selbst entscheiden, ob es dieser Wegrichtung folgt oder nicht.
  • In jeder Welt kann jeweils nur ein Flowfield generiert werden.

Im unten abgebildeten Beispiel ist das gelbe Objekt das Ziel und die pinke Kugel die Spielfigur:

Die Zielposition kann in Echtzeit verändert werden. Auch die Hindernisse dürfen sich währenddessen bewegen:

Wie konfiguriert man ein Flowfield?

Für dieses Beispiel wird vorausgesetzt, dass zuvor die GameObject-Klassen „Impassable„, „Wall„, „Player“ und „Enemy“ angelegt wurden.

Anlegen einer Welt-Klasse
using KWEngine3;
using KWEngine3.GameObjects;
using KWEngine3.Helper;
using OpenTK.Mathematics;
using OpenTK.Windowing.GraphicsLibraryFramework;

namespace FlowfieldTutorial
{
    public class FlowFieldWorld : World
    {
        public override void Act()
        {
           
        }

        public override void Prepare()
        {
            SetCameraPosition(0, 15, 0);
            SetCameraTarget(0, 0, 0);
            SetColorAmbient(1, 1, 1);

            Impassable impassable1= new Impassable();
            impassable1.SetScale(4, 1, 2);
            impassable1.SetColor(1, 1, 1);
            impassable1.FlowFieldCost = 255;      // Höchste Kosten bedeutet: unüberwindbar!
            impassable1.IsCollisionObject = true;
            AddGameObject(impassable1);

            Wall wall1 = new Impassable();
            wall1.SetScale(4, 1, 2);
            wall1.SetColor(1, 1, 1);
            wall1.SetPosition(0, 0, 4);
            wall1.FlowFieldCost = 255;            // Höchste Kosten bedeutet: unüberwindbar!
            wall1.IsCollisionObject = true;
            AddGameObject(wall1);

            Player p = new Player();
            p.SetPosition(-4.5f, 0.5f, 3.5f);
            p.SetColor(1, 1, 0);
            p.IsCollisionObject = true;
            AddGameObject(p);

            Enemy e = new Enemy();
            e.SetModel("KWSphere");
            e.SetPosition(4.5f, 0.5f, 3.5f);
            e.SetColor(1, 0, 1);
            e.IsCollisionObject = true;
            AddGameObject(e);

            // Hier findet das Anlegen und die Konfiguration des Flowfields statt:
            FlowField f = new FlowField(
                0,                               // x-Position der Feldmitte
                0,                               // y-Position der Feldmitte
                0,                               // z-Position der Feldmitte
                10,                              // Anzahl der Zellen in X-Richtung
                8,                               // Anzahl der Zellen in Z-Richtung
                0.5f,                            // Radius je Zelle
                1,                               // Höhe des Flowfields (für Wegfindung irrelevant)
                typeof(Impassable), typeof(Wall) // Liste der Klassen, die das Flowfield beachten soll
            );
            f.IsVisible = true;                  // Visualisiert das Flowfield (zu Debugging-Zwecken)
            SetFlowField(f);                     // Verankert das Flowfield-Objekt in der Welt
        }
    }
}
Act()-Methode in der Welt zum Festlegen des Ziels

Im obigen Code-Beispiel fehlt der Inhalt der Act()-Methode. Dieser wird jetzt hier separat vorgestellt.

public override void Act()
{
    // Wenn die linke Maustaste betätigt wird...
    if(Mouse.IsButtonPressed(MouseButton.Left))
    {
        // ...ermittle die 3D-Koordinaten des Mauszeigers...
        Vector3 cursorPos = HelperIntersection.GetMouseIntersectionPointOnPlane(
            Plane.XZ, // ...auf der XZ-Ebene...
            0         // ...auf der Höhe 0.
        );

        // Hole anschließend die Referenz auf das aktuelle Flowfield:
        FlowField f = GetFlowField();

        // Wenn das FlowField nicht leer ist und außerdem noch die 
        // Mauszeigerkoordinaten innerhalb des Flowfields liegen,...
        if(f != null && f.ContainsXZ(cursorPos);
        {
            // ...setze im Flowfield die für diese Koordinaten 
            // zuständige Zelle als Ziel:
            f.SetTarget(cursorPosWorld);
        }
    }          
}
Eine Klasse muss sich jetzt an diesem Flowfield orientieren
using KWEngine3.GameObjects;
using KWEngine3.Helper;
using OpenTK.Mathematics;
using OpenTK.Windowing.GraphicsLibraryFramework;

namespace FlowFieldTutorial
{
    public class Player : GameObject
    {
        public override void Act()
        {
            // Erstelle eine Variable, die die aktuelle Bewegungsrichtung festlegt:
            Vector3 myNewDirection = Vector3.Zero;

            // Erfrage die Referenz auf das Flowfield in der aktuellen Welt:
            FlowField f = CurrentWorld.GetFlowField();

            // Ist die Referenz nicht leer, ...
            if(f != null)
            {
                // ...prüfe, ob die eigene Position innerhalb des Flowfields liegt
                // und ob das Flowfield gerade ein gesetztes Ziel hat:
                if(f.ContainsXZ(this.Position) && f.HasTarget)
                {
                    // Überschreibe die Bewegungsrichtung mit der vom Flowfield
                    // vorgeschlagenen Bewegung:
                    myNewDirection = f.GetBestDirectionForPosition(this.Position);
                }
            }

            // Wenn die Bewegung nicht 0 ist, dann bewege die Figur entlang dieser 
            // Richtung mit der Beispielgeschwindigkeit 0.01f:
            if(myNewDirection != Vector3.Zero)
            {
                MoveAlongVector(myNewDirection, 0.01f);
            }

            // Die folgenden Zeilen sind nur normale Kollisionsbehandlung und haben
            // mit dem Flowfield selbst nichts zu tun:
            List<Intersection> intersections = GetIntersections();
            foreach(Intersection intersection in intersections)
            {
                MoveOffset(intersection.MTV);
            }
        }
    }
}

Was muss ich tun, wenn sich Objekte innerhalb des Flowfields verändern?

Das Flowfield muss neu berechnet werden, wenn…

  • …sich Objekte innerhalb des Flowfields in ihrer Position, Größe oder Rotation verändern.
  • …sich die Kosten eines Objekts innerhalb des Flowfields verändern.
FlowField f = KWEngine.CurrentWorld.GetFlowField();
if(f != null)
{
    f.Update();
}

Wichtig:
Weil das Aktualisieren des Flowfield viele Berechnungen erfordert, wird jedes Flowfield intern mit lediglich 30FPS aktualisiert.


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.