Sztuczna inteligencja (AI) w Unreal Engine 4

Wstęp

Podstawowe Pojęcia

Tradycyjny Behaviour Tree, a Behaviour Tree w UE4

Behaviour Tree w UE4

Blackboard

Rodzaje nodów

Observer Aborts

Notify observer

AI w Blueprintach

AI w C++

Inne

Zakończenie

WSTĘP

Sztuczna inteligencja (AI) symulująca zachowania człowieka (możliwość podejmowania decyzji w zależności od zmieniającej się sytuacji) jest wielkim wyzwaniem stojącym przed programistami. I chociaż jeszcze wiele brakuje do ideału, to jednak uczyniono spore postępy w odtworzeniu procesu myślowego człowieka. Obecne silniki gier są w stanie symulować wiele zachowań ludzkich, a grający w grę ma wrażenie, że jego przeciwnicy “myślą”, bowiem obserwuje różne zachowania AI botów (NPC) zależnie od panującej sytuacji na scenie gry. Duże postępy w tej dziedzinie możemy obserwować w Unreal Engine 4, gdzie mamy rozbudowany zestaw narzędzi dla implementacji algorytmów symulacji zachowań ludzkich. Przede wszystkim należy zwrócić uwagę, że UE4 zawiera mechanizmy odzwierciedlające “części ciała” człowieka w celu lepszego pogrupowania logiki:

Character – ciało, gdzie implementujemy poruszanie się postaci

AI Controller – głowa, gdzie jest logika kontrolującą ciało

Behaviour Tree – mózg, gdzie następuje proces podejmowania decyzji (myślenia)

Blackboard – pamięć, gdzie przechowywane są pewne informacje

PODSTAWOWE POJĘCIA

UE4 dostarcza następujące narzędzia dla implementacji AI:

Behavior Tree – Podstawowy element AI w UE4. Umożliwia tworzenie różnych stanów i logiki dla AI. Na podstawie tam określonych warunków są podejmowane przez postać złożone decyzje.

Navigation Component – Obsługa poruszania się dla AI bota.

Blackboard – Przechowywanie lokalnych zmiennych dla AI.

Enumeration – Tworzenie listy stanów AI bota, między którymi możemy przełączać.

Target Point – Punkty docelowe, między którymi porusza się AI bot. Klasa Waypoints pochodzi z klasy Target Point, którą tworzy podstawową formę Path node.

AI Controller dla Character – Kontroler obsługuje komunikację między światem a kontrolowanym Pawnem (w tym wypadku postacią) dla AI bota.

Navigation Volumes – Tworzenie Navigation Mesh w środowisku dla działania Path Finding dla AI bota.

NavMesh Bounds volume – Określa obszar dla NavMesh.

Nav Modifier volume – Dostarczony z klasą Nav Area. Ma wpływ na nawigacyjne atrybuty NavMesh Bounds volume, gdzie te dwa przecinają się.

Navigation Mesh (NavMesh) – Wyznaczenie obszarów, gdzie AI bot może poruszać się. Poligonalna reprezentacja poziomu, gdzie każdy poligon działa jak pojedynczy nod połączony z innymi najbliższymi nodami. Generowana jest automatycznie przez silnik i jest możliwie zoptymizowana.

Path Finding – Znajdowanie najkrótszej drogi między dwoma punktami.

Path Following (Path nodes) – Podobnie jak NavMesh, Path Nodes wyznaczają obszar, gdzie AI może się poruszać.

Steering behaviors – Unikanie przeszkód, czyli wpływanie na ruch postaci, w zależności od sytuacji.

Sensory systems – Krytyczne dane jak najbliższe postacie, dźwięki, kryjówki i inne informacje, które mogą wpływać na ruch AI bota. Tworzenie realistycznego ruchu i zachowania w świecie. Zwykle składa się z kilku modułów takich jak wzrok, słuch i pamięć, aby zbierać informacje o środowisku.

Randomness and probability – podejmowanie racjonalnych decyzji przez AI bota.

Finite State Machines (FSM) – model zdefiniowania reguł przechodzenia między stanami. Behaviour Tree w UE4 ma zimplementowany hierarchiczny FSM.

Observer Aborts Sprawdzanie warunków w dekoratorach, które obserwują swoje wartości i przerywają, jeśli trzeba, kiedy nastąpi zmiana stanu.

Machine learning – Zdolność AI bota uczenia się z sytuacji.

Tracing – Wykrywanie obiektów przez ray tracing przez AI bota. Pojedynczy promień kolidujący z aktorem zwraca nam informacje o tym akotrze.

A* Algorithm – W Path Finding oblicza najkrótszą sterowną ścieżkę między dwoma punktami położonymi w NavMesh. Zawiera listę punktów do przejścia i obliczany jest koszt przejścia z jednego punktu do drugiego na podstawie heurystycznych wartości. Przejście następuje przy jak najmniejszym koszcie.

TRADYCYJNY BHEAVIOUR TREE A BEHAVIOUR TREE W UE4

