KI des Roboters programmieren

roland

Active member
Hallo zusammen,

ich eröffne heute mal einen Thread wie ich die KI meines Roboters realisiert habe. Möglicherweise kann der eine oder ander ja einige Anregungen in dieser Hinsicht verwenden oder auch mir noch einige Tipps geben, wie ich die Sache verbessern kann. Aktuell habe ich nur das Mähverhalten abgebildet mit Perimetererkennung und Rundumbumper und das auch noch nicht bis zum Ende, da ich erst nächstes Jahr den Robbi wieder auf die Wiese schicke.

Als ich 2013 meinen ersten Rasenroboter gebaut habe, habe ich einen Subsumtion Algorithmus verwendet. Den aktuellen Roboter wollte ich dann mit einer Hierarchical Finite State Machine realisieren. Ich habe bezüglich der FSM ein sehr einfaches C++ Template gefunden und die Umsetzung des Verhaltens in das Programm war relative einfach. Dann kam aber der Zeitpunkt, wo ich den Rundumbumper eingebaut habe. Die Anpassung der FSM wurde komplexer, da ich zu diesem Zeitpunkt davon Ausging das in jedem State der Perimeter überfahren werden kann oder der Bumper aktiviert werden kann sowie dass ich das Mähverhalten nicht in unterschiedliche FSM aufteilen wollte . Daher habe ich eine andere Lösung gesucht und bin dann auf den BehavoiurTree gestossen. Diese Art eine KI zu programmieren wird in der Spieleindustrie bereits seit über 10 Jahren eingesetzt und das war der Anlass für mich mal zu erforschen was denn dahinter steckt und wie ich dieses in meinen Robbi einbringen kann.

Ich will hier nicht erklären, was BehaviourTrees sind. Dazu gibt es genug Artikel im Netz wie hier zum Beispiel: http://www.gamasutra.com/blogs/ChrisSimpson/20140717/221339/Behavior_trees_for_AI_How_they_work.php http://guineashots.com/2014/07/25/an-introduction-to-behavior-trees-part-1/

Nachdem ich mir diverse Artikle bezüglich Behavior Trees durchgelesen habe, ging es an die Umsetzung. Da es keine wirklich für mich 100%ig verwendbare Library gab, habe ich mir diese selber zusammen geschrieben. D.h. aus unterschiedlichen Quellen zusammengeführt und dann noch meinen Senf dazugegeben.

Die Umsetzung des Verhaltens von einer FSM zum Behaviour Tree hat sich am Anfang etwas schwierig gestalte, da der gedankliche Ansatz ein anderer war. Teilweise habe ich schon gezweifelt, ob die Nutzung des BHT für mich zu Aufwändig ist, da die Umsetzung mit der FSM dagegen relative einfach war - oder vielleicht doch nur eine Sache der Gewohnheit? Na ja, es wurde immer besser. Ich habe erst das Cruise Verhalten programmiert, dann das Perimeter Verhalten. Bei dem Bumperverhalten fehlte mir anfänglich die Idee, da ich diese ja bisher parallel zu dem Perimeterverhalten in der FSM geprüft habe. Ich habe mich daher erstmal für eine Subsumption Ansatz entschieden. D.h. der Bumper hat Priorität vor dem Perimeter und wird als erstes überprüft und abgehandelt, wenn ein Bumperereignis auftritt. Als ich dann die Escape Verhalten für die einzelnen Bumpereinschläge programmiert habe, habe ich als erstes den Vorteil dieser Art der Programmierung gesehen. Wenn man erstmal die Gedankengänge geändert hat von einer FSM auf den Behaviour Tree umzusteigen ist die Umsetzung genau so einfach. Der aktuelle Vorteil liegt aber in der Wiederverwendbarkeint der Nodes mit nur einem Befehl. So kann ich an einem Punkt eine Node einfügen die ich bereits woanders programmiert habe oder ich kann einen ganzen Zweig einfügen ohne Aufwand. Man kann dies sehr schön an dem mselRotate Zweig sehen in dem angefügten Bild.

