Wie greifen KI-Agenten auf Informationen zu ihrer Umgebung zu?


9

Dies mag eine triviale Frage sein, aber ich habe Probleme, dies zu verstehen. Würde mich sehr über Ihre Hilfe freuen.

In der Spieleentwicklung mit objektorientiertem Design möchte ich verstehen, wie KI-Agenten auf die Informationen zugreifen, die sie aus der Spielwelt benötigen, um ihre Aktionen auszuführen.

Wie wir alle wissen, müssen KI-Agenten in Spielen sehr oft „ihre Umgebung wahrnehmen“ und entsprechend dem handeln, was um sie herum geschieht. Beispielsweise kann ein Agent so programmiert sein, dass er den Spieler verfolgt, wenn er nahe genug kommt, Hindernisse während der Bewegung vermeidet (unter Verwendung des Lenkverhaltens zur Vermeidung von Hindernissen) usw.

Mein Problem ist, dass ich nicht sicher bin, wie ich das machen soll. Wie kann ein KI-Agent auf die Informationen zugreifen, die er über die Spielwelt benötigt?

Ein möglicher Ansatz besteht darin, dass die Agenten einfach die benötigten Informationen direkt von der Spielwelt anfordern.

Es gibt eine Klasse namens GameWorld. Es behandelt wichtige Spiellogiken (Spielschleife, Kollisionserkennung usw.) und enthält Verweise auf alle Entitäten im Spiel.

Ich könnte diese Klasse zu einem Singleton machen. Wenn ein Agent Informationen aus der Spielwelt benötigt, erhält er diese einfach direkt von der GameWorld-Instanz.

Beispielsweise kann ein Agent für Seekden Spieler programmiert werden , wenn er sich in der Nähe befindet. Dazu muss der Agent die Position des Spielers ermitteln. So kann es einfach direkt angefordert werden : GameWorld.instance().getPlayerPosition().

Ein Agent könnte auch einfach die Liste aller Entitäten im Spiel abrufen und sie auf ihre Bedürfnisse analysieren (um herauszufinden, welche Entitäten in der Nähe sind oder irgendetwas anderes): GameWorld.instance().getEntityList()

Dies ist der einfachste Ansatz: Agenten wenden sich direkt an die GameWorld-Klasse und erhalten die benötigten Informationen. Dies ist jedoch der einzige Ansatz, den ich kenne. Gibt es eine bessere?

Wie würde ein erfahrener Spieleentwickler dies gestalten? Ist der Ansatz "Holen Sie sich eine Liste aller Entitäten und suchen Sie nach dem, was Sie brauchen" naiv? Welche Ansätze und Mechanismen ermöglichen es KI-Agenten, auf die Informationen zuzugreifen, die sie zur Ausführung ihrer Aktionen benötigen?


Wenn Sie Zugang zu einem GDCVault-Abonnement haben, gab es 2013 einen ausgezeichneten Vortrag mit dem Titel "Erstellen der KI für die lebendige, atmende Welt der Hitman-Absolution", der ausführlich auf das KI-Wissensmodell eingeht.
DMGregory

Antworten:


5