W dzisiejszych czasach BT zastępują Finite State Machine (FSM), które były oparte na statycznym procesie, gdyż stany i zasady zmiany między stanami były ustalone statycznie. Nie uwzględniało to jednak dynamiki zachodzącej w świecie. BT w porównaniu z FSM dostarcza wiekszej elastyczności i możliwości przejścia między stanami, dlatego jest powszechną techniką w tworzeniu AI w grach. Każdy silnik gry ma własną implementację BT, gdyż nie istnieje jeden obowiązujący model. Zwykle BT jest strukturą hierarchiczną, którą możemy przedstawić jako:

Root —>Nody Złożone (kompozytowe) —>Nody Złożone/Zadania

Tradycyjnie BT jest wykonywane od korzenia w dół do tzw. Nodów-liści, czyli węzłów końcowych (konkretnych zadań do wykonania). Dzieje się to za każdym razem, kiedy drzewo jest wykonywane, czyli teoretycznie co ramkę, a w praktyce prawdopodobnie rzadziej. Natomiast BT w UE4 jest sterowany zdarzeniami, dzięki czemu unikamy ciągłej i ciężkiej pracy w każdej ramce. Zamiast nieustannego sprawdzania, czy nastąpiła jakakolwiek zmiana, BT w UE4 pasywnie nasłuchuje zdarzeń, które mogą wywołać zmiany w drzewie. Ważne to jest dla wydajności i debugowania, gdyż lepiej kontrolujemy przepływ wykonania i dzięki temu wiemy, gdzie powstają błędy lub nieprawidłowe zachowania. Możemy przerwać wykonanie drzewa w jakimkolwiek momencie, cofnąć się czy pójść do przodu, sprawdzić wartość w tablicy (Blackboard).

W zwykłych BTs warunki są po prostu samymi zadaniami (tasks), czyli nodami liści, które albo dają sukces albo porażkę. W UE4 warunki są w dekoratorach, co daje nam pewne korzyści jak lepszą przejrzystość drzewa (dekoratory są w początkach gałęzi, a same liście zadaniami). Dekoratory są także obserwatorami (observers) w krytycznych nodach drzewa. Obserwują, czyli czekają na zdarzenia.

Normalne drzewa często używają nodów współbieżnych złożonych (parallel composite) dla obsługi jednoczesnych zachowań. Wykonują jednocześnie na wszystkich swoich nodach potomnych. Specjalne zasady ustalają, jak mają działać, jeśli jedno lub więcej tych nodów potomnych zakończy swoje zadanie (zależnie od żądanego zachowania).

Nody współbieżne niekoniecznie są wielowątkowe (czyli rzeczywiste wykonywanie zadań w tym samym czasie). W praktyce często zadania są wykonywane w jednym wątku. Nody współbieżne utrudniają optymizację wydajności, zwłaszcza w tworzeniu drzew sterowanych zdarzeniami, dlatego używamy ich jeśli naprawdę tego potrzebujemy. UE4 używa tutaj nodów prostych współbieżnych i specjalnego typu nodów zwanych serwisami. Nie są to więc zwykłe nody, znane z tradycyjnych drzew. Nody proste współbieżne pozwalają tylko na podłączenie dwóch nodów potomnych: jeden to pojedyncze zadanie (z opcjonalnymi dekoratorami), a drugi to może być cała gałąź. Nod prosty współbieżny możemy interpretować jako “kiedy robimy A, zrób także B”. Realnym przykładem użycia może być sytuacja, kiedy idąc w kierunku wroga, równocześnie jego atakujemy. Zwykle A jest podstawowym zadaniem, a B jest zadaniem dodatkowym, np. czekającym na wykonanie zadania A.

BEHAVIOUR TREE W UE4

BT w UE4 rozwiązuje pewne problemy w AI rozdzielając nasz kod na dane (zawarte w tablicy BB) i logikę zawartą w BT. Wykonanie logiki jest więc sterowane danymi. Bez tego rozdziału, mielibyśmy spory nieporządek przy większych projektach.

Tradycyjne drzewo zawsze wykonuje od korzenia, co jest zbyt kosztowne, dlatego w UE4, są pewne różnice. Po pierwsze wykonanie zatrzymuje się w jednym z nodzie złożonym (na daną chwilę aktywnym, bedącym “odpowiednim” w danym czasie) i jego nodach potomnych do czasu, kiedy cała sekwencja nodów dojdzie do końca wykonania lub nod złożony otrzyma informację o wcześniejszym zakończeniu wykonania. Jednak w takim rozwiązaniu musimy mieć inne nody do kontroli przerwania wykonania, czyli dekoratory i serwisy. Tutaj serwis w selektorze decyduje którą gałąź (sekwencję) ruchomić na podstawie posiadanych danych.