Mein gesamt Programm habe ich so aufgebaut, dass es Service Threads gibt, die die Sensoren auslesen, die Motoren steuern oder die Position anfahren. Diese laufen im "Hindergrund". Die Services stellen ihre Dienste auf einem Blackboard zur Verfügung. Dieses wird in der tick() Funktion welche jedes Node implementiert hat übergeben.

Ich habe mal die alte FSM und den neuen Behaviour Tree rangehängt. Falls es jemanden gibt der mehr Infos haben möchte oder den Code benötigt, einfach sagen. Wenn jemand sowas schon mal umgesetzt hat, wäre gut wenn er mir seine Infos zukommen lassen könnte.

FSM.jpg


BehaviourTree.jpg
 
Hallo Roland,

super schöne Ausführung Deiner Arbeit. Danke dafür.
Leider sind die Bilder zu klein es ist nichts zu erkennen.
Kannst Du die nochmal mit einer besseren Qualität vielleicht gezippt reinstellen.

Gruß

Jürgen
 
Sensationelle Arbeit Roland! Mal wieder eine Softwareentwicklung bei der ich was abgucken kann ;-) Ich werde mich noch genauer in Deine Arbeit einlesen und schauen wieviel wir davon für den Ardumower übernehmen können.
 
Hallo Roland,

mich würde der Code auch interessieren. Nachdem Du von Threads schreibst scheint das bei Dir ja auf einer anderen Plattform zu laufen, ich selbst denke über eine RasPI (mit Realtime Kernel) Lösung mit über I2C angebundenen Arduino Minis nach.

VG
Rajiva
 
@rajiva
Ich verwende die Arduino thread Library (etwas für mich abgewandelt). Das sind nur Pseudothreads, die nicht parallel laufen sondern nacheinander abgearbeitet werden.
Aktuell habe ich auch nicht die Notwendigkeit parallele Tasks laufen zu lassen, da ich keine langlaufenden Routinen habe. Arbeitsintensive Routinen, wie Perimeterauswertung und zeitkritische wie Ultraschallsensoransteuerung lagere ich auf extra Boards aus. Der Hintergrund warum ich die Thread Library verwende, ist die Art, dass man das Programm anders strukturieren kann. Ich schreibe eine Threadklasse für ein Thema (z.B. rangeSensor auslesen), erzeuge das Objekt und schmeiße es in den Threadcontroller. Damit läuft das Teil und im gesamten Programm habe ich damit nichts mehr zu tun, außer die Werte auszulesen, wenn ich diese benötige. Damit ist das Teil für sich gekapselt und wenn ich den Sensor ändere, muss ich nur diese Klasse anpassen.
Meine main.cpp hat die Aufgabe, die Threads zu erstellen, dem Threadcontroller zu übergeben, das Blackboard zu erstellen und ein BehaviourTree Objekt. In der Loop funktion werden dann nur noch controller.run(); ausgeführt, der die Threads ausführt und myBehaviour.loop(); das den BehaviourTree ausführt.
Ich habe mal mein ganzes Roboter Programm rangehängt - was noch lange nicht fertig ist und auch noch nicht richtig aufgeräumt. Vielleicht findest du ja einige Anregungen. Zusätzliche habe ich noch den BehaviourTree als extra Programm rangehängt, damit man nicht ganz den Überblick verliert.

@StefanM
Ich befürchte, dass unsere Systeme zu divergent sind. Da ich anfänglich vom Ardumowercode profitiert habe, setze ich hier meine Kreationen rein, in der Hoffnung, dass auch andere davon profitieren können.
Attachment: https://forum.ardumower.de/data/med...936/NucleoMower_sw4stm32_nucleo_f411re10.zip/
 
Zuletzt bearbeitet von einem Moderator:
Hi Roland,


Roland schrieb:
Ich verwende die Arduino thread Library (etwas für mich abgewandelt). Das sind nur Pseudothreads, die nicht parallel laufen sondern nacheinander abgearbeitet werden.
ah verstehe. ;)