Was Sie beschreiben, ist ein klassisches "Pull" -Modell zur Abfrage der Welt. Meistens funktioniert dies ziemlich gut, insbesondere für Spiele mit einfacher KI (was am meisten ist). Es gibt jedoch einige Punkte, die Sie berücksichtigen sollten und die möglicherweise Nachteile haben:

  • Sie möchten wahrscheinlich den Puffer verdoppeln. Siehe Spielprogrammiermuster zu diesem Thema . Indem Sie die Daten immer direkt von der Welt anfordern, können Sie seltsame Rennbedingungen erhalten, bei denen das Ergebnis davon abhängt, in welcher Reihenfolge die KI aufgerufen wird. Ob dies für Ihr Spiel wichtig ist, müssen Sie selbst bestimmen. Ein mögliches Ergebnis ist, dass das Spiel auf denjenigen ausgerichtet wird, der "zuerst" oder "zuletzt" spielt, was den Mehrspielermodus unfair macht.

  • Stapelanfragen können häufig wesentlich effizienter sein, insbesondere an bestimmte Datenstrukturen. Hier können Sie jeden KI-Agenten, der nach Hindernissen suchen möchte, dazu bringen, ein "Abfrageobjekt" zu erstellen und dieses bei einem zentralen Hindernis-Singleton zu registrieren. Dann werden vor der Haupt-AI-Schleife alle Abfragen für die Datenstruktur ausgeführt, wodurch die Hindernisdatenstruktur mehr im Cache bleibt. Während des AI-Teils verarbeitet jeder Agent seine Abfrageergebnisse, darf diese jedoch nicht direkter erstellen. Am Ende des Frames aktualisieren die AI-Objekte die Abfragen mit ihrem neuen Speicherort oder fügen sie hinzu oder entfernen sie. Dies ähnelt dem datenorientierten Design .

    Beachten Sie, dass dies im Grunde genommen eine doppelte Pufferung bewirkt, indem das Ergebnis der Abfragen in einem Puffer gespeichert wird. Außerdem müssen Sie vorhersehen, ob Sie den Frame zuvor abfragen müssen. Dies ist ein "Push" -Modell, da die Agenten angeben, für welche Art von Aktualisierungen sie interessiert sind (indem sie ein entsprechendes Abfrageobjekt erstellen), und diese Aktualisierungen an sie weitergeleitet werden. Beachten Sie, dass das Abfrageobjekt auch einen Rückruf enthalten kann, anstatt alle Ergebnisse für einen Frame zu speichern.

  • Schließlich möchten Sie wahrscheinlich Schnittstellen oder Komponenten für Ihre durchsuchbaren Objekte verwenden, anstatt die Vererbung, die an anderer Stelle gut dokumentiert ist. Das Durchlaufen einer Liste von EntitiesÜberprüfungen instanceOfist wahrscheinlich ein Rezept für ziemlich fragilen Code, genau in der Minute, in der Sie beides wollen StaticObjectund MovingObjectsein sollen Healable. (es sei denn, es instanceOffunktioniert für Schnittstellen in der Sprache Ihrer Wahl.)


5

Da KI teuer ist, ist Leistung oft der treibende Faktor in der Architektur.

Um Ihre Bedenken hinsichtlich Datenzugriffsmodellen zu zerstreuen, betrachten wir einige verschiedene KI-Beispiele sowohl innerhalb als auch außerhalb der Spielebranche, die von dem, was von der menschlichen Navigation am weitesten entfernt ist, bis zu dem, was uns am vertrautesten ist.

(Jedes Beispiel setzt eine einzelne globale Logikaktualisierung voraus.)

  • Ein * kürzester WegJede KI berechnet den Status der Karte für Pfadfindungszwecke. Ein * erfordert, dass jede KI bereits die gesamte (lokale) Umgebung kennt, in der sie Pfad finden muss. Daher müssen wir ihr Informationen über Kartenhindernisse und den Raum (häufig ein boolesches 2D-Array) übergeben. A * ist eine spezielle Form des Dijkstra-Algorithmus, eines Suchalgorithmus für offene Graphen mit kürzestem Pfad. Solche Ansätze geben Listen zurück, die den Pfad darstellen, und bei jedem Schritt wählt die KI einfach den nächsten Knoten in dieser Liste aus, zu dem sie gehen soll, bis sie ihr Ziel erreicht oder eine Neuberechnung erforderlich ist (z. B. aufgrund einer Änderung des Kartenhindernisses). Ohne dieses Wissen kann kein realistischer kürzester Weg gefunden werden. A * ist der Grund, warum AIs in RTS-Spielen immer wissen, wie sie von Punkt A nach Punkt B gelangen - wenn ein Pfad vorhanden ist. Es wird den kürzesten Weg für jede einzelne KI neu berechnen. weil die Pfadgültigkeit auf der Position der AIs basiert, die zuvor verschoben wurden (und möglicherweise bestimmte Pfade blockiert haben). Der iterative Prozess, mit dem A * die Zellwerte während der Pfadfindung berechnet, ist ein mathematischer Konvergenzprozess. Man könnte sagen, das Endergebnis ähnelt einem Geruchssinn, der mit einem Sehsinn vermischt ist, aber insgesamt ist es unserer Denkweise etwas fremd.

  • Kollaborative Diffusion Auch in Spielen zu finden, ähnelt dies am ehesten einem Geruchssinn, der auf der Diffusion von Gasen und Partikeln basiert. CD löst das Problem der kostspieligen Nachbearbeitung in A *: Stattdessen wird ein einzelner Kartenstatus gespeichert, der einmal pro Aktualisierung für alle AIs verarbeitet wird, und die AIs greifen dann nacheinander auf die Ergebnisse zu, damit sie ihre jeweilige Bewegung ausführen können . Ein einzelner Pfad (Liste der Zellen) wird nicht mehr von einem Suchalgorithmus zurückgegeben. Stattdessen überprüft jede KI die Karte, nachdem sie verarbeitet wurde, und wechselt zu der benachbarten Zelle mit dem höchsten Wert. Dies nennt man Bergsteigen . Trotzdem muss die Kartenverarbeitungsphase schon seinhaben zuvor Zugriff auf die Karteninformationen, die hier auch die Standorte aller KI-Körper enthalten. Daher verweist die Karte auf AIs, und dann verweisen AIs auf die Karte.

  • Computer Vision und Raycasting + kürzester Weg In der Rover- und Drohnenrobotik wird dies zur Norm, um die Ausdehnung der Räume zu bestimmen, in denen der Roboter navigiert. Auf diese Weise können Roboter ein vollständiges Volumenmodell ihrer Umgebung erstellen, so wie wir es durch Sehen oder sogar Tönen oder Berühren (für Blinde oder Taube) tun würden, das der Roboter dann auf ein minimales topografisches Diagramm (ein bisschen wie ein Wegpunktdiagramm) reduzieren kann wird in Spielen mit A *) verwendet, auf die dann Algorithmen mit kürzestem Pfad angewendet werden können. In diesem Fall kann "Vision" zwar einen Hinweis auf die unmittelbare Umgebung geben, führt aber dennoch dazuEine Diagrammsuche, die letztendlich den Weg zum Ziel liefert. Dies kommt dem menschlichen Denken nahe: Um vom Schlafzimmer aus in die Küche zu gelangen, muss ich durch das Wohnzimmer gehen. Die Tatsache, dass ich sie bereits gesehen habe und ihre Räume und Portale kenne, ermöglicht diesen kalkulierten Zug. Dies ist eine Graphentopologie, auf die ein Algorithmus mit dem kürzesten Pfad angewendet wird, obwohl er eher in weiches Protein als in hartes Silizium eingebettet ist.