W skrócie system AI w UE4 to BT w świecie gry, uczące się z tego świata i używające pozyskanych informacji dla modyfikacji zachowań. Warto przy okazji zauważyć, że serwisy otrzymują informacje z BB i je zmieniają zapisując je tam ponownie, gdy dekorator tylko otrzymuje gotowe informacje z BB, na podstawie czego określana jest wartość w warunku. Kiedy wykonanie w drzewie zostanie zrealizowane, wykonanie kończy się wypełnieniem zadania w świecie. Dzięki temu AI bot wchodzi w interakcje ze światem i trwa to do czasu, kiedy AI bot jest usunięty ze świata. Dekoratory i serwisy są ze sobą połączone w jeden system.

BT w UE4 zawiera (na podstawie tablicy) trzy podstawowe elementy symulujące funkcje ludzkie:

Serwisy (services) – Zmysły, czyli odbieranie bodźców zewnętrznych

Dekoratory (decorators) – Podejmowanie decyzji w obliczu zmieniającej się sytuacji

Zadania (tasks) – Konkretne działania

Dekorator prosi BB o informacje, aby wiedzieć, czy wartość jest true czy false, jak to ma miejsce w instrukcji if w programowaniu. Dlatego zwany jest także warunkiem. Serwis zaś obserwuje świat i zmienia wartości w BB zależnie od tego, co zachodzi w świecie. Dlatego zwany jest także pętlą. Przykładowo serwis sprawdza, czy AI bot ma stan cooldown podczas strzelania z broni i zapisuje wynik w BB. Następnie dekorator odpytuje BB, czy AI bot jest w stanie cooldown. I postępuje dalej zależnie od zwróconej wartości, kończąc na wykonaniu któregoś z zadań.

BT w UE4 zawiera cztery główne typy nodów:

Nody złożone zwane też kompozytowymi (composites, czyli selectors, sequences and simple parallels) – początki gałęzi, czyli części drzewa, które mogą funkcjonować niezależnie.

Zadania (tasks) – konkretne zadania do wykonania.

Dekoratory (decorators) – w composite nodes sprawdzają, czy nody potomne mogą być wykonywane lub czy nie przerwać wykonanie całej gałęzi.

Serwisy (services) – funkcje, które są uruchamiane co jakiś czas w composite nodes, sprawdzające cały czas stan i uaktualniające dane.

Wszystkie typy nodów zwracają wartość: sukces, porażka lub działanie. Mają także swoją kolejność wykonania czyli priorytet, gdzie nod położony po lewej ma wyższy priorytet od noda położonego po prawej. Kolejność (priorytet) jest widoczny na podstawie małych numerków po prawej strony nodów poczynając od 0.

BLACKBOARD

Tablica (Blackboard, odpowiada pamięci w programowaniu) – dane są tu zapisywane i odczytywane zależnie od potrzeby podobnie jak ma to miejsce ze zmiennymi w programowaniu. Służy więc do centralizowania danych i przechowywania obliczeń. Jest to dzielona przestrzeń pamięci. Dane mogą być zapisywane dla pojedynczej postaci AI jak i dzielone między wieloma postaciami AI. Tablica może dziedziczyć z innej tablicy jak ze zwykłej klasy. TB ma dostęp do danych trzymanych BB w czasie wykonania. BB może przechowywać specyficzne dane dla każdej postaci. Kiedy zmienne w BB są instanced i synced, mamy zmienne globalne dla danego AI bota. Inaczej mówiąc, każda instancja postaci ma swoje własne (indywidualne) wartości w tablicy. Można ustawić wartość tablicy dla synchronizacji między wszystkimi instancjami tej postaci przez zaznaczenie instance synced w panelu Details. Jeśli mamy kilka postaci AI dzielących wartości zmiennej w tablicy, decydujemy, czy wartości dla każdej z nich będą oddzielne np. jeden ma zdrowie 50 drugi 100, czy wartości te ma być jednakowe dla wszystkich postaci, czyli np. wszystkie postacie AI mają zdrowie 75. Jeśli więc nie zaznaczymy tej opcji, postacie będą zachowywały swoje własne zdrowie, a jak zaznaczymy – będą miały takie samo. Funkcja ta więc replikuje zmienną do każdej instancji klasy BB na świecie.

RODZAJE NODÓW

Są dwa główne typy nodów: nody złożone (kompozytowe) i nody końcowe (liście). Zasadniczą różnicą między nimi jest to, że nody kompozytowe mają nody potomne, a nody końcowe nie. Trzecim typem nodów są nody sterujące dla warunków sterujących wykonaniem w drzewie. Wszystkie te nody zwracają wartość do swoich nodów nadrzędnych, jeśli wykonają zadanie lub nie. Możemy zatem wyróżnić trzy stany nodów:

– sukces (Success)

– porażka (Failure)

– działanie (Running)

Pierwsze dwa informują noda rodzicielskiego, że operacja zakończyła się sukcesem lub porażką. Trzeci stan nie jest jeszcze zdefiniowany, ponieważ operacja ciągle trwa.