Aktuell habe ich auch nicht die Notwendigkeit parallele Tasks laufen zu lassen, da ich keine langlaufenden Routinen habe. Arbeitsintensive Routinen, wie Perimeterauswertung und zeitkritische wie Ultraschallsensoransteuerung lagere ich auf extra Boards aus.
Sehr gut, das habe ich auch vor! Ich denke es macht keinen Sinn die Sensoren ununterbrochen auf unnötige Daten abzufragen, das läßt sich viel schöner über einen Interrupt lösen der dem Hauptprogramm signalisiert dass interessante Daten anliegen.

Mein oberstes Ziel ist die Perimeterschleife los zu werden, oder besser gar nicht erst verlegen zu müssen. Mein Garten ist groß und sehr, z.B. durch die Hochbeete, Gewächshaus usw., verwinkelt. Wenn ich diese Stellen alle per Perimeter ausklammere kann ich auch per Hand mähen. Ich bin davon überzeugt dass eine realtime Kartografie mit den uns zur verfügung stehenden Sensoren und einer realtime Kamera Auswertung zum Ziel führen müsste. Hier finde ich Dein Konzept sehr interessant da sich so ein Tree ja auch völlig dynamisch zur Laufzeit verändern könnte.


Der Hintergrund warum ich die Thread Library verwende, ist die Art, dass man das Programm anders strukturieren kann. Ich schreibe eine Threadklasse für ein Thema (z.B. rangeSensor auslesen), erzeuge das Objekt und schmeiße es in den Threadcontroller. Damit läuft das Teil und im gesamten Programm habe ich damit nichts mehr zu tun
Sehr schön, genau so, bis auf das ich Systemprozesse (systemd Programme) einsetze, möchte ich das auch machen. So lässt sich alles sehr bequem zu und abschalten. Das ganze super bequem, sogar im laufenden Betrieb, updaten zu können ist ein weiterer Vorteil eines so verteilten Systems. B)

Danke! und VG
Rajiva
 
Zuletzt bearbeitet von einem Moderator:
Auf welcher Hardware Platform läuft dein System und was verwendest du für Boards für deine Sensoren ?

Beim Mega ist ja das große Problem, das es zu wenig Interrupts gibt und wenn dann noch die Odometrie und der Drehzahlsensor vom Mähmotor sich einen Interrupt teilen kann das nicht gut gehen, das umgehst du natürlich durch die Boards.
Da es ja gerade eine Diskussion über die 1,3 Platine gibt, sollte man natürlich auch mal sehen was anderen machen und vielleicht lernen.

Toll das du deinen Code zur Verfügung stellst.
 
Hallo Roland,

das klingt sehr gut - bestechend sozusagen! Hört sich für mich so an, als würden sich damit Code-Pflege und Optimierung besser gestalten lassen. Auch die Verteilung der vorhandenen Rechenleistung auf die der Situation entsprechend wirklich benötigten Prozesse scheint mir ein Problem zu sein, dass mit dieser Vorgehensweise transparenter und einfacher gestaltbar ist - oder liege ich da falsch?

Ich würde mir Deinen Code gern mal anschauen. Habe den Eindruck, sonst hat noch niemand danach gefragt(?), was mich etwas wundert. Könntest Du den Code zur Verfügung stellen?

Danke und frohes Schaffen.

Weihnachtliche Grüße ...

Peter
 
Hallo Peter,

den Code habe ich vor ca. einer Woche oben veröffentlicht.
Der Aufwand den BehaviourTree gegenüber einer FSM abzuarbeiten ist erstmal größer.
Zum einen wird der Tree immer vom Root aus abgearbeitet und durchlaufen.
Zum anderen werden die Running nodes auf den Stack geschrieben. Wenn nun bei dem nächsten Durchlauf ein anderer Zweig aktiviert wird,
werden alle Running nodes soweit notwendig reseted. Also vom running Status auf invalide gesetzt, damit bei einem späteren Aufruf diese wieder von beginn an laufen.
Die Struktur des Behavourtrees aus dem Code herauszulesen ist ohne die grafische Darstellung recht schwer.
Der Speicherverbrauch kann auch gößer sein, je nachdem wie die FSM realisiert wurde.