Sie sehen also, dass die ersten beiden darauf angewiesen sind, die Umgebung bereits vollständig zu kennen. Dies ist aus Gründen der Kosten für die Bewertung einer Umgebung von Grund auf in Spielen üblich. Der letzte ist eindeutig der mächtigste. Ein auf diese Weise ausgestatteter Roboter (oder beispielsweise eine Spiel-KI, die den Tiefenpuffer jedes Frames liest) kann in jeder Umgebung ohne vorherige Kenntnis ausreichend navigieren. Wie Sie wahrscheinlich vermutet haben, ist dies auch bei weitem der teuerste der drei oben genannten Ansätze, und in Spielen können wir es uns normalerweise nicht leisten, dies pro KI zu tun. Natürlich ist es in 2D weitaus kostengünstiger als in 3D.

Architektonische Punkte

Oben wird klar, dass wir nicht nur ein korrektes Datenzugriffsmuster für AI annehmen können. Die Wahl hängt davon ab, was Sie erreichen möchten. Der GameWorlddirekte Zugriff auf die Klasse ist absolut Standard: Sie liefert lediglich Informationen zur Welt. Im Wesentlichen ist es Ihr Datenmodell, und dafür sind Datenmodelle gedacht. Singleton ist dafür in Ordnung.

"Holen Sie sich eine Liste aller Entitäten und suchen Sie nach dem, was Sie brauchen."

Daran ist überhaupt nichts Naives. Das einzige, was naiv sein könnte, ist, mehr Listeniterationen durchzuführen, als Sie benötigen. Bei der Kollisionserkennung vermeiden wir dies, indem wir z. B. Quadtrees verwenden, um den Suchraum zu reduzieren. Ähnliche Mechanismen können für KI gelten. Und wenn Sie dieselbe Schleife gemeinsam nutzen können, um mehrere Aufgaben auszuführen, tun Sie dies, da Zweige teuer sind.