Nody złożone (Composite Nodes) – root gałęzi w drzewie i podstawowy blok budujący drzewo, mogący działać niezależnie. Nody złożone nadają porządek wykonania nodów, decydują jaką gałąź wykonać w danym momencie, czyli mają wplyw na przepływ kontroli w drzewie. Mogą mieć dekoratory i/lub serwisy. Nodami potomnymi mogą być zadania lub inne nody złożone, czyli podrooty dla podgałęzi. W ten sposób możemy zbudować wielopoziomowe drzewo.

Selektor (Selector, logiczny OR) – Ma tylko jeden input i tylko jeden output. Wykonuje od lewej do prawej tylko jeden ze swoich nodów potomnych (nawet jeśli pozostałe nody również zadziałałyby) na podstawie posiadanych warunków (dekoratora i/lub działającej usługi). Jeśli któryś z tych nodów zadziała, selektor kończy działanie całej gałęzi z sukcesem i nie wykonuje innych swoich nodów potomnych. Jeśli pierwszy od lewej nod potomny nie zadziała, wykonanie idzie do drugiego z kolei, jeśli ten nie zadziała, idzie do trzeciego z kolei itd. Jeśli wszystkie nody potomne nie zadziałają, cała gałąź selektora także nie zadziała i zwróci porażkę. Wtedy wykonanie idzie do noda wyżej, który także może być selektorem i przekaże wykonanie do innej swojej gałęzi (także może być selektor i jego nody potomne). Innymi słowy, selektor wybiera (select) właściwe w danym momencie zadanie w nodzie potomnym z kilku zadań do wykonania na podstawie posiadnych warunków, a jeśli wszystko zawiedzie, wykonanie opuszcza gałąź drzewa i idzie do innej (jeśli takowa jest) za pośrednictwem noda nadrzędnego. Selektor jest odpowiedni dla stanu, którego celem nie jest sprawdzenie, czy nody są wykonane z sukcesem.

Sukces – przynajmniej jeden nod potomny zwraca Sukces

Porażka – wszystkie nody potomne zwracają Porażkę, ani jeden nie zwraca Sukces

Sekwencja (Sequence, logiczny AND) – Ma tylko jeden input i tylko jeden output. Wykonuje od lewej do prawej nody potomne jeden za drugim w kolejności w oparciu o warunek dekoratora i/lub działającą usługę i jeśli nod potomny zadziała, zamiast kończenia działania całej gałęzi, idzie do następnego, jeśli drugi zadziała, idzie do trzeciego itd. Kończy działanie całej gałęzi z sukcesem, jeśli wszystkie nody zostaną wykonane z sukcesem. Jeśli zaś napotka nod, który nie zadziała, sekwencja zwraca porażkę i przerywa wykonanie pozostałych nodów. Innymi słowy, sekwencja wykonuje sekwencyjnie zadania w swoich nodach potomnych do momentu napotkania zadania, którego nie może wykonać. Aby sekwencja zwróciła sukces, wszystkie jej nody potomne muszą zwrócić sukces, dzięki czemu sekwencja może być wykonana pomyślnie.

Sukces – wszystkie nody potomne zwracają Sukces

Porażka – przynajmniej jeden nod potomny zwraca Porażkę

Prosty równobieżny (Simple parallel) – Wykonuje więcej niż jeden nod potomny w jednym wykonaniu (jednocześnie, równobieżnie). W UE4 prosty równobieżny może mieć tylko dwa nody potomne: nod pojedynczy np. odtwarzanie dźwięku (zadanie) i grupę nodów selektora, zawierającą z kolei dwa nody (zadania): MoveTo i Wait. Zarówno dźwięk jak i selektor (zawierający MoveTo i Wait) będą wykonywane jednocześnie. Ma zatem jeden input i dwa outputs (jeden tylko dla zdań, a drugi taki jak outputs w innych composite nodes: nie tylko dla zadań). Jako jedyny nod złożony ma parametry dla trybu zakończenia wykonania całej gałęzi (Finish Mode: immediate, delayed). Domyślnie jest immediate. Prosty równobieżny służy zatem do tworzenia stanu, wymagającego innego zadania, które zawsze będzie wywoływane z pierwszym stanem aż ten pierwszy stan zwróci sukces. Np. Poruszamy się i równocześnie pozostałe zadanie może podejmować decyzję na szczycie tego pierwszego, np. strzelanie z broni, przeładowanie, zmiana broni itd. Drugie zadanie jest jak gdyby zadaniem uzupełniającym dla pierwszego.

Tryb zakończenia (Finish Mode) w nodzie prostym równobieżnym:

Immediate – Działanie natychmiastowe. Kiedy główne zadanie w pierwszym outpucie kończy, natychmiast następuje przerwanie innych nodów w drugim outpucie (zwanych background tree). Dzięki temu mamy wymuszenie restartu noda prostego współbieżnego za każdym razem po pomyślnym wykonaniu pierwszego noda potomnego. Przykładowo pierwszy nod to zadanie Wait nastawione na 2 sekundy, a drugi to Selektor z jednym zadaniem MoveTo. Po dwóch sekundach Wait kończy z sukcesem i od razu wraca wyżej do drzewa (do noda nadrzędnego), kończąc wszystko, co jest po prawej stronie (w tym wypadku jest to Selektor z zadaniem MoveTo). Wszystko odbywa się tylko w oparciu o warunek zadania Wait. I potem znowu może być wykonany Wait i po dwóch sekundach wszystko kończy itd. Innymi słowy, po wykonaniu pierwszego zadania, mamy powrót do noda nadrzędnego z przerwaniem wykonania innych nodów, nawet jeśli ich zadania mogły jeszcze trwać.

Delayed – Działanie z opóźnieniem. Kiedy główne zadanie w pierwszym outpucie po zakończeniu działania czeka na zakończenie innych nodów w drugim outpucie (zwanych background tree). Nie występuje wymuszenie restartu. Kiedy kończy Wait po dwóch sekundach, zadanie to nie wraca do noda nadrzędnego, ale czeka aż nody po prawej zostaną wykonane i dopiero wtedy cała gałąź jest uznawana za zakończoną z sukcesem i następuje powrót do wyższych nodów.

Nody sterujące – nody, których celem jest nie tyle budowa struktury drzewa, co wykonanie określonych funkcji w drzewie. Sterują wykonaniem w drzewie.

Dekoratory (Decorators, odpowiadają Conditions w programowaniu) – Warunki true/false (odpowiedniki boolean/bool w programowaniu), które konstruuje się w oddzielnych blueprintach dla podjęcia decyzji. Dekorator to warunek wykonania nodów, czyli sprawdzenie czy powinniśmy lub nie powinniśmy coś zrobić w danym momencie. Przykładowo selektor z zawartym w sobie dekoratorem ma sprawdzenie warunku true lub false. Jeśli wartość dekoratora będzie true, to wykonane zostaną nody potomne naszego selektora, w innym wypadku (false) nie będą wykonane, co jest logiczne. Może być kilka dekoratorów na jednym nodzie i ich wykonanie idzie wtedy z góry na dół. Jeśli któryś dekorator zawiedzie i zwróci porażkę, inne dekoratory poniżej nie będą już sprawdzane dla warunku. Wartym uwagi jest to, że dekorator jest tylko uruchamiany, kiedy otrzymuje wykonanie i nie działa z każdym tickiem. Aby zadziałał, musi być tzw. dekoratorem odpowiednim w danym momencie. Kiedy przepływ wykonania naszego drzewa przechodzi przez dany dekorator, ten dekorator staje się odpowiednim. To znaczy, że ten dekorator jest obliczany od czasu do czasu. Jeśli wynik dekoratora zmieni się w tym czasie, przerywa to, co w danej chwili dzieje się w swojej gałęzi i wykonanie wraca do tego noda, gdzie jest ten dekorator. Jednak kiedy ustawimy dla dekoratora observer aborts, wtedy dekorator będzie sprawdzany z każdym tickiem. Czystą klasą bazową dla dekoratora jest BlueprintBase. Wszystko inne jest dekoratorami, które utworzyliśmy i możemy ich także użyć jako klasę rodzicielską dla naszych dekoratorów. Najpopularniejszym typem dekoratora jest Dekorator Tablicy (Blackboard decorator), czyli dekorator działający w oparciu o dane z tablicy. Jest wbudowanym dekoratorem, dostępnym domyślnie, który ma warunek value != null dla szybkiego sprawdzenia czy lub nie poszczególny klucz w BB dla AI jest ustawiony (true = set) czy nie (false = not set). Przykładem działania dekoratora w drzewie może być następująca sytuacja. Uciekinier X znalazł dobrą kryjówkę, więc jego dekorator w drzewie, który sprawdza czy znaleziono kryjówkę otrzymuje wartość true i przepływ drzewa idzie niżej po gałęzi do zadania Biegnij do kryjówki. Zadanie jest wykonywane i jest w trakcie trwania (zwraca stan: Działanie), jak uciekinier biegnie do kryjówki. Kiedy uciekinier jest ciągle poza kryjówką (a minie określony czas), dekorator sprawdzający czy znaleziono kryjówkę nie jest już true, gdyż ma wartość false, więc przerywa zadanie Biegnij do kryjówki i wykonania wraca do noda, który ma ten dekorator. Innymi słowy, tutaj dekorator postaci w drzewie decyduje o tym, czy zadanie Biegnij ma być wykonane czy nie na postawie wartości true/false.