Der Vorteil liegt hier in der Strukturierung des Codes auf einer höheren abstrakten Ebene, der einfachen Wiederverwendbarkeit von Nodes und der klaren, übersichtlichen Darstellung und und vor allen Dingen der einfacheren Erweiterbarkeit gegenüber einer FSM wenn diese Komplex wird (meine persönliche Meinung). Jedes EndNode stellt eine Art Status der FSM dar und ist als eigene Klasse definiert. Wenn ich das Verhalten der Node ändern möchte, gehe ich nur in diese Klasse. Da diese relative klein vom Code schnipsel sind, sind diese auch leichter zu durchschauen.
Bezüglich der Strukturierung kann auch ein nicht Programmierer seine Ideen veranschaulichen. Es benötigt jedoch jemanden mit Programmier-Kenntnissen, diese dann umzusetzen.

Wenn du Fragen bezüglich dem Code hast einfach stellen. Kann anfänglich etwas verwirrend sein.
 
Moin Hermann - so sagt man bei uns, wenn einer - in diesem Falle: ich - was verpennt hat :)

Vielen Dank Roland - das der Code zugänglich ist, hatte ich übersehen.
Dann gucke ich mal, was mir das sagt ...

Gruss
Peter
 
In Bezug auf Jürgens Video bezüglich des Bumperduino Schlauches hatte ich gefragt, ob der Ardumowercode auf einen Rundumbumper angepasst wurde.
Die Verwendung eine Rundumbumpers mit der Form des frontangetriebenen Ardumowers ist doch relative aufwändig relativ zur Verwendung nur eines Frontbumpers.

Folgende Verhalten habe ich zum jetzigen Zeitpunkt in unerschiedlichsten Variationen beobachten können:
* mähen - standard verhalten
* robbi fährt über perimeter schleife rüber
* bumper in perimeter schleife aktiviert
* bumper außerhalb perimeter schleife aktiviert
* bei befreiung bumper wird perimeter von innen nach außen überschritten
* bei befreiung bumper wird perimeter von außen nach innen überschritten
* robbi verläßt rückwärts über die Schleife aufgrund der bumperaktivierung - ggf. noch dritte spule hinten einbauen
* sich wiederholende sequenzen

Und wenn man meint, es kann nicht auftreten, wird es immer auftreten.
Anbei mal mein aktueller BehaviourTree. Der hat sich doch seit Beginn um einiges verändert. Er enthält nun auch anfahren des Perimeters und Linefollowing bei niedriger Batterieladung.
Attachment: https://forum.ardumower.de/data/media/kunena/attachments/2936/BehaviourTree_2016-12-29.zip/
 
Zuletzt bearbeitet von einem Moderator:
Ganz ehrlich Roland: was Du hier an KI zauberst geht über das was ich bisher gemacht habe :) Mein Problem ist immer dass ich eigentlich nie genug Zeit zum Testen habe und man für ein vernünftiges KI-Verhalten (welche praktisch überall funktioniert) meines Erachtens sehr viel testen muss (bzw. simulieren müsste). Bisher hat sich kein anderer an den Ardumower-Code herangetraut (wohl auch weil er schon relativ komplex ist und man ihn zunächst erstmal stärker modularisieren müsste). Die Ideen wie man es machen müsste sind ja bereits alle in Deinem Code vorhanden. Vielleicht lässt sich ja eine zukünftige Ardumower-Software-Version (abgestimmt auf die Ardumower Hardware) mit Deinen Ideen bauen.

Bis dahin muss sich jeder selber den Code so anpassen wie er ihn braucht :) (der größte Vorteil dieses Projektes: Open Source)
 
Wenn ich bedenke, dass ich mit dem Verhalten immer noch an der Oberfläche rumkratze :eek:hmy: . Da ist noch einiges zu tun. Aber ich glaube, dass der Ansatz mit dem BT gut gewählt ist, da dieser leichter zu erweitern und zu ändern ist als eine FSM.
Auf jeden Fall euch allen einen guten Rutsch ins neue Jahr. :woohoo:
 