Danke für die Antwort. Da ich ein Anfänger in der Spieleentwicklung bin, denke ich, dass ich mich vorerst an den einfachen Ansatz "Liste aus der Spielwelt holen" halten werde :) Eine Frage: In meiner Frage habe ich die GameWorldKlasse als die Klasse beschrieben, die Verweise auf enthält alle Spielentitäten und enthält auch die meisten wichtigen 'Engine'-Logik: die Hauptspielschleife, die Kollisionserkennung usw. Es ist im Grunde die' Hauptklasse 'des Spiels. Meine Frage ist: Ist dieser Ansatz in Spielen üblich? Haben Sie eine 'Hauptklasse'? Oder sollte ich es in kleinere Klassen aufteilen und eine Klasse als 'Entity Database'-Objekte abrufen können?
Aviv Cohn

@Prog Gern geschehen. Auch hier gibt es nichts in den oben genannten KI-Ansätzen (oder anderen), was darauf hindeutet, dass Ihre "Liste aus der Spielwelt erhalten" in irgendeiner Weise, Form oder Form architektonisch falsch ist. Die KI- Architektur muss den Anforderungen der KI entsprechen. Wie Sie vorschlagen, sollte diese Logik jedoch modularisiert und (in ihrer eigenen Klasse) von Ihrer breiteren Anwendungsarchitektur getrennt sein. Ja, Subsysteme sollten immer in separate Module zerlegt werden, wenn solche Fragen auftauchen. Ihr Leitprinzip sollte SRP sein .
Ingenieur

2

Grundsätzlich hätte ich zwei Möglichkeiten, Informationen abzufragen.

  1. Wenn sich das AIState ändert, weil Sie eine Kollision oder einen beliebigen Cache festgestellt haben, ist ein Verweis auf ein beliebiges Objekt wichtig. Auf diese Weise wissen Sie, welche Referenz Sie benötigen. Wenn andere Systeme in jedem Frame große Suchvorgänge ausführen müssen, würde ich empfehlen, sie huckepack zu nehmen, damit Sie nicht mehrere Suchvorgänge durchführen müssen. Eine "Kollision" mit der Zone, die einen feindlichen "Alarm" auslöst, sendet ihm eine Nachricht / ein Ereignis, das ihn bei diesem Objekt registriert, falls er es noch nicht ist, und ändert den Spielstatus in einen Status, in dem er sein Geschäft basierend auf seiner Anwesenheit erledigt dieser Spielzustand. Sie benötigen ein Ereignis, das Sie auffordert, Änderungen vorzunehmen. Ich würde lediglich einen Verweis auf den Rückruf übergeben, mit dem Sie diese Informationen angeben. Dies ist erweiterbarer, als sich nur mit dem Spieler befassen zu müssen. Vielleicht möchten Sie, dass ein Feind einen anderen Feind oder ein anderes Objekt verfolgt. Auf diese Weise müssen Sie nur das Tag ändern, anhand dessen Sie es identifizieren.

  2. Mit diesen Informationen führen Sie dann eine Abfrage an ein Pfadfindungssystem durch, das A * oder einen anderen Algorithmus verwendet, um Ihnen einen Pfad zu geben, oder Sie können ihn mit einem bestimmten Lenkverhalten verwenden. Vielleicht eine Kombination von beidem oder was auch immer. Grundsätzlich sollten Sie mit der Transformation von beiden in der Lage sein, Ihr Knotensystem oder Navmesh abzufragen und sich einen Pfad geben zu lassen. Ihre Spielwelt hat wahrscheinlich viele andere Dinge als die Wegfindung. Ich würde Ihre Anfrage nur an die Pfadfindung senden. Auch das Stapeln dieser Dinge ist wahrscheinlich am besten, wenn Sie viele Fragen haben, da dies ziemlich intensiv werden kann und das Stapeln die Leistung verbessert.

    Transform* targetTransform = nullptr;
    EnemyAIState  AIState = EnemyAIState::Idle;
    void OnTriggerEnter(GameObject* go)
    {
       if(go->hasTag(TAG_PLAYER))
       {
       //Cache important information that will be needed during pursuit
       targetTransform = go->getComponent<Transform>();
       AIState = EnemyAIState::Pursue;
       }
    }
    
    void Update()
    {
       switch(AIState)
       {
          case EnemyAIState::Pursue:
           //Find position to move to
           Vector3 nextNode = PathSystem::Seek(
                              transform->position,targetTransform->position);
           /*Update the position towards the target by whatever speed the unit moves
             Depending on how robust your path system is you might want to raycast
             against obstacles it can't take into account or might clip the path.*/
            transform->Move((nextNode - transform->position).unitVector()*speed*Time::deltaTime());
            break;
        }
     }
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.