Serwisy (Services, odpowiadają Loops w programowaniu) – Zbierają lub uaktualniają informacje dla wprowadzenia natychmiastowych zmian w grze. Działają na zasadzie usługi, a więc ciągle. Serwisy modyfikują stan AI, gdyż są zawsze wywoływane i dlatego stan jest ciągle utrzymywany. Są specjalnymi nodami związanymi z jakimkolwiek nodem złożonym (Selektor, Sekwencja lub Prosty Współbieżny), które mogą rejestrować dla wywołań zwrotnych każde X sekund i wykonywać uaktualnienia, które powinny zachodzić co jakiś czas. Serwisy nie działają cały czas, a są tylko aktywne tak długo jak wykonanie pozostaje w gałęzi z danym nodem złożonym jako root i z serwisem. Wtedy serwis działa z każdym tickiem. Serwis także jest sprawdzany z każdym tickiem podczas wykonania zadania położonego niżej w drzewie w każdym okresie czasu w oparciu o wartość interwału ustawionego w panelu Details. Jeśli ustawimy interwał (Interval) na 5, serwis będzie uruchamiany co 5 sekund, nawet jeśli podrzędny nod MoveTo jest ciągle wykonywany pod spodem.Tick Interval zaś pozwala nam kontrolować jak często serwis jest wykonywany w tle.

Nody końcowe – stanowią liście drzewa, a więc nie mają swoich nodów potomnych i wykonanie w drzewie na nich się kończy. Stanowią konkretne zdanaie do wykonania w drzewie.

Zadania – Wykonują operacje wpływające bezpośrednio na AI i jak nody złożone zwracają sukces lub porażkę, jeśli trzeba. W blueprincie zadania istnieją dwa nody: Event Receive Execute otrzymuje sygnał do wykonania przyłączonych skyptów i Finish Execute – wysyła sygnał z powrotem i zwraca true lub false w przypadku sukcesu lub porażki. Nody liście nie mają nodów potomnych, ale są dwa ich typy:

Warunek (Condition) – Warunki służą do testowania stanu zmiennych (dowiadujemy się stanu zmiennej)

Działanie (Action) – Działania do wykonywania akcji.

OBSERVER ABORTS

Obserwatorzy przerwania (Observer Aborts) – dotyczą zmiany warunku dekoratora, przerywającej wykonanie zadania. Możemy na przykład przerwać zadanie Move To, jeśli stan AI nagle zmieni się. Obserwatorzy przerwania none/self/both/lower priority ustalają, co stanie się, kiedy drzewo wykryje zmianę w testowanym warunku. Jest to sterowanie przepływem wykonania. Observer aborts są dla stałego sprawdzania stanu dekoratora i jeśli się zdarzy coś, co zmienia stan, zostaje zauważony przez naszego observera i następuje:

none – Nie przerywaj niczego. Nic się nie zdarzy, kiedy warunek noda zwróci porażkę, nic nie zostanie przerwane.

self – Przerwanie siebie i podległych podnodów. Kiedy warunek noda zwróci porażkę, dany nod i wszystko pod nim będzie przerwane.

lower priority – Przerwanie jakichkolwiek nodów na prawo od tego nodu. Kiedy warunek noda zwróci porażkę, wszystko po prawej (czyli z lower priority) będzie przerwane.

both – Przerwanie siebie i podległych podnodów oraz jakichkolwiek nodów na prawo od tego nodu. Kiedy warunek noda zwróci porażkę, dany nod, wszystko pod nim i wszystko po prawej z lower priority będzie przerwane.

Zależnie od tego, co wybierzemy w Observer Aborts, będzie miało wpływ na to, co się zdarzy, kiedy np. śledzimy naszą postać i nasza postać nagle umrze. Śmierć naszej postaci jest ważnym zdarzeniem, które zmienia wiele w drzewie. Od nas zależy, co ma się stać, kiedy to nastąpi (jak drzewo ma zareagować na śmierć naszej postaci). W tym wypadku mamu do wyboru:

Czy powinniśmy ciągle robić to, co robiliśmy przed tym, jak nasz gracz zginął, jakby nic się nie stało (None).

Czy powinniśmy przestać robić to, co robiliśmy przedtem, jak nasz gracz zginął, ale jednocześnie próbować robić coś innego, co robiliśmy przedtem także (Self).

Czy powinniśmy przestać robić te inne rzeczy, jak nasz gracz zginął, ale próbować dalej śledzić naszego gracza, jakby była szansa, że ożyje (Lower Priority).

Czy powinniśmy przestać próbować śledzić naszego gracza wiedząc, że nie ożyje i przestać robić inne rzeczy, które robiliśmy jak nasz gracz zginął i zacząć wszystko od początku (Both).

NOTIFY OBSERVERS

Mamy klucz w Blackboard, który przechowuje instancję typu Actor. Obserwujący Dekorator Blackboard reaguje (przerywa bieżące zadanie), kiedy warunek się zmienia (w tym wypadku “is or is not NULL”) lub reaguje kiedy obserwowane wartości się zmienią, np. instancja Actor była ustawiona na ActorA i teraz przechowuje ActorB. Pierwszy przypadek to “On result change” a drugi to “On value change”. Pozwala zatem drzewu reagować na zmiany w testowanych warunkach lub testowanych wartościach (np. bHasFullAmmo lub targetEnemyIsSet) natychmiast, co jest skrajnie różne od synchronicznego przepływu kontroli z noda do noda przez drzewo, gdzie każdy warunek będzie obliczany tylko jeśli przepływ przechodzi z korzenia do podnoda i dalej.