Hi Roland,

Aber ich glaube, dass der Ansatz mit dem BT gut gewählt ist, da dieser leichter zu erweitern und zu ändern ist als eine FSM.

ich bin auch gerade dabei auf den BT einzusteigen und versuche Deinen Code zu verstehen um es etwas leichter zu haben. Im BT->Tick() betreibst Du einen relativ großen Aufwand um running Leichen zu finden. Ich meine dass Problem entsteht in der Node->tick() wo du mit bb.runningNodes.Push(this); Nodes hinzufügst obwohl die vielleicht schon laufen und wohl auch schon in der Liste sind. Oder verstehe ich da was falsch?

Ich hab die Node-Tick() mal so gelößt:


Code:
CTreeNode::eNodeStatus CTreeNode::Tick(IBlackboard& bb)
{
	if (!IsRunning())
	{
		bb.AddRunningNode(this);
		mNodeStartTick = bb.GetTimeTicks();
		OnInitialize(bb);
	}

	mNodeStatus = OnUpdate(bb);

	if (!IsRunning())
	{
		OnTerminate(mNodeStatus, bb);
		bb.RemoveRunningNode(this);
	}

	return mNodeStatus;
}


Grüße
Rajiva
 
Hallo Rajiva,

ich verwende zwei Stacks. In bb.runningNodes werden die nodes abgespeichert, vom root zum aktuell ausgeführten node. In lastRunningNodes - welcher sich nicht auf dem BB sondern in class BehaviourTree befindet - sind die Nodes des letzten Durchgangs abgespeichert. Am Ende eines tick durchlaufes vergleiche ich nun beide Stacks von unten nach oben und rufe die Abort Funktionen der Nodes in dem lastRunningNodes Stack auf wenn die Nodes ungleich sind und stoppe, wenn diese gleich sind.
Danach wird bb.runningNode nach lastRunningNodes kopiert und ein neuer tick wird gestartet.

Jede Node die nun aufgerufen wird, legt sich auf den Stack und holt sich wieder runter, wenn sie nicht mir running beendet wird. Somit habe ich dann, wenn die letzte Node running zurückgibt alle Nodes auf dem Stack, die zur running node führen und nun ggf. selber running sind. Wenn nun ein anderer Zweig aufgerufen wird, müssen alle diese Nodes zurückgesetzt werden, damit diese bei dem nächsten Aufruf wieder von vorne beginnen und auch onInitialize aufrufen.

Bei deiner Lösung wird die Node nur einmal auf den Stack gelegt, wenn sie nicht running ist. So könntest du den Stack von der root zur running node nicht erneut aufbauen, um ihn dann mit dem vorherigen stack zu vergleichen.

Möglicherweise gibt es einen anderen Weg einen Zweig zu beenden, wenn ein anderer aufgerufen wird - wäre gut, wenn dies vereinfacht werden könnte.
 
Ich möchte hier nochmal 2 Verhalten dokumentieren, die mit der aktuellen Umsetzung auftreten können:

a) Wenn die aktuelle Node in onInitialize etwas setzt z.B: enablePerTrackRamping();
und der tree dann nach root zurückkehrt und vor Aufruf der aktuellen Node
war eine andere Node im running status, so wird diese nun vom Stack beendet und onTerminate aufgerufen
NACHDEM die aktuelle Node ausgeführt wurde. Wenn nun in onTerminate z.B. enableDefaultRamping()
aufgerufen wird, so wird enablePerTrackRamping() der aktuellen Node wieder rückgängig gemacht.

b) Was ist, wenn eine Node in einem neuen Zweig ist die wiederverwendet wird aber auch auf dem lastRunningNodes Stack
im Status running ist?
Bei Aufruf der Node wird onInitialize nicht aufgerufen, da die node bereits in running ist.
D.h. onUpdate wird ausgeführt und bei der Rückkehr zum root befindet sich die Node immer noch
im lastRunningNodes Stack als running und wird nun zurückgesetzt!!! Beim nächsten Aufruf wird dann
onInitialize aufgerufen und die Node geht wieder in running.
 
