Próbuję omijać drzewa zachowań, więc dodam kod testowy. Jedną z rzeczy, z którymi się zmagam, jest sposób na zapobieganie obecnie działającemu węzłowi, gdy pojawia się coś o wyższym priorytecie.
Rozważ następujące proste, fikcyjne drzewo zachowań dla żołnierza:
Załóżmy, że minęła pewna liczba tyknięć i nie było w pobliżu wroga, żołnierz stał na trawie, więc do wykonania wybrano węzeł Usiądź :
Teraz akcja Usiądź zajmuje trochę czasu, ponieważ do odtworzenia jest animacja, więc powraca Running
jako jej status. Przechodzi tyknięcie lub dwa, animacja nadal działa, ale wróg jest blisko? wyzwala węzeł warunku. Teraz musimy jak najszybciej zatrzymać węzeł Usiądź, abyśmy mogli wykonać węzeł Ataku . Idealnie byłoby, gdyby żołnierz nie skończył nawet siadania - zamiast tego mógłby odwrócić kierunek animacji, gdyby tylko zaczął siadać. Dla większego realizmu, jeśli przekroczy on punkt krytyczny w animacji, moglibyśmy zamiast tego pozwolić mu skończyć siadanie, a następnie stanąć ponownie, lub może sprawić, że potknie się w pośpiechu, aby zareagować na zagrożenie.
Próbowałem, jak mogłem, ale nie byłem w stanie znaleźć wskazówek, jak poradzić sobie z tego rodzaju sytuacją. Cała literatura i filmy, które zużyłem w ciągu ostatnich kilku dni (a było ich dużo), wydają się omijać ten problem. Najbliższą rzeczą, jaką udało mi się znaleźć, była koncepcja resetowania uruchomionych węzłów, ale nie daje to takim węzłom, jak Sit, szansy na powiedzenie „hej, jeszcze nie skończyłem!”
Pomyślałem o zdefiniowaniu metody Preempt()
lub Interrupt()
metody w mojej Node
klasie bazowej . Różne węzły poradzą sobie z tym, jak uznają za stosowne, ale w tym przypadku postaramy się przywrócić żołnierza na nogi JAK NAJSZYBCIEJ, a następnie wrócić Success
. Myślę, że takie podejście wymagałoby również, aby moja baza Node
miała pojęcie warunków oddzielnie od innych działań. W ten sposób silnik może sprawdzać tylko warunki i, jeśli przejdą, zablokować aktualnie wykonywany węzeł przed rozpoczęciem wykonywania akcji. Gdyby to zróżnicowanie nie zostało ustalone, silnik musiałby wykonywać węzły bez rozróżnienia, a zatem mógłby uruchomić nowe działanie przed zablokowaniem działającego.
Dla odniesienia poniżej są moje obecne klasy podstawowe. Ponownie, jest to skok, więc starałem się utrzymać rzeczy tak proste, jak to możliwe i dodawać złożoności tylko wtedy, gdy ich potrzebuję i kiedy to rozumiem, z czym obecnie mam problem.
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>
{
}
Czy ktoś ma wgląd, który mógłby mnie poprowadzić we właściwym kierunku? Czy moje myślenie jest prawidłowe, czy obawiam się tak naiwności?
źródło
Stop()
oddzwanianie przedOdpowiedzi:
Stwierdziłem, że zadaję to samo pytanie, co Ty i przeprowadziłem świetną krótką rozmowę w sekcji komentarzy na tej stronie blogu, gdzie otrzymałem inne rozwiązanie problemu.
Pierwszą rzeczą jest użycie współbieżnego węzła. Współbieżny węzeł to specjalny rodzaj węzła kompozytowego. Składa się z sekwencji kontroli warunków wstępnych, po których następuje pojedynczy węzeł działania. Aktualizuje wszystkie węzły potomne, nawet jeśli jego węzeł akcji jest w stanie „uruchomionym”. (W przeciwieństwie do węzła sekwencji, który musi rozpocząć aktualizację od bieżącego działającego węzła potomnego).
Główną ideą jest utworzenie dwóch kolejnych stanów zwrotnych dla węzłów akcji: „anulowanie” i „anulowanie”.
Niepowodzenie sprawdzania warunków wstępnych w węźle współbieżnym jest mechanizmem, który powoduje anulowanie działającego węzła akcji. Jeśli węzeł akcji nie wymaga długotrwałej logiki anulowania, natychmiast zwróci „anulowano”. W przeciwnym razie przechodzi w stan „anulowania”, w którym można umieścić całą logikę potrzebną do prawidłowego przerwania akcji.
źródło
Myślę, że twój żołnierz może zostać rozłożony na umysł i ciało (i cokolwiek innego). Następnie ciało może zostać rozłożone na nogi i ręce. Następnie każda część potrzebuje własnego drzewa zachowań, a także interfejsu publicznego - do żądań z części wyższego lub niższego poziomu.
Zamiast więc mikrozarządzania każdą akcją, po prostu wysyłasz natychmiastowe wiadomości, takie jak „ciało, usiądź na jakiś czas” lub „ciało, biegnij tam”, a ciało będzie zarządzać animacjami, zmianami stanu, opóźnieniami i innymi rzeczami ty.
Alternatywnie, ciało może samodzielnie zarządzać takimi zachowaniami. Jeśli nie ma zamówień, może zapytać: „Czy możemy tu usiąść?”. Co ciekawsze, z powodu enkapsulacji możesz łatwo modelować cechy takie jak zmęczenie lub ogłuszenie.
Możesz nawet zamieniać części - stwórz słonia z intelektem zombie, dodaj skrzydła człowiekowi (nawet tego nie zauważy) lub cokolwiek innego.
Założę się, że bez takiego rozkładu istnieje ryzyko, że prędzej czy później spotkasz się z eksplozją kombinatoryczną.
Również: http://www.valvesoftware.com/publications/2009/ai_systems_of_l4d_mike_booth.pdf
źródło
Leżąc wczoraj w łóżku, miałem coś w rodzaju objawienia, w jaki sposób mógłbym to zrobić bez wprowadzania złożoności, do której pochylałem się w swoim pytaniu. Polega ona na zastosowaniu „równoległego” kompozytu (źle nazwanego, IMHO). Oto co myślę:
Mam nadzieję, że nadal jest to dość czytelne. Ważne punkty to:
Myślę, że to zadziała (wkrótce wypróbuję to w moim kolcu), mimo że jestem trochę bardziej niechlujny, niż się spodziewałem. Dobrą rzeczą jest to, że w końcu byłbym w stanie obudować sub-drzewa jako logikę wielokrotnego użytku i odwoływać się do nich z wielu punktów. To rozwiąże większość moich obaw, więc myślę, że jest to realne rozwiązanie.
Oczywiście nadal chciałbym usłyszeć, czy ktoś ma jakieś przemyślenia na ten temat.
AKTUALIZACJA : chociaż to podejście technicznie działa, zdecydowałem, że to sux. Wynika to z faktu, że niepowiązane poddrzewa muszą „wiedzieć” o warunkach określonych w innych częściach drzewa, aby mogły wywołać własną śmierć. Chociaż dzielenie się odniesieniami do sub-drzewek przyczyniłoby się w pewnym stopniu do złagodzenia tego bólu, wciąż jest to sprzeczne z tym, czego można się spodziewać, patrząc na drzewo zachowań. Rzeczywiście popełniłem ten sam błąd dwa razy na bardzo prostym skoku.
Dlatego zamierzam pójść inną drogą: wyraźne wsparcie dla wyprzedzania w modelu obiektowym oraz specjalny kompozyt, który pozwala na wykonanie innego zestawu działań w przypadku wystąpienia uprzedzenia. Wyślę osobną odpowiedź, gdy coś będzie działać.
źródło
Preempt()
metodę, która przedostałaby się przez drzewo. Jednak jedyną rzeczą, którą tak naprawdę „obsłużyć” byłby kompozyt wyprzedzający, który natychmiast przełączałby się na swój węzeł potomny zapobiegający.Oto rozwiązanie, na które się zdecydowałem ...
Node
klasa podstawowa maInterrupt
metodę, która domyślnie nic nie robibool
(co oznacza, że są szybkie do wykonania i nigdy nie potrzebują więcej niż jednej aktualizacji)Node
udostępnia zbiór warunków osobno do kolekcji węzłów potomnychNode.Execute
wykonuje najpierw wszystkie warunki i od razu kończy się niepowodzeniem, jeśli jakikolwiek warunek się nie powiedzie. Jeśli warunki się powiodą (lub nie ma żadnych), wywołuje,ExecuteCore
aby podklasa mogła wykonać swoją rzeczywistą pracę. Jest parametr, który pozwala pominąć warunki, z powodów, które zobaczysz poniżejNode
umożliwia również wykonywanie warunków w izolacji za pomocąCheckConditions
metody. OczywiścieNode.Execute
tak naprawdę dzwoni tylkoCheckConditions
wtedy, gdy trzeba zweryfikować warunkiSelector
kompozyt wymaga terazCheckConditions
od każdego dziecka, które rozważa wykonanie. Jeśli warunki się nie spełnią, przechodzi on prosto do następnego dziecka. Jeśli zdadzą, to sprawdza, czy dziecko już wykonuje dziecko. Jeśli tak, wywołuje,Interrupt
a następnie kończy się niepowodzeniem. To wszystko, co może zrobić w tym momencie, w nadziei, że aktualnie działający węzeł odpowie na żądanie przerwania, co może zrobić przez ...Interruptible
węzeł, który jest swego rodzaju specjalnym dekoratorem, ponieważ ma regularny przepływ logiki jako jego udekorowane dziecko, a następnie oddzielny węzeł dla przerw. Wykonuje swoje zwykłe dziecko do ukończenia lub niepowodzenia, o ile nie zostanie przerwane. Jeśli zostanie przerwany, natychmiast przełącza się na wykonanie swojego podrzędnego węzła obsługującego przerwanie, który może być tak złożonym poddrzewem, jak to konieczneWynik końcowy jest mniej więcej taki, zaczerpnięty z mojego kolca:
Powyżej znajduje się drzewo zachowania pszczoły, która zbiera nektar i przywraca go do ula. Kiedy nie ma nektaru i nie znajduje się w pobliżu kwiatu, który ma, wędruje:
Gdyby ten węzeł nie był przerywany, nigdy by nie zawiódł, więc pszczoła błąkałaby się wiecznie. Ponieważ jednak węzeł nadrzędny jest selektorem i ma elementy potomne o wyższym priorytecie, ich uprawnienia do wykonania są stale sprawdzane. Jeśli warunki się spełnią, selektor wywołuje przerwanie, a pod-drzewo powyżej natychmiast przełącza się na ścieżkę „Przerwaną”, która po prostu ratuje jak najszybciej, gdy zawiedzie. Mógłby oczywiście wykonać najpierw inne czynności, ale mój kolec tak naprawdę nie ma nic innego do roboty jak zwolnienie za kaucją.
Aby jednak powiązać to z moim pytaniem, można sobie wyobrazić, że ścieżka „Przerwana” może próbować odwrócić animację siadania i, w przeciwnym razie, potknąć się o żołnierza. Wszystko to powstrzymałoby przejście do stanu o wyższym priorytecie i właśnie taki był cel.
I pomyśleć , że jestem zadowolony z tego podejścia - zwłaszcza podstawowych elementów Przedstawię powyżej - ale szczerze mówiąc, to podniesione dalsze pytania dotyczące rozprzestrzeniania wdrożeń konkretnych warunków i działań, i wiązanie drzewo zachowanie w systemie animacji. Nie jestem nawet pewien, czy potrafię sformułować te pytania, więc będę się zastanawiał.
źródło
Rozwiązałem ten sam problem, wymyślając dekorator „When”. Ma stan i dwoje zachowań dziecka („wtedy” i „inaczej”). Po wykonaniu „When” sprawdza stan i, w zależności od jego wyniku, uruchamia następnie / w przeciwnym razie potomek. Jeśli wynik warunku ulegnie zmianie, uruchomione dziecko jest resetowane i uruchamiane jest dziecko odpowiadające innej gałęzi. Jeśli dziecko zakończy wykonywanie, całe „Kiedy” kończy wykonywanie.
Kluczową kwestią jest to, że w przeciwieństwie do początkowego BT w tym pytaniu, w którym warunek jest sprawdzany tylko na początku sekwencji, moje „Kiedy” sprawdza stan podczas działania. Tak więc górna część drzewa zachowań jest zastąpiona przez:
W przypadku bardziej zaawansowanego użycia „Gdy” należy również wprowadzić akcję „Czekaj”, która po prostu nic nie robi przez określony czas lub na czas nieokreślony (do momentu zresetowania przez zachowanie rodzica). Ponadto, jeśli potrzebujesz tylko jednej gałęzi „When”, druga może zawierać akcje „Success” lub „Fail”.
źródło
Chociaż jestem spóźniony, ale mam nadzieję, że to może pomóc. Głównie dlatego, że chcę się upewnić, że osobiście czegoś nie umknęło, ponieważ starałem się to rozgryźć. Najczęściej zapożyczyłem ten pomysł
Unreal
, ale bez robienia go jakoDecorator
nieruchomości na bazieNode
lub silnie związanej zBlackboard
, jest bardziej ogólny.Spowoduje to wprowadzenie nowego typu węzła o nazwie,
Guard
który jest jak kombinacjaDecorator
aComposite
i macondition() -> Result
podpis obokupdate() -> Result
Posiada trzy tryby wskazujące sposób anulowania po
Guard
powrocieSuccess
lubFailed
faktyczne anulowanie zależy od osoby dzwoniącej. Więc doSelector
połączeniaGuard
:.self
-> Anuluj tylkoGuard
(i działające dziecko), jeśli jest uruchomione, a warunek byłFailed
.lower
-> Anuluj węzły o niższym priorytecie tylko wtedy, gdy są uruchomione, a warunek toSuccess
lubRunning
.both
-> Zarówno.self
i.lower
zależnie od warunków i uruchomienie węzłów. Chcesz anulować self, jeśli jest uruchomiony i warunekfalse
lub anulować działający węzeł, jeśli są one uważane za niższe priorytety na podstawieComposite
reguły (Selector
w naszym przypadku), jeśli warunek jest spełnionySuccess
. Innymi słowy, w zasadzie oba te pojęcia są połączone.Podobnie jak
Decorator
i inaczejComposite
to zajmuje tylko jedno dziecko.Chociaż
Guard
tylko zrób pojedyncze dziecko, można zagnieździć tyleSequences
,Selectors
lub innych typówNodes
, jak chcesz, łącznie z innymiGuards
lubDecorators
.Selector1 Guard.both[Sequence[EnemyNear?]] Sequence1 MoveToEnemy Attack Selector2 Sequence2 StandingOnGrass? Idle HumATune
W powyższym scenariuszu, za każdym razem
Selector1
, gdy nastąpi aktualizacja, zawsze będzie uruchamiał sprawdzanie stanu strażników powiązanych z jego dziećmi. W powyższym przypadkuSequence1
jest Strzeżony i musi zostać sprawdzony przedSelector1
kontynuowaniemrunning
zadań.Kiedykolwiek
Selector2
lubSequence1
działa, gdy tylkoEnemyNear?
powrócisuccess
podczasGuards
condition()
sprawdzaniaSelector1
, wyda przerwanie / anuluje,running
node
a następnie kontynuuje jak zwykle.Innymi słowy, możemy zareagować na gałąź „bezczynności” lub „ataku” w oparciu o kilka warunków, dzięki czemu zachowanie jest znacznie bardziej reaktywne, niż gdybyśmy zdecydowali się na
Parallel
Pozwala to również chronić pojedyncze,
Node
które mają wyższy priorytet przed uruchomieniemNodes
w tym samymComposite
Selector1 Guard.both[Sequence[EnemyNear?]] Sequence1 MoveToEnemy Attack Selector2 Guard.both[StandingOnGrass?] Idle HumATune
Jeśli
HumATune
to długo trwaNode
,Selector2
zawsze sprawdzi to najpierw, jeśli nie byłoGuard
. Jeśli więc NPC zostanie teleportowany na trawę, następnym razemSelector2
uruchomi się, sprawdziGuard
i anulujeHumATune
, aby uruchomićIdle
Jeśli robi się teleportował się z plastra trawy, będzie zrezygnować z systemem (węzeł
Idle
) i przenieść doHumATune
Jak widać tutaj, podejmowanie decyzji zależy od osoby dzwoniącej,
Guard
a nie odGuard
samej osoby. Zasady dotyczące tego, kto jest uważany zalower priority
pozostającego, pozostają po stronie dzwoniącego. W obu przykładach to,Selector
kto definiuje to, co stanowilower priority
.Jeśli miałeś
Composite
wywołanieRandom Selector
, możesz zdefiniować reguły w ramach implementacji tego konkretnegoComposite
.źródło