Notify Observer

On Result Change Ponowna ewaluacja tylko jeśli warunek się zmienił

On Value Change Ponowna ewaluacja tylko jeśli obserwowany klucz w Blackboard się zmienił.

AI W BLUEPRINTACH

W AI Controler Blueprint używanego przez naszą postać AI ustawiamy nasz BT. Dzięki temu wiemy, jaki BT będzie sterował naszą postacią. W typowym ustawieniu AI w naszym blueprincie postaci NPC (sterowanej przez AI) z Self (czyli samego Blueprinta postaci) dostajemy nasz BB (czyli pamięć postaci), gdzie mamy zmienne typu BB zwane kluczami (keys). Tutaj wiążemy zmienną utworzoną w Blueprincie postaci z odpowiednim kluczem w Blackboard. I tak postępujemy z innymi zmiennymi. Dzięki temu zmienne obecne w tablicy będą powiazane z jej kluczami. I te klucze użyjemy w naszym BT. Inny Blueprint zawierający serwis monitoruje dane na bieżąco i w razie czego uruchamia daną gałąź. Użycie zdarzenia Tick i koniecznie nodu isValid. Utworzony Blueprint Service dodajemy do noda Selektor, gdzie sprawdzany jest cały czas warunek, czy odległość wynosi tyle, ile ustaliliśmy w naszym serwisie, aby uruchomić odpowiednie zadanie (task).

Pamiętamy, że do zadania, serwisu itd. musimy dodać za początkowym nodem z eventem casting na nasz AI Controler, gdyż stąd bierzemy Get Controlled Pawn. A potem jest dalszy kod, czyli nasza logika. Przykładowo do sekwencji dajemy instrukcje decyzyjne (warunki) przełączania między nodami potomnymi.

AI W C++

Warto przyjrzeć się podstawowym elementom budującym kod C++ działający z AI, aby wiedzieć, jak gdzie umieszcza się te elementy i w jaki sposób są powiązane między sobą. Zaprezentowany kod jest tylko pokazaniem poszczególnych elementów w C++, które mają wpływ na AI, nie jest kodem działającym poprawnie.

Na początek warto wiedzieć, aby pamiętać o podlinkowaniu naszego projektu do modułu AIModule w pliku NaszaNazwaProjektu.Build.cs:

 PublicDependencyModuleNames.AddRange(new string[]{ 
"Core",              
"CoreUObject",              
"Engine",              
"InputCore",             
"AIModule",          
});

Teraz możemy rpzejść do kodu naszego projektu. Najpierw w klasie naszej postaci AI otwieramy plik nagłówkowy i piszemy:

UPROPERTY(EditAnywhere,Category=”AI”)

class UBehaviorTree* BehaviorTree;

W Details pod kategorią AI ustawiamy nasz BT a pod kategorią Pawn nasz AI Contoller. AI Controller komunikujue sie z BB i z BT, a także kontroluje AI bot/boty, które są z nim powiązane. W pliku nagłówkowym AI Controller dodajemy:

/* Zmienna referencyjna do BT */

UBehaviorTreeComponent* OurBehaviorTree

/* Zmienna referencyjna do BB */

UBlackboardComponent* OurBlackboard;

// Definicje kluczy w tablicy

UPROPERTY(EditDefaultsOnly, Category = “AI”)

FName location;

UPROPERTY(EditDefaultsOnly, Category = “AI”)

bool isWeaponReady;

/* Nasza postać musi być spawnowana, zanim będziemy mogli ją posiadać i wykonać inne czynności */

virtual void Possess(APawn* Pawn) override;

W pliku źródłowym AI Controller inkludujemy:


#include “BehaviorTree/BlackboardComponent.h”
#include “BehaviorTree/BehaviorTreeComponent.h”
#include “BehaviorTree/BehaviorTree.h”

W konstruktorze:

AmyAIController::AmyAIController(){

//Inicjalizowanie zmiewnnej referencyjnej OurBehaviourTree, OurBlackboard i odpowiednich kluczy

OurBehaviorTree = CreateDefaultSubobject<UBehaviorTreeComponent>(TEXT(“BehaviorTree”));

OurBlackboard = CreateDefaultSubobject<UBlackboardComponent>(TEXT(“Blackboard”));

location = “PlaceToHide”;

isWeaponReady = false;

}

void AMyAIController::Possess(APawn* Pawn){

Super::Possess(Pawn);

//Dostanie posiadanej postaci i sprawdzenie, czy ta postać jest moją postacią AI Character

AAICharacter* AICharacter = Cast<AAICharacter>(Pawn);

if (AICharacter){

//Jeśli BB jest ważna, inicjalizujemy tablicę dla odpowiedniego BT

if (AIChar->BehaviorTree->BlackboardAsset){

OurBlackboard->InitializeBlackboard(*(AICharacter->BehaviorTree->BlackboardAsset));

}

//Startujemy BT odpowiadające naszej postaci

OurBehavior->StartTree(*AICharacter->BehaviorTree);

}

}