Hi Roland,

Roland schrieb:
Bei deiner Lösung wird die Node nur einmal auf den Stack gelegt, wenn sie nicht running ist. So könntest du den Stack von der root zur running node nicht erneut aufbauen, um ihn dann mit dem vorherigen stack zu vergleichen.

ich denke das stimmt so nicht, denn JEDE Node, also auch CCompositeNode oder CDecoratorNode, ist von CTreeNode abgeleitet und speichert sich ebenfalls auf dem Stack ab.;)

Aber mir scheint ich hab das mit dem BHT noch nicht so richtig verstanden, denn gerade der Running State macht den BHT meines Erachtens so viel mächtiger als z.B. eine SM. Mir ist auch noch nicht so richtig klar wo bei Dir der Unterschied zwischen einer CSequenceNode und einer CMemSequenceNode ist wenn die runner beendet werden.

Ich hab das so verstanden:
Die CSequenceNode tickt solange eine node bis eine der nodes != BH_SUCCESS zurück liefert. Im nächsten Durchgang fängt die CSequenceNode ja wieder bei 0 an zu ticken. Ist in der CSequenceNode ein running Child würde dieser im laufe des Durchganges getickt werden und könnte einen Zustand != BH_RUNNING zurück liefern. Wäre der Zustand dann BH_SUCCESS würde fleißig weiter getickt werden. Liefert eine Node < der Running Node != BH_SUCCESS muss die später getickte Node abgebrochen werden weil die CSequenceNode ja auch abricht. Bei der CMemSequenceNode kann diese Usecase nicht auftreten da hier ja bei der running node angefangen wird. Auf diese Weise lässt sich das Behavior der running node bei jedem Durchlauf erneut prüfen.

Ein kleines Beispiel wäre z.B. Sequence:
"!Bumper" -> "Vorwärts Fahren"

Solange Bumper BH_SUCCESS liefert ist "Vorwärts Fahren" im Running state ganz unabhängig von behaviors in anderen trees. Wenn in einem anderen Tree z.B. "Anhalten" getriggert wird, wird im nächsten durchlauf auch "Vorwärts Fahren" mit BH_FAILURE zurück kommen.

Vielleicht bin ich mit dem Verständnis des BHT auf dem Holzweg, aber ich meinte genau dieses Verhalten macht den BHT so mächtig, denn es können rein theoretisch beliebig viele Abhängigkeiten modelliert werden ohne den eigentlichen Code anpassen zu müssen. Auch können beliebig viele runner in beliebig vielen trees laufen, oder sehe ich da so verkehrt?

Grüße
Rajiva
 
Zuletzt bearbeitet von einem Moderator:
JEDE Node, also auch CCompositeNode oder CDecoratorNode, ist von CTreeNode abgeleitet und speichert sich ebenfalls auf dem Stack ab.

Nicht, wenn sie im running state ist und running nodes ausgeschlossen werden:

