Ich versuche, mich in Verhaltensbäumen zurechtzufinden, also schreibe ich einen Testcode heraus. Eine Sache, mit der ich zu kämpfen habe, ist, wie man einen gerade laufenden Knoten verhindert, wenn etwas mit höherer Priorität auftaucht.
Betrachten Sie den folgenden einfachen, fiktiven Verhaltensbaum für einen Soldaten:
Angenommen, es sind einige Zecken vorbeigekommen und es war kein Feind in der Nähe, der Soldat stand auf Gras, und der Sitzknoten wurde zur Ausführung ausgewählt:
Jetzt dauert die Ausführung der Aktion " Hinsetzen" einige Zeit, da eine Animation abgespielt werden kann, die Running
als Status zurückgegeben wird. Ein oder zwei Häkchen vergehen, die Animation läuft noch, aber der Feind in der Nähe? Bedingungsknoten löst aus. Jetzt müssen wir die preempt Hinsetzen Knoten so schnell wie möglich , damit wir die ausführen kann Angriff Knoten. Im Idealfall würde der Soldat das Sitzen nicht beenden - er könnte stattdessen seine Animationsrichtung umkehren, wenn er gerade erst anfängt zu sitzen. Für zusätzlichen Realismus, wenn er einen Wendepunkt in der Animation überschritten hat, können wir stattdessen festlegen, dass er aufhört, sich zu setzen und wieder aufzustehen, oder dass er in seiner Hast stolpert, um auf die Bedrohung zu reagieren.
Wie auch immer, ich habe keine Anleitung gefunden, wie ich mit dieser Situation umgehen soll. All die Literatur und Videos, die ich in den letzten Tagen konsumiert habe (und es war eine Menge), scheinen dieses Problem zu umgehen. Das Nächste, was ich finden konnte, war das Zurücksetzen von aktiven Knoten, aber das gibt Knoten wie Sit down nicht die Möglichkeit zu sagen: "Hey, ich bin noch nicht fertig!"
Ich dachte daran, vielleicht eine Preempt()
oder Interrupt()
-Methode für meine Basisklasse zu definieren Node
. Verschiedene Knoten können damit umgehen, wie sie es für richtig halten, aber in diesem Fall würden wir versuchen, den Soldaten so schnell wie möglich wieder auf die Füße zu bekommen und dann zurückzukehren Success
. Ich denke, dieser Ansatz würde auch erfordern, dass meine Basis Node
das Konzept der Bedingungen getrennt von anderen Aktionen hat. Auf diese Weise kann die Engine nur die Bedingungen überprüfen und, falls sie erfolgreich sind, einen aktuell ausgeführten Knoten vor der Ausführung der Aktionen deaktivieren. Wenn diese Unterscheidung nicht hergestellt würde, müsste die Engine Knoten wahllos ausführen und könnte daher eine neue Aktion auslösen, bevor die laufende Aktion verhindert wird.
Nachstehend finden Sie meine aktuellen Basisklassen. Auch dies ist eine Spitze, daher habe ich versucht, die Dinge so einfach wie möglich zu halten und die Komplexität nur dann zu erhöhen, wenn ich sie brauche und wenn ich sie verstehe, womit ich gerade zu kämpfen habe.
public enum ExecuteResult
{
// node needs more time to run on next tick
Running,
// node completed successfully
Succeeded,
// node failed to complete
Failed
}
public abstract class Node<TAgent>
{
public abstract ExecuteResult Execute(TimeSpan elapsed, TAgent agent, Blackboard blackboard);
}
public abstract class DecoratorNode<TAgent> : Node<TAgent>
{
private readonly Node<TAgent> child;
protected DecoratorNode(Node<TAgent> child)
{
this.child = child;
}
protected Node<TAgent> Child
{
get { return this.child; }
}
}
public abstract class CompositeNode<TAgent> : Node<TAgent>
{
private readonly Node<TAgent>[] children;
protected CompositeNode(IEnumerable<Node<TAgent>> children)
{
this.children = children.ToArray();
}
protected Node<TAgent>[] Children
{
get { return this.children; }
}
}
public abstract class ConditionNode<TAgent> : Node<TAgent>
{
private readonly bool invert;
protected ConditionNode()
: this(false)
{
}
protected ConditionNode(bool invert)
{
this.invert = invert;
}
public sealed override ExecuteResult Execute(TimeSpan elapsed, TAgent agent, Blackboard blackboard)
{
var result = this.CheckCondition(agent, blackboard);
if (this.invert)
{
result = !result;
}
return result ? ExecuteResult.Succeeded : ExecuteResult.Failed;
}
protected abstract bool CheckCondition(TAgent agent, Blackboard blackboard);
}
public abstract class ActionNode<TAgent> : Node<TAgent>
{
}
Hat jemand eine Einsicht, die mich in die richtige Richtung lenken könnte? Geht mein Denken in die richtige Richtung oder ist es so naiv, wie ich befürchte?
quelle
Stop()
Rückrufs vor dem Verlassen der aktiven Knoten)Antworten:
Ich stellte die gleiche Frage wie Sie und führte ein kurzes Gespräch im Kommentarbereich dieser Blogseite, in dem mir eine andere Lösung des Problems angeboten wurde.
Als Erstes müssen Sie einen gleichzeitigen Knoten verwenden. Concurrent Node ist ein spezieller Typ von Composite Node. Es besteht aus einer Folge von Vorbedingungsprüfungen, gefolgt von einem einzelnen Aktionsknoten. Es werden alle untergeordneten Knoten aktualisiert, auch wenn sich der Aktionsknoten im aktiven Zustand befindet. (Im Gegensatz zum Sequenzknoten, dessen Aktualisierung vom aktuell ausgeführten untergeordneten Knoten gestartet werden muss.)
Die Hauptidee besteht darin, zwei weitere Rückgabestatus für Aktionsknoten zu erstellen: "Abbrechen" und "Abgebrochen".
Das Fehlschlagen der Voraussetzungsprüfung im gleichzeitigen Knoten ist ein Mechanismus, der das Abbrechen des ausgeführten Aktionsknotens auslöst. Wenn der Aktionsknoten keine langfristige Abbruchlogik erfordert, gibt er sofort "Abgebrochen" zurück. Andernfalls wird in den Status "Abbrechen" gewechselt, in dem Sie alle erforderlichen Logik für die korrekte Unterbrechung der Aktion einfügen können.
quelle
Ich denke, dein Soldat könnte in Körper und Geist zerlegt werden (und was auch immer). Anschließend kann der Körper in Beine und Hände zerlegt werden. Dann benötigt jeder Teil seinen eigenen Verhaltensbaum und auch eine öffentliche Schnittstelle - für Anforderungen von Teilen höherer oder niedrigerer Ebene.
Anstatt jede einzelne Aktion im Mikromodus zu verwalten, senden Sie einfach Sofortnachrichten wie "Körper, setzen Sie sich für einige Zeit" oder "Körper, rennen Sie dorthin", und der Körper verwaltet Animationen, Zustandsübergänge, Verzögerungen und andere Dinge für Sie.
Alternativ kann der Körper solche Verhaltensweisen auch selbst steuern. Wenn es keine Befehle hat, kann es den Verstand fragen, "können wir hier sitzen?". Interessanter ist, dass Sie aufgrund der Verkapselung leicht Merkmale wie Müdigkeit oder Betäubung modellieren können.
Sie können sogar Teile austauschen - Elefanten mit dem Verstand eines Zombies herstellen, dem Menschen Flügel verleihen (er merkt es nicht einmal) oder was auch immer.
Ohne eine solche Zersetzung laufen Sie wahrscheinlich früher oder später Gefahr, einer kombinatorischen Explosion zu begegnen.
Auch: http://www.valvesoftware.com/publications/2009/ai_systems_of_l4d_mike_booth.pdf
quelle
Gestern Abend im Bett gelegen, hatte ich eine Art Offenbarung darüber, wie ich vorgehen könnte, ohne die Komplexität einzuführen, zu der ich mich in meiner Frage neigte. Es handelt sich um die Verwendung des (nach IMHO ungenannten) "parallelen" Verbundwerkstoffs. Hier ist was ich denke:
Hoffentlich ist das noch ziemlich lesbar. Die wichtigen Punkte sind:
Ich denke, dass dies funktionieren wird (ich werde es bald in meinem Spike versuchen), obwohl es etwas chaotischer ist, als ich es mir vorgestellt hatte. Das Gute ist, dass ich in der Lage sein würde, Teilbäume als wiederverwendbare Teile der Logik zu kapseln und von mehreren Punkten aus auf sie zu verweisen. Das wird die meisten meiner Sorgen dort lindern, daher halte ich dies für eine praktikable Lösung.
Natürlich würde ich immer noch gerne hören, ob jemand darüber nachdenkt.
UPDATE : obwohl dieser Ansatz technisch funktioniert, habe ich es sux entschieden. Dies liegt daran, dass nicht verwandte Teilbäume die in anderen Teilen des Baums definierten Bedingungen "kennen" müssen, damit sie ihren eigenen Untergang auslösen können. Das Teilen von Teilbaumreferenzen würde zwar dazu beitragen, diesen Schmerz zu lindern, aber es widerspricht immer noch den Erwartungen beim Betrachten des Verhaltensbaums. In der Tat habe ich denselben Fehler zweimal an einem sehr einfachen Spike gemacht.
Daher gehe ich den anderen Weg: explizite Unterstützung für das Preempting innerhalb des Objektmodells und ein spezielles Composite, das die Ausführung einer anderen Gruppe von Aktionen ermöglicht, wenn das Preempting auftritt. Ich werde eine separate Antwort posten, wenn etwas funktioniert.
quelle
Preempt()
Methode, die durch den Baum sickern würde. Das einzige, was dies wirklich "handhaben" würde, wäre das Preempt-Composite, das sofort zu seinem Preempt-Child-Knoten wechseln würde.Hier ist die Lösung, für die ich mich jetzt entschieden habe ...
Node
hat eineInterrupt
Methode, die standardmäßig nichts tutbool
(was bedeutet, dass sie schnell ausgeführt werden können und niemals mehr als ein Update benötigen).Node
macht eine Auflistung von Bedingungen separat zu ihrer Auflistung von untergeordneten Knoten verfügbarNode.Execute
Führt zuerst alle Bedingungen aus und schlägt sofort fehl, wenn eine Bedingung fehlschlägt. Wenn die Bedingungen erfolgreich sind (oder es keine gibt), ruft es auf,ExecuteCore
damit die Unterklasse ihre eigentliche Arbeit erledigen kann. Es gibt einen Parameter, der das Überspringen von Bedingungen aus den nachfolgend aufgeführten Gründen ermöglichtNode
Ermöglicht auch die isolierte Ausführung von Bedingungen über eineCheckConditions
Methode. NatürlichNode.Execute
ruft eigentlich nur an,CheckConditions
wenn es um die Validierung von Bedingungen gehtSelector
Composite ruft jetztCheckConditions
jedes Kind auf, das es für die Ausführung in Betracht zieht. Wenn die Bedingungen nicht erfüllt sind, bewegt es sich direkt zum nächsten Kind. Wenn sie bestehen, wird geprüft, ob bereits ein ausführendes Kind vorhanden ist. Wenn ja, ruft es aufInterrupt
und schlägt dann fehl. Das ist alles, was es an diesem Punkt tun kann, in der Hoffnung, dass der aktuell laufende Knoten auf die Interrupt-Anfrage reagiert, was es tun kann, indem es ...Interruptible
Knoten hinzugefügt , der eine Art Spezialdekorateur ist, weil er den regulären Logikfluss als dekoriertes Kind hat und dann einen separaten Knoten für Unterbrechungen. Es führt sein reguläres Kind bis zur Vollendung oder zum Scheitern aus, solange es nicht unterbrochen wird. Bei einer Unterbrechung wird sofort auf die Ausführung des untergeordneten Knotens für die Unterbrechungsbehandlung umgeschaltet, der ein beliebig komplexer Teilbaum sein kannDas Endergebnis ist ungefähr so, entnommen aus meinem Dorn:
Das Obige ist der Verhaltensbaum für eine Biene, die Nektar sammelt und in ihren Bienenstock zurückbringt. Wenn es keinen Nektar hat und nicht in der Nähe einer Blume ist, die einige hat, wandert es:
Wenn dieser Knoten nicht unterbrechbar wäre, würde er niemals ausfallen, sodass die Biene immer weiter wandern würde. Da der übergeordnete Knoten jedoch ein Selektor ist und untergeordnete Knoten mit höherer Priorität hat, wird die Berechtigung zur Ausführung ständig überprüft. Wenn ihre Bedingungen erfüllt sind, löst der Selektor eine Unterbrechung aus, und der darüber liegende Unterbaum wechselt sofort zum Pfad "Unterbrochen", der bei einem Fehler so schnell wie möglich beendet wird. Es könnte natürlich zuerst einige andere Aktionen ausführen, aber mein Spike hat eigentlich nichts anderes zu tun als Kaution.
Um dies mit meiner Frage in Verbindung zu bringen, könnten Sie sich vorstellen, dass der "Unterbrochene" Pfad versuchen könnte, die Sitzanimation umzukehren, und andernfalls den Soldaten stolpern lassen. All dies würde den Übergang in den Zustand höherer Priorität aufhalten, und genau das war das Ziel.
Ich denke, ich bin mit diesem Ansatz zufrieden - insbesondere mit den Kernelementen, die ich oben skizziere -, aber um ehrlich zu sein, wirft er weitere Fragen zur Verbreitung spezifischer Implementierungen von Bedingungen und Aktionen und zur Einbindung des Verhaltensbaums in das Animationssystem auf. Ich bin mir nicht mal sicher, ob ich diese Fragen noch artikulieren kann, also werde ich weiter nachdenken.
quelle
Ich habe das gleiche Problem behoben, indem ich den "Wann" -Dekorateur erfunden habe. Es hat eine Bedingung und zwei kindliche Verhaltensweisen ("dann" und "anders"). Wenn "When" ausgeführt wird, prüft es den Zustand und läuft je nach Ergebnis dann / sonst child. Wenn sich das Ergebnis der Bedingung ändert, wird das laufende untergeordnete Element zurückgesetzt und das dem anderen Zweig entsprechende untergeordnete Element gestartet. Wenn das Kind die Ausführung beendet, beendet das ganze "Wann" die Ausführung.
Der entscheidende Punkt ist, dass im Gegensatz zum anfänglichen BT in dieser Frage, bei dem der Zustand nur beim Start der Sequenz überprüft wird, mein "Wann" den Zustand weiterhin überprüft, während er ausgeführt wird. Daher wird der obere Rand des Verhaltensbaums durch Folgendes ersetzt:
Für fortgeschrittenere "Wann" -Verwendung würde man auch eine "Warte" -Aktion einführen wollen, die einfach für einen bestimmten Zeitraum oder auf unbestimmte Zeit nichts tut (bis sie durch das übergeordnete Verhalten zurückgesetzt wird). Wenn Sie nur einen Zweig von "Wann" benötigen, kann der andere entweder "Erfolg" - oder "Fehlgeschlagen" -Aktionen enthalten, die jeweils erfolgreich sind und sofort fehlschlagen.
quelle
Ich bin zwar spät dran, hoffe aber das kann helfen. Vor allem, weil ich sicherstellen möchte, dass ich persönlich nichts verpasst habe, da ich auch versucht habe, dies herauszufinden. Ich habe diese Idee größtenteils von ausgeliehen
Unreal
, aber ohne sie zu einerDecorator
Eigenschaft auf einer Basis zu machenNode
oder stark mit der verbunden zu seinBlackboard
, ist sie allgemeiner.Dies wird einen neuen Knotentyp genannt einzuführen ,
Guard
die wie eine Kombination aus a istDecorator
, und istComposite
und einecondition() -> Result
Unterschrift neben einemupdate() -> Result
Es gibt drei Modi, die angeben, wie die Stornierung bei der
Guard
Rückgabe erfolgen soll,Success
oderFailed
die tatsächliche Stornierung hängt vom Anrufer ab. Also für einenSelector
Anruf einGuard
:.self
-> Bricht den VorgangGuard
(und sein laufendes untergeordnetes Element) nur ab, wenn er ausgeführt wird und die Bedingung erfüllt istFailed
.lower
-> Brechen Sie die Knoten mit niedrigerer Priorität nur ab, wenn sie ausgeführt werden und die BedingungSuccess
oder warRunning
.both
-> Beides.self
und.lower
abhängig von den Bedingungen und laufenden Knoten. Sie möchten self stornieren, wenn es ausgeführt wird, und würdenfalse
den ausgeführten Knoten bedingen oder stornieren, wenn sie aufgrund derComposite
Regel als niedriger eingestuft werden (Selector
in unserem Fall), wenn die Bedingung erfüllt istSuccess
. Mit anderen Worten, es sind im Grunde beide Konzepte kombiniert.So
Decorator
oderComposite
so braucht es nur ein einziges Kind.Obwohl
Guard
nehmen nur ein einziges Kind, können Sie nisten so vieleSequences
,Selectors
oder andere Arten ,Nodes
wie Sie wollen, einschließlich andererGuards
oderDecorators
.Selector1 Guard.both[Sequence[EnemyNear?]] Sequence1 MoveToEnemy Attack Selector2 Sequence2 StandingOnGrass? Idle HumATune
Im obigen Szenario werden bei
Selector1
Aktualisierungen immer Zustandsüberprüfungen für die den untergeordneten Elementen zugeordneten Wachen ausgeführt. Im obigen FallSequence1
ist es bewacht und muss überprüft werden, bevorSelector1
mit denrunning
Aufgaben fortgefahren wird.Jedes Mal , wenn
Selector2
oderSequence1
läuft, sobaldEnemyNear?
kehrtsuccess
während einerGuards
condition()
Überprüfung dannSelector1
eine Unterbrechung Ausgabe / cancel auf dierunning
node
und dann wie gewohnt weiter.Mit anderen Worten, wir können auf einen "Leerlauf" - oder einen "Angriff" -Zweig reagieren, basierend auf ein paar Bedingungen, die das Verhalten weitaus reaktiver machen, als wenn wir uns entschieden hätten
Parallel
Auf diese Weise können Sie auch Einzelpersonen
Node
mit höherer Priorität davor schützen ,Nodes
in derselben zu laufenComposite
Selector1 Guard.both[Sequence[EnemyNear?]] Sequence1 MoveToEnemy Attack Selector2 Guard.both[StandingOnGrass?] Idle HumATune
Wenn
HumATune
es ein langer Lauf istNode
,Selector2
wird immer zuerst geprüft, ob es nicht für dieGuard
. Wenn der npc also beim nächsten Mal auf eine Rasenfläche teleportiertSelector2
wird, überprüft er dasGuard
und brichtHumATune
ab, um zu laufenIdle
Wenn es aus dem Grasfeld teleportiert wird, bricht es den laufenden Knoten (
Idle
) ab und bewegt sich zuHumATune
Wie Sie hier sehen, hängt die Entscheidungsfindung vom Anrufer
Guard
und nicht von ihmGuard
selbst ab. Die Regeln, wer als wer gilt,lower priority
verbleiben beim Anrufer. In beiden Beispielen ist es derjenige,Selector
der definiert, was a ausmachtlower priority
.Wenn Sie einen
Composite
Aufruf hättenRandom Selector
, würden Sie die Regeln innerhalb der Implementierung dieses spezifischen definierenComposite
.quelle