Mamy więc podstawową logikę w AI Controller. Teraz musimy dodać nasze klucze zdefiniowane w kodzie do BB. Otwieramy nasz BB i dodajemy nowe klucze typu Object i nazywamy go location oraz typu bool i nazywamy go isWeaponReady. Teraz możemy tworzyć zadania, czyli swoje klasy dziedziczące z BTTaskNode.

W pliku nagłówkowym utworzonej klasy naszego zadania nadpisujemy funkcję:

virtual OurTask::Type ExecuteTask(OurOwner) override;

Funkcja ta zawiera logikę dla naszego zadania. Za każdym razem, kiedy zadanie jest wykonywane, implementacja tej funkcji jest uruchamiana.

W pliku źródłowym inkludujemy naszą klasę postaci oraz:

#include “BehaviorTree/BlackboardComponent.h”

Kod przykładowego zadania powinien zawierać:

ExecuteTask(OurOwner){

//Dostajemy kontroler

AMyAIController* AIControler = Cast<AMyAIController>(OurOwner.GetAIOwner());

//Jeśli kontroler jest ważny…

if (AIControler){

//dostajemy BB z kontrolera

UBlackboardComponent* OurBlackboard = AIControler->GetBlackboardComp();

//Nasz kod, np.

//Uaktualnianie wartości w BB

OurBlackboard->SetValueAsObject(“PlaceToHide”, NextPlaceToHide);

//Tutaj zadanie musi być wykonane z sukcesem i zwrócić ten stan

return OurTask::Succeeded;

}

//W innym wypadku zadanie zwraca porażkę

return OurTask::Failed;

}

Aby utworzyć BTTask dziedziczymy z UBTTaskNode, jeśli chcemy utworzyć zwykłe zadanie lub z UBTTask_ BlackboardBase, jeśli chcemy mieć w naszym zadaniu funkcjonalności związane z BB. Następnie tworzymy zatem odpowiednie warunki i akcje do wykonania. Dzięki rozszerzaniu powyższych klas dane będą dostępne w Behavior Tree Editor po skompilowaniu. Teraz możemy utworzyć aset BT dla naszego kodu w projekcie i wstawić tam odpowiednie nody.

INNE ELEMENTY

Przedstawione poniżej rzeczy również bywają przydatne w budowaniu systemu AI w UE4. Opisuję je tu w skrócie, a dalszych informacj proszę szukać w internecie.

Enumeration – definiowanie opcji lub stanów w postaci struktury, do których potem możemy się odwołać w kodzie. Trzy stany dla AI bota:

Patrol

Search

Attack

Są to czytelniejsze inty, czyli Patrol ma 0, Search 1, a Attack 2. Możemy odwoływać się do pozycji na tej liście wyliczeniowej przez numery int. Stany (inty) możemy przełączać za pomocą noda Switch on.

Data Assets mają określony cel. Obecnie BlackBoard Data Assets są używane dla przechowywania informacji w celu użycia przez BTs. Umożliwia przechowywanie danych tablicy, do których dostęp mają też funkcje. Tam są klucze naszej tablicy. Tworzymy klucze, podajemy ich nazwę i podajemy ich tablicowy typ. To jakby zastępuje samą BB, bowiem nasz BT będzie miało w miejsce gdzie idzie BB tą Data Assets.

Potem w naszej tablicy, jak stworzyliśmy Data Assets dla naszej BB, wybieramy ją w oknie dialogowym. Jeśli mamy wiele enums w naszej BB, filtrujemy je tak:

movementType.AddNativeEnumFilter(this, TEXT("EMovementInfoType"));

movementContext.AddNativeEnumFilter(this,  TEXT("EMovementContext"));

Oba użyją ostatniego filtra, jakiego ustawimy.

ObjectLibrary jest obiektem zawierającym listę albo załadowanych obiektów albo FAssetData dla rozładowanych obiektów, które dziedziczą z dzielonej klasy bazowej. Mogą służyć dla zbierania assets w oparciu o ścieżkę.

ZAKOŃCZENIE

Sztuczna inteligencja w UE4 jest nieco zawiłym aspektem nawet dla zaawansowanych użytkowników. Dokumentacji jest jak na lekarstwo i wiele kwestii jest niejasnych. Często jedynie metoda prób i błędów daje nam jednoznaczne odpowiedzi na nasze pytania i wątpliwości. Wiele tutoriali traktuje ten temat fragmentarycznie i często wiedza tam zawarta jest już nieaktualna. Początkujący uzytkownik UE4 chcący użyć AI w swoim projekcie zderza się z tą niezbyt zachęcającą rzeczywistością. Mam nadzieję, że mój artykuł pomoże takim osobom zrozumieć tajniki AI w UE4. W razie zauważania jakichkolwiek błędów lub nieścisłości proszę o powiadomienie mnie, abym mógł to skorygować.

Leave a comment