Code:
if (!IsRunning())
	{
		bb.AddRunningNode(this);


Ich gehe davon aus, dass du mit "runner" eine Node meinst, die sich im Status running befindet.
"Auch können beliebig viele runner in beliebig vielen trees laufen, oder sehe ich da so verkehrt?"
Im Behaviour Tree ist immer nur ein running Zweig aktiv (Voraussetzung: eine Node gibt running zurück). Ein Zweig beginnt bei root und endet bei dem letzten Blatt, was ausgeführt wird.
Wenn dieses running zurückgibt, wird nach root zurückgegangen und bei dem zurück-hochgehen im Tree werden dann alle nodes auf dem Weg dorthin in den Status running gesetzt: m_eNodeStatus = onUpdate(bb);
Wenn das letzte Blatt Success oder Failure zurückgibt, wird entsprechend den Sequenzen bzw. Selectoren weiter im Baum verzweigt.

Angenommen, der ganz rechte/letzte Zweig eines Baumes wird ausgeführt und ist im Status running, da die letzte node running zurückgegeben hat. Bei einem neuem tick entscheidet sich der tree nun das der ganz linke Zweig des Baumes aktiv ist und die entsprechende node running zurückgibt. Somit ist in dem Behaviour tree nun der ganz linke und ganz rechte Zweig im status running. Die nodes des ganz linken Zweiges befinden sich nun im Stack bb.runningNode und die des ganz rechten Zweiges im Stack lastRunningNodes. Nun müssen alle Nodes des rechten Zweiges in den Status BH_ABORTED gesetzt werden, damit bei neuem Aufruf des rechten Zweiges alle nodes wieder von vorne beginnen anstatt den running Modus beizubehalten.

Mir ist auch noch nicht so richtig klar wo bei Dir der Unterschied zwischen einer CSequenceNode und einer CMemSequenceNode ist wenn die runner beendet werden.
Es gib keinen Unterschied. Mem bedeutet Memory. Der Unterschied zwischen CSequenceNode und CMemSequenceNode besteht darin, dass CSequenceNode immer mit der ersten node beginnt auch wenn eine node running zurückgemeldet hat
und CMemSequenceNode sofort die running node aufruft und die nodes davor überspringt. Dies mach CMemSequenceNode indem eine runningChild variable verwendet wird. Diese wird auf 0 gesetzt, wenn onInitialize aufgerufen wird. Solange eine node nun running zurückgibt, und somit CMemSequenceNode selber in running status ist, wird onInitialize nicht aufgerufen. CSequenceNode hat kein onInitialize.


Code:
// ============================================================================
//The sequence node calls its children sequentially until one of them returns FAILURE or RUNNING. If all children return the success state, the sequence also returns SUCCESS.
NodeStatus Sequence::onUpdate(Blackboard& bb)
{
    NodeStatus s;
    for (int i = 0; i < _size; i++) {
        // Object exists?
        if (_children[i]) {
            s = _children[i]->tick(bb);
            if (s != BH_SUCCESS) {
                // If one child fails, then enter operation run() fails.  Success only results if all children succeed.
                return s;
            }
        }
    }
    return BH_SUCCESS;  // All children failed so the entire run() operation fails.
}

// ============================================================================
// MemSequence is similar to Sequence node, but when a child returns a RUNNING state, its index is recorded and in the next tick the
// MemSequence call the child recorded directly, without calling previous children again.

MemSequence::MemSequence(): runningChild(0) {}

void MemSequence::onInitialize(Blackboard& bb)
{
    runningChild = 0;
    Node::onInitialize(bb);  // Print out that onInitialize was called. For debugging.
}

NodeStatus MemSequence::onUpdate(Blackboard& bb)
{
    NodeStatus s;
    for(int i = runningChild; i < _size; i++) {
        // Object exists?
        if(_children[i]) {
            s = _children[i]->tick(bb);
            if (s != BH_SUCCESS) { // If one child fails, then enter operation run() fails.  Success only results if all children succeed.
                if (s == BH_RUNNING) {
                    runningChild = i;
                }
                return s;
            }
        }
    }
    return BH_SUCCESS;  // All children failed so the entire run() operation fails.
}


Es gibt zwei Möglichkeiten eine running node zu beenden:
a) durch Aufruf von abort(Blackboard& bb) oder b) die Node gibt einen anderen Status als running zurück.

a)durch Aufruf von abort(Blackboard& bb)
CSequenceNode und CMemSequenceNode werden in den Status abort bei Aufruf von void Node::abort(Blackboard& bb) gesetzt. Damit beginnen bei erneutem Aufruf beide Sequenzen von vorne - was CSequenceNode sowieso immer macht. CMemSequenceNode beginnt von vorne, da diese nun im status abort ist und somit onInitialize aufgerufen wird.

b)die Node gibt einen anderen Status als running zurück.
Wird success zurückgegeben, so wird sofort die nächste node in der Sequenz aufgerufen. Wird failure zurückgegeben, beenden beide Sequenzen die Arbeit und fangen beim nächsten Aufruf wieder von vorne an.
 
Oben