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. Insgesamt sollte die Zellanzahl je Flowfield auf ca. 1000 Zellen beschränkt werden, um Performance-Einbußen zu vermeiden.
  • 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)
                FlowFieldMode.Simple             // Art der Suche nach Hindernissen pro Zelle
                typeof(Impassable), typeof(Wall) // Aufzählung der Klassen, die das Flowfield beachten soll
            );
            f.IsVisible = true;                  // Visualisiert das Flowfield (zu Debugging-Zwecken)
            f.Name = "FlowField#1";              // Setzt den Namen der Instanz (dies ist sehr wichtig!)
            AddFlowField(f);                     // Fügt das Flowfield-Objekt der Welt hinzu
        }
    }
}
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 = GetFlowFieldByName("FlowField#1");

        // 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 GameObject-Klasse KANN 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.GetFlowFieldByName(FlowField#1);

            // 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.GetFlowFieldByName("FlowField#1");
if(f != null)
{
    f.Update();
}

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


Objekte navigieren im FlowField auch ohne festgelegtes Ziel

GameObject-Instanzen können jederzeit erfragen, in welcher FlowField-Zelle sie sich gerade befinden. Wird eine passende Zelle gefunden, können sowohl ihre Kosten als auch ihre Nachbarzellen abgefragt werden. Auf diese Weise können GameObject-Instanzen herausfinden, ob sich Wände oder andere Hindernisse in ihrer Umgebung befinden.

using KWEngine;
using KWEngine3.GameObjects;
using KWEngine.Helper;
using OpenTK.Mathematics;

public class SomeClass : GameObject
{
    public override void Act()
    {
        FlowField f = CurrentWorld.GetFlowFieldByName("FlowField#1");
        if(f != null)
        {
            // Erfrage die aktuelle Zelle, in der sich die Instanz befindet:
            FlowFieldCell cell = f.GetCellForWorldPosition(this.Position);

            // Wenn eine Zelle gefunden wurde, prüfe die Kosten der Zelle im Norden:
            if(cell != null)
            {
                // Hole die Referenz auf die Zelle nördlich der eigentlichen Zelle:
                FlowFieldCell neighbourNorth = cell.GetNeighbourCellAtOffset(0, -1);
               
                // Wenn die Nachbarzelle existiert, dann erfrage ihre Kosten:
                if (neighbourNorth != null)
                {
                    int costNorth = neighbourNorth.Cost;
                }
            }
        }
    }
}

Beitrag veröffentlicht

in

von

Kommentare

Schreibe einen Kommentar

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.