Agent ukierunkowany na celProgramownie A.I. Gier

Zachowanie agenta ukierunkowanego na cel

Do tej pory sprawdziliśmy agentów wykorzystujących architekturę opartą na skończonej maszynie stanów, w której zachowanie jest rozkładane na kilka stanów, z których każdy zawiera logikę umożliwiającą przejście do innych stanów. Ten rozdział wprowadza nieco inne podejście. Zamiast stanów zachowanie agenta jest definiowane jako zbiór hierarchicznych celów. Cele mają charakter atomowy lub złożony. Cele atomowe definiują pojedyncze zadanie, zachowanie lub akcję, takie jak dążenie do ustawienia lub przeładowania broni, podczas gdy cele złożone składają się z kilku podzadań, które z kolei mogą być atomowe lub złożone, definiując w ten sposób zagnieżdżoną hierarchię. Kompozyty zwykle opisują bardziej złożone zadania niż ich bracia atomowi, takie jak budowanie fabryki broni lub wycofywanie się i szukanie schronienia. Oba typy celów są w stanie monitorować swój status i mają zdolność do przeplanowania, jeśli się nie powiedzie. Ta hierarchiczna architektura zapewnia programiście AI intuicyjny mechanizm definiowania zachowania agentów, ponieważ dzieli wiele podobieństw z ludzkim procesem myślowym. Ludzie wybierają abstrakcyjne cele wysokiego poziomu w oparciu o ich potrzeby i pragnienia, a następnie rekurencyjnie rozkładają je na plan działania, który można zastosować. Na przykład w deszczowy dzień możesz zdecydować się odwiedzić kino. Jest to abstrakcyjny cel, którego nie można zrealizować, dopóki nie zostanie on podzielony na mniejsze cele cząstkowe, takie jak opuszczenie domu, podróż do kina i wejście do kina. Z kolei każdy z nich jest abstrakcyjny i należy go dalej podzielić. Proces ten jest zwykle przejrzysty, ale czasami zdajemy sobie z tego sprawę, gdy rozkład wymaga wyboru. Na przykład subgalowa podróż do kina może być zaspokojona na wiele sposobów - możesz tam podróżować samochodem, komunikacją miejską, rowerem lub pieszo - i możesz spędzić kilka chwil zastanawiając się nad wyborem. (Jest to szczególnie prawdziwe, jeśli współpracujesz z kilkoma innymi ludźmi, aby osiągnąć cel - przypomnij sobie ostatnią wizytę w wypożyczalni wideo / DVD ze znajomymi. Argh!) Ten proces trwa, dopóki cele nie zostaną rozłożone na podstawowe czynności motoryczne, które twoje ciało jest w stanie wykonać. Na przykład cel opuszczenia domu można podzielić na następujące cele: iść do szafy, otworzyć drzwi szafy, zdjąć płaszcz z wieszaka na płaszcz, założyć płaszcz, iść do kuchni, założyć buty, otworzyć drzwi, wyjść na zewnątrz i wkrótce. Co więcej, ludzie nie lubią marnować energii, więc generalnie nie marnujemy cennych kalorii na myślenie o celu, dopóki nie będzie to absolutnie konieczne. Na przykład nie zdecydowałbyś, jak otworzyć puszkę fasoli, dopóki nie będziesz mieć jej w dłoni, lub jak zawiązać sznurowadła, dopóki buty nie staną na nogach. Celowy agent naśladuje to zachowanie. Każda aktualizacja myśli agent sprawdza stan gry i wybiera zestaw predefiniowanych celów lub strategii wysokiego poziomu - ten, który według niego najprawdopodobniej pozwoli mu zaspokoić najsilniejsze pragnienie (zazwyczaj wygraną). Następnie agent będzie próbował podążać za tym celem, rozkładając go na dowolne podzadania, spełniając kolejno każdy z nich. Będzie to robić, dopóki cel nie zostanie spełniony lub nie uda mu się, lub dopóki stan gry nie będzie wymagał zmiany strategii.

Powrót Erica Chrobrego

Przyjrzyjmy się przykładowi, używając naszego ulubionego agenta gier, Erica, który niedawno znalazł zatrudnienie w RPG "Dragon Slayer 2." Programista AI Erica opracował dla niego kilka strategii, w tym Obroń przed smokiem, Smokiem ataku, Kup miecz, Zdobądź jedzenie i Upij się. Strategie te reprezentują abstrakcyjne cele wysokiego poziomu, które składają się z mniejszych celów cząstkowych, takich jak Utwórz ścieżkę, Śledź ścieżkę, Traverse Path Edge, Stab Dragon, Slice Dragon, Run Away i Hide. Dlatego, aby ukończyć strategię, Eric musi ją rozłożyć na odpowiednie podzadania i kolejno zaspokajać każdy z nich (w razie potrzeby dalej je rozkładając). Eric właśnie wszedł do świata gry, a ponieważ nie nosi broni i dlatego czuje się wrażliwy, jego największym pragnieniem jest znalezienie jakiegoś spiczastego kija, zanim smok go zauważy. Jego "mózg" (specjalny rodzaj celu zdolny do podejmowania decyzji) bierze pod uwagę wszystkie dostępne strategie i stwierdza, że Kup Miecz znakomicie pasuje do rachunku, więc jest to wyznaczone jako cel, do którego należy dążyć, dopóki nie zdecyduje inaczej



Eric nie może jednak działać zgodnie z tym celem, ponieważ na tym poziomie jest on zbyt abstrakcyjny i musi zostać rozłożony - lub rozszerzony, jeśli wolisz myśleć o tym w ten sposób - na składowe podzadania. Na potrzeby tego przykładu założymy, że Kup miecz składa się z podzadań pokazanych na rysunku



Aby zdobyć miecz, Eric musi najpierw znaleźć trochę złota, a następnie udać się do kuźni, gdzie kowal chętnie przyjmie je jako zapłatę. Agenci realizują cele kolejno, więc Go To Smithy nie będzie oceniane, dopóki nie będzueGetGold. Jest to jednak kolejny złożony cel, więc aby go zrealizować, Eric musi dalej poszerzać hierarchię. Zdobądź złoto składa się z podzadań Plan Path (Goldmine), Follow Path i Pick Up Nugget.



Cel Plan Path (Goldmine) jest spełniony, wysyłając zapytanie do Planowanie ścieżki, aby zaplanować ścieżkę do kopalni złota. Następnie jest usuwany z listy celów. Następną rzeczą, którą Eric rozważa, jest Podążanie ścieżką, którą można dalej rozłożyć na kilka atomowych celów Traverse Edge, z których każda zawiera logikę wymaganą do podążania za krawędzią ścieżki prowadzącej do kopalni złota.



Ten proces rozkładania i spełniania celów trwa, dopóki cała hierarchia nie zostanie przemierzona, a Eric pozostanie z lśniącym nowym mieczem w rękach. Cóż, to jest to; zobaczmy teraz, jak to zrobić.

Realizacja

Hierarchie obiektów zagnieżdżonych, takie jak te wymagane do wdrożenia hierarchicznych agentów opartych na celach, są często spotykane w oprogramowaniu. Na przykład edytory tekstu, takie jak te, których używam do pisania dokumentów z księgarni jako kolekcje elementów atomowych i kompozytowych. Najmniejsze elementy, znaki alfanumeryczne, są pogrupowane w coraz bardziej złożone kolekcje. Na przykład słowo "filozofia" jest złożonym składnikiem złożonym z kilku składników atomowych, a zdanie "Myślę, że dlatego jestem" jest złożonym składnikiem złożonym z trzech obiektów złożonych i dwóch obiektów atomowych. Z kolei zdania można grupować w obiekty akapitowe, które można grupować w strony i tak dalej. Jestem pewien, że masz pomysł. Należy zauważyć, że aplikacja jest w stanie równomiernie obsługiwać obiekty złożone i atomowe, niezależnie od ich wielkości i złożoności - tak samo łatwo jest wyciąć i wkleić słowo, jak kilka stron tekstu. Jest to dokładnie właściwość wymagana przez cele hierarchiczne. Ale jak to kodujemy? Złożony wzór projektowy stanowi rozwiązanie. Działa poprzez zdefiniowanie abstrakcyjnej klasy bazowej, która reprezentuje zarówno obiekty złożone, jak i atomowe. Umożliwia to klientom identyczne manipulowanie wszystkimi celami, bez względu na to, jak proste lub złożone mogą być.



Na rysunku wyraźnie pokazano, w jaki sposób obiekty kompozytowe agregują instancje komponentów, które z kolei mogą być kompozytowe lub atomowe. Zwróć uwagę, w jaki sposób obiekty złożone przekazują żądania klientów do swoich dzieci. W tym archetypowym przykładzie żądania są przekazywane wszystkim dzieciom. Jednak inne projekty mogą wymagać nieco innej implementacji, jak ma to miejsce w przypadku celów. Rysunek pokazuje złożony wzór zastosowany do projektowania celów hierarchicznych. Subgoale są dodawane przez popchnięcie ich na przód pojemnika subgoal i są przetwarzane w kolejności LIFO (ostatnie wejście, pierwsze wyjście) w w taki sam sposób jak struktura danych podobna do stosu. Zwróć uwagę, w jaki sposób żądania klientów są przekazywane tylko do subgoalu z przodu, zapewniając, że subgoale są oceniane w kolejności



Obiekty bramkowe mają wiele podobieństw z klasą State. Mają metodę obsługi wiadomości, podobnie jak metody Stan, Aktywuj, Przetwarzaj i Zakończ, które mają podobieństwa z metodami wprowadzania, wykonywania i wychodzenia stanu. Metoda Activate zawiera logikę inicjalizacji i reprezentuje fazę planowania celu. Jednak w przeciwieństwie do metody State :: Enter, która jest wywoływana tylko raz, gdy stan staje się aktualny, cel może wywoływać metodę Activate dowolną liczbę razy, aby wykonać operację ponownie, jeśli sytuacja tego wymaga. Proces, który jest wykonywany na każdym etapie aktualizacji, zwraca wyliczoną wartość wskazującą status celu. Może to być jedna z czterech wartości:

•  nieaktywne: Cel czeka na aktywację.
•  aktywne: cel został aktywowany i będzie przetwarzany na każdym etapie aktualizacji.
•  zakończone: cel został osiągnięty i zostanie usunięty przy następnej aktualizacji.
•  nie powiodło się: cel nie powiódł się i zostanie ponownie wyświetlony lub usunięty przy następnej aktualizacji.

Metoda Terminate wykonuje wszelkie niezbędne porządki przed wyjściem z bramki i jest wywoływana tuż przed zniszczeniem bramki. W praktyce część logiki zaimplementowanej przez cele kompozytowe jest wspólna dla wszystkich kompozytów i można ją wyodrębnić do klasy Goal_Composite, z której wszystkie konkretne cele kompozytowe mogą odziedziczyć, w wyniku czego ostateczny projekt przedstawiono na rysunku 9.7. Diagramy UML dobrze opisują hierarchię klas celów. Nie będę marnować papieru na listę ich deklaracji, ale myślę, że będzie pomocne, jeśli wymienię źródło kilku metod Goal_Composite.



Goal_Composite :: ProcessSubgoals

Wszystkie złożone cele nazywają tę metodę każdym krokiem aktualizacji, aby przetworzyć swoje podzadania. Metoda zapewnia, że wszystkie zrealizowane i nieudane cele zostaną usunięte z listy przed przetworzeniem następnego podpolecenia w linii i zwróceniem jego statusu. Jeśli lista podrzędnych celów jest pusta, zakończona jest zwracana.

template < class entity_type >
int Goal_Composite::ProcessSubgoals() {
// usuń wszystkie ukończone i nieudane bramki z przodu listy podrzędnej
while (!m_SubGoals.empty() &&
(m_SubGoals.front()->isComplete() || m_SubGoals.front()->hasFailed()))
{
m_SubGoals.front()->Terminate();
delete m_SubGoals.front();
m_SubGoals.pop_front();
}
// jeśli pozostaną jakieś podzadania, przetworz to z przodu listy
if (!m_SubGoals.empty()) {
// chwyć status czołowego podrzędu
int StatusOfSubGoals = m_SubGoals.front()->Process();
// musimy przetestować w specjalnym przypadku, w którym subgoal z przodu jest
// zgłasza "ukończono", a lista podrzędnych celów zawiera dodatkowe cele.
// W takim przypadku, aby upewnić się, że rodzic nadal przetwarza swoje
// lista podrzędna, zwracany jest status "aktywny"
if (StatusOfSubGoals == completed && m_SubGoals.size() > 1)
{
return active;
}
return StatusOfSubGoals;
}
// nie ma już poddań do przetworzenia - zwróć "zakończone"
else
{
return completed;
}
}

Goal_Composite::RemoveAllSubgoals

Ta metoda usuwa listę podrzędną. Zapewnia, że wszystkie podzadania są niszczone, wywołując metodę Terminate każdego z nich przed usunięciem.

template
void Goal_Composite::RemoveAllSubgoals()
{
for (SubgoalList::iterator it = m_SubGoals.begin();
it != m_SubGoals.end();
++it)
{
(*it)->Terminate();
delete *it;
}
m_SubGoals.clear();
}

UWAGA. Niektórzy z was mogą się zastanawiać, w jaki sposób cele atomowe implementują metodę AddSubgoal. W końcu ta metoda jest bez znaczenia w tym kontekście (ponieważ cel atomowy nie może z definicji agregować celów potomnych), ale wciąż musi zostać zaimplementowany w celu zapewnienia wymaganego wspólnego interfejsu. Ponieważ klienci powinni wiedzieć, czy cel jest złożony, czy nie, i dlatego nie powinni nigdy wywoływać AddSubgoal w celu atomowym, wybrałem metodę zgłaszania wyjątku

Przykłady celów używanych przez Raven Bots

Boty Raven wykorzystują cele wymienione w Tabeli do zdefiniowania swojego zachowania.



Goal_Think jest najwyższym celem wszystkich. Każdy bot tworzy kopię tego celu, która trwa do momentu zniszczenia bota. Jego zadaniem jest wybieranie innych celów wysokiego poziomu (strategicznych) zgodnie z ich przydatnością do aktualnego stanu gry. Niedługo przyjrzymy się bliżej Goal_Think, ale najpierw myślę, że dobrym pomysłem będzie zbadanie kodu kilku innych celów, abyś mógł poznać ich działanie.

Goal_Wander

To najłatwiejszy do zrozumienia cel i najprostszy w palecie bota Raven. Jest to cel atomowy, który aktywuje zachowanie kierowania wędrówką. Oto jego deklaracja.

class Goal_Wander : public Goal< Raven_Bot >
{
public:
Goal_Wander(Raven_Bot* pBot):Goal(pBot, goal_wander){}
// musi zostać wdrożony
void Activate();
int Process();
void Terminate();
};

Jak widać, deklaracja jest bardzo prosta. Klasa dziedziczy po celu i ma metody, które implementują interfejs celu. Rzućmy okiem na każdą z kolei po kolei.

void Goal_Wander::Activate()
{
m_Status = active;
m_pOwner->GetSteering()->WanderOn();
}

Metoda Active po prostu włącza zachowanie kierowania wędrówką (patrz rozdział 3, jeśli potrzebujesz odświeżenia) i ustawia status celu na aktywny.

int Goal_Wander::Process()
{
// jeśli status jest nieaktywny, wywołaj Activate () i ustaw status na active
ActivateIfInactive();
return m_Status;
}
Goal_Wander :: Process jest równie prosty. ActivateIfInactive is wywoływane na początku logiki procesów każdego celu. Jeśli status celu jest nieaktywny (jak zawsze będzie to pierwsze wywołanie Procesu, ponieważ m_Status jest ustawiony jako nieaktywny w konstruktorze), wywoływana jest metoda Akctive, tym samym inicjując cel. Wreszcie metoda Terminate wyłącza zachowanie wędrówki.

void Goal_Wander::Terminate()
{
m_pOwner->GetSteering()->WanderOff();
}

Teraz przeanalizujmy bardziej złożony cel atomowy

Goal_TraverseEdge

To kieruje bota wzdłuż krawędzi ścieżki i stale monitoruje jego postępy, aby upewnić się, że nie utknie. Aby to ułatwić, wraz z lokalną kopią krawędzi ścieżki, posiada ona członków danych do rejestrowania czasu aktywacji celu i czasu, jaki bot ma zająć pokonanie krawędzi. Jest także właścicielem logicznego elementu danych, który rejestruje, czy krawędź jest ostatnią na ścieżce. Ta wartość jest potrzebna do określenia, jakie zachowanie kierownicze należy zastosować, aby przejść przez krawędź (szukaj normalnych krawędzi ścieżki, dotrzyj do ostatniej). Oto jego deklaracja:

class Goal_TraverseEdge : public Goal< Raven_Bot >
{
private:
// krawędź, którą podąży bot
PathEdge m_Edge;
// true, jeśli m_Edge jest ostatnim na ścieżce
bool m_bLastEdgeInPath;
// szacowany czas, przez który bot powinien przejść przez krawędź
double m_dTimeExpected;
// rejestruje czas aktywacji tego celu
double m_dStartTime;
// wraca true, jeśli bot utknie
bool isStuck()const;
public:
Goal_TraverseEdge(Raven_Bot* pBot,
PathEdge edge,
bool LastEdge);
// Podejrzani
void Activate();
int Process();
void Terminate();
};

Przed określeniem szacunkowego czasu potrzebnego do przejścia, metoda Aktywuj wysyła zapytanie do pola flagi krawędzi wykresu, aby ustalić, czy jest z nim powiązany jakiś specjalny rodzaj terenu - błoto, śnieg, woda itp. - a zachowanie bota odpowiednio się zmienia . (Ta funkcja nie jest używana przez Raven, ale chciałem pokazać, jak sobie z nią poradzić, jeśli Twoja gra korzysta z określonych typów terenu). Metoda kończy się kodem aktywującym odpowiednie zachowanie kierowania. Oto źródło:
void Goal_TraverseEdge::Activate()
{
m_Status = active;
// flaga zachowania krawędzi może określać rodzaj ruchu, który wymaga znaku
// zmiana zachowania bota podążającego za tą krawędzią
switch(m_Edge.GetBehaviorFlag())
{
case NavGraphEdge::swim:
{
m_pOwner->SetMaxSpeed(script->GetDouble("Bot_MaxSwimmingSpeed"));
// ustawić odpowiednią animację
}
break;
case NavGraphEdge::crawl:
{
m_pOwner->SetMaxSpeed(script->GetDouble("Bot_MaxCrawlingSpeed"));
// ustawić odpowiednią animację
}
break;
}
// rejestruj czas, w którym bot rozpoczyna ten cel
m_dStartTime = Clock->GetCurrentTime();
// obliczyć oczekiwany czas wymagany do osiągnięcia tego punktu. Ta wartość
// służy do określenia, czy bot utknął m_dTimeExpected =
m_pOwner->CalculateTimeToReachPosition(m_Edge.GetDestination());
// uwzględni margines błędu dla każdego zachowania reaktywnego. 2 sekundy
// powinno być dużo
static const double MarginOfError = 2.0;
m_dTimeExpected += MarginOfError;
// ustawić cel sterowania
m_pOwner->GetSteering()->SetTarget(m_Edge.GetDestination());
// Ustaw odpowiednie zachowanie kierowania. Jeśli to ostatnia krawędź ścieżki // bot powinien dotrzeć do pozycji, na którą wskazuje, w przeciwnym razie powinien szukać
if (m_bLastEdgeInPath)
{
m_pOwner->GetSteering()->ArriveOn();
}
else
{
m_pOwner->GetSteering()->SeekOn();
}
}

Po aktywowaniu celu przetworzenie go jest proste. Za każdym razem, gdy wywoływana jest metoda Process, kod sprawdza, czy bot utknął lub osiągnął koniec krawędzi i odpowiednio ustawia m_Status

int Goal_TraverseEdge::Process()
{
// jeśli status jest nieaktywny, wywołaj Activate ()
ActivateIfInactive();
// jeśli bot utknął, błąd powrotu
if (isStuck())
{
m_Status = failed;
}
// jeśli bot osiągnął koniec krawędzi powrót jest zakończony
else
{
if (m_pOwner->isAtPosition(m_Edge.GetDestination()))
{
m_Status = completed;
}
}
return m_Status;
}

Metoda Terminate wyłącza zachowanie kierownicy i resetuje maksymalną prędkość bota do normy.

void Goal_TraverseEdge::Terminate()
{
// wyłącz zachowania kierownicze
m_pOwner->GetSteering()->SeekOff();
m_pOwner->GetSteering()->ArriveOff();
// przywrócić maksymalną prędkość do normy
m_pOwner->SetMaxSpeed(script->GetDouble("Bot_MaxSpeed"));
}

Przejdźmy teraz do zbadania kilku złożonych celów

Goal_FollowPath

To kieruje bota wzdłuż ścieżki poprzez wielokrotne wyskakiwanie krawędzi z przodu ścieżki i wypychanie celów typu krawędzi trawersowania na przód listy podrzędnej. Oto jego deklaracja:

class Goal_FollowPath : public Goal_Composite< Raven_Bot >
{
private:
// lokalna kopia ścieżki zwrócona przez narzędzie do planowania ścieżki
std::list m_Path;
public: Goal_FollowPath(Raven_Bot* pBot, std::list path);
//podejrzani
void Activate();
int Process();
void Terminate(){}
}

Oprócz powiązania z nimi określonych typów terenu, krawędzie wykresów mogą również wymagać od bota użycia określonej akcji do poruszania się wzdłuż nich. Na przykład krawędź może wymagać od agenta latania, skakania, a nawet używania haka do chwytania, aby poruszać się wzdłuż niej. Tego rodzaju ograniczenia ruchu nie można rozwiązać, po prostu dostosowując maksymalną prędkość i cykl animacji agenta. Zamiast tego dla każdej akcji należy utworzyć unikalny cel typu krawędzi poprzecznych. Cel podążania ścieżką może następnie sprawdzać flagi krawędzi w ramach metody Aktywuj i dodawać poprawny typ celu krawędzi trawersu do swojej listy podrzędnej, gdy wyskakuje z ścieżki. Aby to wyjaśnić, oto lista metod aktywacji:

void Goal_FollowPath::Activate()
{
m_iStatus = active;
// uzyskać odniesienie do następnej krawędzi
PathEdge edge = m_Path.front();
// usuń krawędź ze ścieżki
m_Path.pop_front();
// niektóre krawędzie określają, że bot powinien używać określonego zachowania, gdy
//podążać za nimi. Ta instrukcja switch pyta o flagę zachowania krawędzi i
// dodaje odpowiednie cele do listy celów cząstkowych.
switch(edge.GetBehaviorFlags())
{
case NavGraphEdge::normal:
{
AddSubgoal(new Goal_TraverseEdge(m_pOwner, edge, m_Path.empty()));
}
break;
case NavGraphEdge::goes_through_door:
{
// also add a goal that is able to handle opening the door
AddSubgoal(new Goal_NegotiateDoor(m_pOwner, edge, m_Path.empty()));
}
break;
case NavGraphEdge::jump:
{
// dodaj subcel, aby przeskoczyć wzdłuż krawędzi
}
break;
case NavGraphEdge::grapple:
{
// dodaj subgoal, aby chwycić się wzdłuż krawędzi
}
break;
default:
throw
std::runtime_error(": Unrecognized edge type");

Aby zwiększyć wydajność, zauważ, jak tylko jedna krawędź jest usuwana na raz ze ścieżki. Aby to ułatwić, metoda Process wywołuje Aktywuj za każdym razem, gdy wykryje, że jej podzadania są zakończone, a ścieżka nie jest pusta. Oto jak:

int Goal_FollowPath::Process()
{
// jeśli status jest nieaktywny, wywołaj Activate ()
ActivateIfInactive();
// jeśli nie ma podzadań i nadal istnieje krawędź do przejścia, dodaj
// krawędź jako subgoal
m_Status = ProcessSubgoals();
// jeśli nie ma żadnych podzadań, sprawdź, czy ścieżka nadal ma krawędzie.
// jeśli wywoła Aktywację, aby złapać następną krawędź.
if (m_Status == completed && !m_Path.empty())
{
Activate();
}
return m_Status;
}

Metoda Goal_FollowPath :: Terminate nie zawiera logiki, ponieważ nie ma nic do uporządkowania.

PORADA. Po uruchomieniu pliku wykonywalnego Raven istnieje możliwość wyświetlenia listy celów wybranego agenta w menu.



Rysunek jest w skali szarości, ale po uruchomieniu demonstracji aktywne cele zostaną narysowane na niebiesko, zakończone na zielono, nieaktywne na czarno i nieudane na czerwono. Wcięcie pokazuje, w jaki sposób cele są zagnieżdżone.

Goal_MoveToPosition

Ten złożony cel służy do przeniesienia bota w dowolne miejsce na mapie. Oto jego deklaracja:

bool Goal_MoveToPosition::HandleMessage(const Telegram& msg)
{
//first, pass the message down the goal hierarchy
bool bHandled = ForwardMessageToFrontMostSubgoal(msg);
// jeśli msg nie został obsłużony, sprawdź, czy ten cel sobie z tym poradzi
if (bHandled == false)
{
switch(msg.Msg)
{
case Msg_PathReady:
// wyczyść wszystkie istniejące cele
RemoveAllSubgoals();
AddSubgoal(new Goal_FollowPath(m_pOwner,
m_pOwner->GetPathPlanner()->GetPath()));
return true; // msg obsługiwane
case Msg_NoPathAvailable:
m_Status = failed;
return true; // msg obsługiwane
default: return false;
}
}
// obsługiwane przez subgoals
return true;
}

Subgoale Goal_MoveToPosition są przetwarzane i stale monitorowane pod kątem awarii. Jeśli jeden z podzadań nie powiedzie się, cel ten reaktywuje się w celu przeplanowania.

int Goal_MoveToPosition::Process()
{
// jeśli status jest nieaktywny, wywołaj Activate () i ustaw status na active
ActivateIfInactive();
// przetwarzać podzadania
m_Status = ProcessSubgoals();
// jeśli którykolwiek z podzadań nie powiódł się, cel ten zostanie ponownie ustawiony
ReactivateIfFailed();
return m_Status;
}

Przejdźmy teraz do sprawdzenia, jak działa jeden z pozostałych celów na poziomie strategii: Goal_AttackTarget.

Goal_AttackTarget

Bot wybiera tę strategię, gdy czuje się zdrowy i wystarczająco uzbrojony, aby zaatakować swój obecny cel. Goal_AttackTarget jest celem złożonym, a jego deklaracja jest prosta.

class Goal_AttackTarget : public Goal_Composite
{
public:
Goal_AttackTarget(Raven_Bot* pOwner);
void Activate();
int Process();
void Terminate(){m_iStatus = completed;}
};

Cała akcja dzieje się w metodzie Aktywuj. Przede wszystkim wszelkie istniejące podzadania są usuwane, a następnie sprawdzane, czy cel bota jest nadal aktualny. Jest to niezbędne, ponieważ cel może zginąć lub wyjść poza horyzont sensoryczny bota, gdy ten cel jest nadal aktywny. W takim przypadku cel musi zakończyć się.

void Goal_AttackTarget::Activate()
{
m_iStatus = active;
// jeśli ten cel zostanie ponownie aktywowany, mogą istnieć pewne podzadania, które
// należy usunąć
RemoveAllSubgoals();
// cel bota może umrzeć, gdy ten cel jest aktywny, więc my // należy przetestować, aby upewnić się, że bot zawsze ma aktywny cel
if (!m_pOwner->GetTargetSys()->isTargetPresent())
{
m_iStatus = completed;
return;
}

Następnie bot sprawdza swój system celowania, aby dowiedzieć się, czy ma bezpośredni strzał w cel. Jeśli strzał jest możliwy, wybiera taktykę ruchu, którą należy zastosować. Pamiętaj, że system broni jest całkowicie oddzielnym składnikiem sztucznej inteligencji i zawsze automatycznie wybiera najlepszą broń, celuje i strzela do bieżącego celu, bez względu na cel, do którego dąży bot. Oznacza to, że ten cel musi jedynie dyktować ruch bota podczas ataku. Udostępniłem botom Raven tylko dwie możliwości: jeśli po lewej lub prawej stronie bota jest miejsce, będzie on toczył się z boku na bok, dodając Goal_DodgeSideToSide do swojej listy podrzędnej. Jeśli nie ma miejsca na unik, bot po prostu szuka aktualnej pozycji celu.

// jeśli bot jest w stanie strzelić do celu (między botem a LOS jest LOS)
// cel), a następnie wybierz taktykę, którą chcesz zastosować podczas strzelania
if (m_pOwner->GetTargetSys()->isTargetShootable())
{
// jeśli bot ma miejsce na atak, zrób to
Vector2D dummy;
if (m_pOwner->canStepLeft(dummy) || m_pOwner->canStepRight(dummy))
{
AddSubgoal(new Goal_DodgeSideToSide(m_pOwner));
}
// jeśli nie jest w stanie atakować, skieruj się bezpośrednio na pozycję celu
else
{
AddSubgoal(new Goal_SeekToPosition(m_pOwner,
m_pOwner->GetTargetBot()->Pos()));
}
}

W zależności od wymagań twojej gry prawdopodobnie będziesz chciał dać botowi znacznie szerszy wybór taktyk agresywnego ruchu do wyboru. Na przykład, możesz chcieć dodać taktykę, która przenosi bota do idealnego zasięgu do strzelania jego prądem (lub ulubiona) broń lub taka, która wybiera dobrą pozycję snajperską lub osłonową (nie zapominaj, że węzłom nawigacyjnym można przypisać takie informacje). Jeśli nie ma bezpośredniego strzału w cel - ponieważ mógł on właśnie biegać za rogiem - bot dodaje Goal_HuntTarget do swojej listy podrzędnej. Metoda Process dla Goal_AttackTarget jest trywialna. Po prostu upewnia się, że podzadania są przetwarzane, a cel zostanie ponownie zainstalowany w przypadku wykrycia problemu

int Goal_AttackTarget::Process()
{
// jeśli status jest nieaktywny, wywołaj Activate()
ActivateIfInactive();
// przetwarzać podzadania
m_iStatus = ProcessSubgoals();
ReactivateIfFailed();
return m_iStatus;
}

Nie będę wchodził w szczegóły Goal_HuntTarget i Goal_Dodge-SideToSide. To oczywiste, co robią i zawsze możesz sprawdzić kod źródłowy, jeśli chcesz spojrzeć na drobiazgi.

Arbitraż celi

Teraz rozumiesz, jak działają cele i widziałeś kilka konkretnych przykładów, ale prawdopodobnie nadal zastanawiasz się, w jaki sposób boty wybierają cele na poziomie strategii. Osiąga się to dzięki złożonemu celowi Goal_Think, którego każdy bot posiada trwałą instancję, stanowiącą podstawę jego hierarchii celów. Funkcja Goal_Think polega na arbitrażu między dostępnymi strategiami, wybierając najbardziej odpowiednie do realizacji. Jest sześć strategicznych celów:

•  Explore : Agent wybiera dowolny punkt w swoim środowisku i planuje i podąża ścieżką do tego punktu.
•  Get Health: Agent znajduje ścieżkę o najniższym koszcie do wystąpienia przedmiotu zdrowia i podąża ścieżką do tego przedmiotu.
•  Get Weapon (Rocket Launcher): Agent znajduje ścieżkę o najniższym koszcie do instancji wyrzutni rakiet i podąża za nią.
•  Get Weapon (Shotgun): Agent znajduje ścieżkę o najniższym koszcie do wystąpienia strzelby i podąża za nią.
•  Get Weapon (Railgun): Agent znajduje ścieżkę o najniższym koszcie do instancji Railguna i podąża za nią.
•  Attack Target: Agent określa strategię ataku na swój obecny cel.

Każda aktualizacja myśli każda z tych strategii jest oceniana i otrzymuje wynik reprezentujący celowość jej realizacji. Strategia o najwyższym wyniku jest przypisywana do strategii, którą agent będzie próbował spełnić. Aby ułatwić ten proces, każdy Goal_Think agreguje kilka instancji Goal_Evaluator, po jednej dla każdej strategii. Obiekty te mają metody obliczania pożądanej strategii, którą reprezentują, oraz dodawania tego celu do listy podrzędnych celów. R



Każda metoda CalculateDesirability to ręcznie wykonany algorytm, który zwraca wartość wskazującą na celowość bota realizującego odpowiednią strategię. Algorytmy te mogą być trudne do utworzenia, dlatego często przydatne jest najpierw skonstruowanie niektórych funkcji pomocniczych, które odwzorowują określone informacje z gry na wartości liczbowe w zakresie od 0 do 1. Są one następnie wykorzystywane w formułowaniu algorytmów pożądalności. Nie jest szczególnie ważne, jaki zakres wartości zwracają metody wyodrębniania obiektów - od 0 do 1, od 0 do 100 lub od -10000 do 1000 są w porządku - ale pomaga, jeśli są one znormalizowane we wszystkich metodach. Ułatwi to mózg, gdy zaczniesz tworzyć algorytmy pożądania. Aby zdecydować, jakie informacje należy pobrać ze świata gry, rozważ kolejno każdy cel strategii i jakie cechy gry mają wpływ na celowość jego realizacji. Na przykład ocena GetHealth będzie wymagała informacji o stanie zdrowia bota i lokalizacji przedmiotu zdrowia. Podobnie ewaluator AttackTarget będzie wymagał informacji o broni i amunicji, którą nosi bot, oprócz jego poziomów zdrowia (bot o niskim poziomie zdrowia znacznie rzadziej zaatakuje przeciwnika niż bot, który czuje się sprawny jako skrzypce). Ewaluator ExploreGoal to szczególny przypadek, jak się wkrótce okaże, ale ewaluator GetWeapon będzie wymagał dodatkowej wiedzy na temat odległości do określonej broni i aktualnej amunicji, którą ma przy sobie bot. Raven używa czterech takich funkcji ekstrakcji funkcji, zaimplementowanych jako metody statyczne klasy Raven_Feature. Oto lista deklaracji klasy, która zawiera opis każdej metody w komentarzach:

class Raven_Feature
{
public:
// zwraca wartość od 0 do 1 w oparciu o zdrowie bota. Im lepsze
// zdrowie, tym wyższa ocena
static double Health(Raven_Bot* pBot);
// zwraca wartość od 0 do 1 na podstawie odległości bota do
// dany element. Im dalej przedmiot, tym wyższa ocena. Jeśli nie ma
// przedmiot danego typu obecny w świecie gry w czasie tej metody
// nazywa się zwracana wartość to 1
static double DistanceToItem(Raven_Bot* pBot, int ItemType);
// zwraca wartość od 0 do 1 w oparciu o ilość amunicji dla bota // dana broń i maksymalna ilość amunicji, którą bot może nosić.
// im bliżej przenoszonej kwoty do kwoty maksymalnej, tym wyższy wynik
static double IndividualWeaponStrength(Raven_Bot* pBot, int WeaponType);
// zwraca wartość od 0 do 1 w oparciu o całkowitą ilość amunicji
// bot przenosi każdą broń. Każda z trzech broni to bot // może podnieść może przyczynić się do uzyskania trzeciej części wyniku. Innymi słowy, jeśli bot // nosi RL i RG i ma maksymalną amunicję do RG, ale tylko połowę max
// dla RL ocena będzie wynosić 1/3 + 1/6 + 0 = 0,5
static double TotalWeaponStrength(Raven_Bot* pBot);
};

Teraz, gdy mamy kilka funkcji pomocniczych, przyjrzyjmy się, jak można ich użyć do obliczenia wyników pożądalności dla każdej strategii, które są również standaryzowane w zakresie od 0 do 1.

Obliczanie celowości zlokalizowania przedmiotu zdrowia

Ogólnie rzecz biorąc, celowość zlokalizowania przedmiotu zdrowia jest proporcjonalna do obecnego poziomu zdrowia bota i odwrotnie proporcjonalna do odległości od najbliższej instancji. Ponieważ każda z tych funkcji jest wyodrębniana metodami omówionymi wcześniej i reprezentowana jako liczba z zakresu od 0 do 1, można to zapisać jako:

Celowośćhealth = (1 - Health / DistToHealth)

gdzie k jest stałą używaną do poprawiania wyniku. Ta relacja ma sens, ponieważ im dalej musisz podróżować, aby znaleźć przedmiot, tym mniej go pragniesz, a im niższy poziom zdrowia, tym większe twoje pragnienie. (Zauważ, że nie musimy się martwić błędem dzielenia przez zero, ponieważ agent nie może zbliżyć się do promienia poza jego promień ograniczający, zanim element zostanie wyzwolony.) Oto kod źródłowy z Raven, który implementuje to algorytm.

double GetHealthGoal_Evaluator::CalculateDesirability(Raven_Bot* pBot)
{
// najpierw złap odległość do najbliższego wystąpienia przedmiotu zdrowia
double Distance = Raven_Feature::DistanceToItem(pBot, type_health);
// jeśli funkcja odległości ma wartość 1, oznacza to, że
// element nie znajduje się na mapie lub jest zbyt daleko, aby był wart
// biorąc pod uwagę, dlatego pożądana jest wartość zero
if (Distance == 1)
{
return 0;
}
else
{
// wartość użyta do ulepszenia pożądalności
const double Tweaker = 0.2;
// celowość znalezienia przedmiotu zdrowia jest proporcjonalna do kwoty
// zdrowie pozostające i odwrotnie proporcjonalne do odległości od zdjęcia
// najbliższa instancja elementu zdrowia.
double Desirability = Tweaker * (1-Raven_Feature::Health(pBot)) /
(Raven_Feature::DistanceToItem(pBot, type_health));
// upewnij się, że wartość mieści się w zakresie od 0 do 1
Clamp(Desirability, 0, 1);
return Desirability;
}
}

Obliczanie celowości zlokalizowania określonej broni

Jest to bardzo podobne do poprzedniego algorytmu. Celowość zlokalizowania określonej broni można podać jako:

Celowośćweapons = k x ( Health x (1-WeaponStrength) / DistToWeapon)

Zwróć uwagę, w jaki sposób zarówno siła broni, jak i funkcje zdrowotne przyczyniają się do potrzeby odzyskania broni. Jest to rozsądne, ponieważ w miarę jak bot zostaje poważnie uszkodzony lub ilość amunicji, którą nosi dla tej konkretnej broni, jego chęć odzyskania powinna się zmniejszyć. Oto jak algorytm wygląda w kodzie:

double GetWeaponGoal_Evaluator::CalculateDesirability(Raven_Bot* pBot)
{
// złap odległość do najbliższego wystąpienia typu broni
double Distance = Raven_Feature::DistanceToItem(pBot, m_iWeaponType);
// jeśli funkcja odległości ma wartość 1, oznacza to, że
// element nie znajduje się na mapie lub jest zbyt daleko, aby był wart
// biorąc pod uwagę, dlatego pożądana jest wartość zero
if (Distance == 1)
{
return 0;
}
else
{
// wartość użyta do ulepszenia pożądalności
const double Tweaker = 0.15f;
double Health, WeaponStrength;
Health = Raven_Feature::Health(pBot);
WeaponStrength = Raven_Feature::IndividualWeaponStrength(pBot, m_iWeaponType);
double Desirability = (Tweaker * Health * (1-WeaponStrength)) / Distance;
// wartość użyta do ulepszenia pożądalności br> Clamp(Desirability, 0, 1);
return Desirability;
}
}

Dodatkową zaletą wykorzystania odległości jako czynnika do obliczenia celowości przy podnoszeniu broni i przedmiotów zdrowotnych jest to, że w odpowiednich okolicznościach boty tymczasowo zmieniają strategię i zmieniają kurs, aby podnieść przedmioty w pobliżu

WSKAZÓWKA. Wpływ odległości w analizowanych do tej pory algorytmach pożądania jest liniowy. Innymi słowy, pożądalność jest wprost proporcjonalna do odległości od przedmiotu. Możesz jednak chcieć, aby "przyciąganie" przedmiotu na bocie stawało się silniejsze w miarę zbliżania się bota (jak siła, którą odczuwasz, gdy przesuwasz dwa magnesy ku sobie), zamiast ze stałą szybkością (np. Siła czujesz, kiedy naciągasz sprężynę). Najlepiej to wyjaśnić za pomocą wykresu.


Aby utworzyć algorytm, który tworzy krzywą pożądania-odległości podobną do wykresu po prawej, musisz podzielić przez kwadrat (lub nawet sześcian) odległości. Innymi słowy, równanie zmienia się na:

Celowośćweapons = k x ( Health x (1-WeaponStrength) /DistToWeapon2)

Nie zapominaj, że będziesz musiał również dostosować k, aby uzyskać pożądane wyniki.

Obliczanie celowości ataku na cel

Celowość zaatakowania przeciwnika jest proporcjonalna do tego, jak zdrowy i silny jest bot. "Potężna" funkcja, w kontekście Kruka, wskazuje liczbę broni i amunicji, którą nosi bot, i jest oceniana za pomocą metody Raven_Feature :: TotalWeaponStrength. (Polecam zajrzeć do tej metody następnym razem, gdy będziesz siedzieć przy komputerze). Korzystając z tych dwóch funkcji, możemy obliczyć celowość realizacji celu AttackTarget:

Celowośćattack = k x TotalWeaponStrength x Health

Oto jak wygląda napisane w kodzie:

double AttackTargetGoal_Evaluator::CalculateDesirability(Raven_Bot* pBot)
{
double Desirability = 0.0;
// wykonuj obliczenia tylko wtedy, gdy istnieje cel
if (pBot->GetTargetSys()->isTargetPresent())
{
const double Tweaker = 1.0;
Desirability = Tweaker *
Raven_Feature::Health(pBot) *
Raven_Feature::TotalWeaponStrength(pBot);
}
return Desirability;

WSKAZÓWKA. W zależności od tego, jak bardzo potrzebujesz agenta, możesz dodawać i usuwać strategie z arbitra. (Pamiętaj, że Goal_Think jest arbitrem celów strategicznych bota Raven.) Rzeczywiście, możesz nawet włączać i wyłączać całe zestawy celów strategicznych, aby zapewnić agentowi zupełnie nowy zestaw zachowań do wyboru. Far Cry na przykład używa tego rodzaju techniki z dobrym skutkiem.

Obliczanie celowości eksploracji mapy

To jest łatwe. Wyobraź sobie, że grasz w tę grę. Możesz eksplorować mapę tylko wtedy, gdy nie ma innych rzeczy wymagających Twojej natychmiastowej uwagi, takich jak atakowanie przeciwnika lub poszukiwanie amunicji lub zdrowia. W konsekwencji celowość eksploracji mapy jest ustalona jako niska stała wartość, dzięki czemu opcja eksploracji jest wybrana tylko wtedy, gdy wszystkie alternatywy mają niższe oceny celowości. Oto kod:

double ExploreGoal_Evaluator::CalculateDesirability(Raven_Bot* pBot)
{
const double Desirability = 0.05;
return Desirability;
}

Łącząc wszystko razem

Po zdefiniowaniu funkcji celowości dla każdego obiektu ewaluatora, pozostaje tylko to, aby Goal_Think przeprowadzał iterację przy każdej aktualizacji myśli i wybierał najwyższą strategię, którą będzie realizował bot. Oto kod wyjaśniający:

void Goal_Think::Arbitrate()
{
double best = 0;
Goal_Evaluator* MostDesirable = NULL;
// teruj przez wszystkich oceniających, aby zobaczyć, który daje najwyższy wynik
GoalEvaluators::iterator curDes = m_Evaluators.begin();
for (curDes; curDes != m_Evaluators.end(); ++curDes)
{
double desirabilty = (*curDes)->CalculateDesirability(m_pOwner);
if (desirabilty >= best)
{
best = desirabilty;
MostDesirable = *curDes;
}
}
MostDesirable->SetGoal(m_pOwner);
}

WSKAZÓWKA. Gracze będący ludźmi mogą przewidywać, co zrobi inny gracz, i odpowiednio postępować. Możemy to zrobić, ponieważ jesteśmy w stanie krótko zmienić punkt widzenia na innych graczy i zastanowić się, jakie mogą być ich pragnienia, jeśli rozumiemy ich stan i świat gry. Oto przykład: z dystansu obserwujesz dwóch graczy, Sid i Erica, walczących z wyrzutniami rakiet, gdy nagle, po dwukrotnym trafieniu z rzędu, Eric odrywa się i zaczyna biec korytarzem. Zmieniasz punkt widzenia na Erica, a ponieważ wiesz, że ma mało zdrowia, przewidujesz, że bardzo prawdopodobne jest, że zmierza w kierunku pakietu zdrowia, o którym wiesz, że znajduje się w pokoju na końcu korytarza. Zdajesz sobie również sprawę, że jesteś umieszczony bliżej zdrowia niż Eric, więc decydujesz się go "ukraść" i odczekać, aż dotrze na miejsce, po czym wysmarujesz jego jelita wzdłuż ściany karabinem plazmowym. Umiejętność przewidywania działań innych w ten sposób jest wrodzoną cechą ludzkich zachowań - robimy to cały czas - ale możliwe jest nadanie agentowi podobnej, choć nieco osłabionej zdolności. Ponieważ pragnienia agentów arbitrażowych bramek są ustalane algorytmicznie, możesz sprawić, by bot uruchomił odpowiednie atrybuty (zdrowie, amunicję itp.) Gracza poprzez własny (lub niestandardowy) arbiter, aby zgadywać, jakie mogą być pragnienia gracza być w tym czasie. Oczywiście dokładność tego przypuszczenia zależy w dużej mierze od tego, jak ściśle pragnienia bota odpowiadają graczowi - i to zależy od twoich umiejętności modelowania behawioralnego - ale zwykle nie jest zbyt trudne wykonanie od czasu do czasu dokładnej prognozy, która pozwala botowi dać graczowi przykra niespodzianka, nawet w przypadku bardzo podstawowego modelu.

Podziały

Jedną wielką zaletą hierarchicznego projektu arbitrażu opartego na celach jest to, że dodatkowe funkcje są dostarczane przy niewielkim dodatkowym wysiłku ze strony programisty. Spędzimy resztę rozdziału, przyglądając się im.

Osobowości

Ponieważ oceny celowości są ograniczone do tego samego zakresu, łatwo jest stworzyć agentów o różnych cechach osobowości, mnożąc każdy wynik przez stałą, która przesuwa go w wymaganym kierunku. Na przykład, aby stworzyć bota Ravena, który gra agresywnie, nie dbając o własne bezpieczeństwo, możesz zmniejszyć jego chęć uzyskania zdrowia o 0,6 i chęć zaatakowania celów o 1,5. Aby stworzyć grę, która gra ostrożnie, możesz zaspokoić pragnienia bota, aby był bardziej podatny na zbieranie broni i zdrowia niż atak. Jeśli miałbyś zaprojektować agentów ukierunkowanych na cel dla gry RTS, możesz stworzyć jednego przeciwnika, który faworyzuje eksplorację i badania technologii, drugiego, który woli tworzyć ogromne armie tak szybko, jak to możliwe, a drugiego, który ma obsesję na punkcie obrony miasta. Aby ułatwić takie cechy osobowości, klasa podstawowa Goal_Evaluator zawiera zmienną składową m_dCharacterBias, której klient w konstruktorze przypisuje wartość:

class Goal_Evaluator
{
protected:
// gdy oceniono celowość dla bramki, jest ona mnożona
// według tej wartości. Można go używać do tworzenia botów z preferencjami opartymi na
// ich osobowości
double m_dCharacterBias;
public:
Goal_Evaluator(double CharacterBias):m_dCharacterBias(CharacterBias){}
/* DODATKOWE SZCZEGÓŁY POMINIĘTO */
};

m_dCharacterBias jest wykorzystywany w metodzie CalculateDesirability każdej podklasy do dostosowania obliczania wyniku oceny pożądalności. Oto jak jest dodawany do obliczania celowości AttackTarget:

double AttackTargetGoal_Evaluator::CalculateDesirability(Raven_Bot* pBot)
{
double Desirability = 0.0;
// wykonuj obliczenia tylko wtedy, gdy istnieje cel
if (pBot->GetTargetSys()->isTargetPresent())
{
const double Tweaker = 1.0;
Desirability = Tweaker *
Raven_Feature::Health(pBot) *
Raven_Feature::TotalWeaponStrength(pBot);
//odchylenie wartości zgodnie z osobowością bota
Desirability *= m_dCharacterBias;
}
return Desirability;
}

Jeśli twój projekt gry wymaga, aby osobowości botów pozostawały między grami, powinieneś utworzyć osobny plik skryptu dla każdego bota zawierający uprzedzenia (plus wszelkie inne dane charakterystyczne dla bota, takie jak dokładność celowania broni, preferencje wyboru broni itp.) . Jednak w Raven nie ma takich botów; za każdym razem, gdy uruchamiasz program, tendencyjności boty są przypisywane losowe wartości w konstruktorze Goal_Think, tak jak:

// te uprzedzenia mogą zostać załadowane ze skryptu dla poszczególnych botów
// ale na razie podamy im losowe wartości
const double LowRangeOfBias = 0.5;
const double HighRangeOfBias = 1.5;
double HealthBias = RandInRange(LowRangeOfBias, HighRangeOfBias);
double ShotgunBias = RandInRange(LowRangeOfBias, HighRangeOfBias);
double RocketLauncherBias = RandInRange(LowRangeOfBias, HighRangeOfBias);
double RailgunBias = RandInRange(LowRangeOfBias, HighRangeOfBias);
double ExploreBias = RandInRange(LowRangeOfBias, HighRangeOfBias);
double AttackBias = RandInRange(LowRangeOfBias, HighRangeOfBias);
// utwórz obiekty oceniające
m_Evaluators.push_back(new GetHealthGoal_Evaluator(HealthBias));
m_Evaluators.push_back(new ExploreGoal_Evaluator(ExploreBias));
m_Evaluators.push_back(new AttackTargetGoal_Evaluator(AttackBias));
m_Evaluators.push_back(new GetWeaponGoal_Evaluator(ShotgunBias,
type_shotgun));
m_Evaluators.push_back(new GetWeaponGoal_Evaluator(RailgunBias,
type_rail_gun));
m_Evaluators.push_back(new GetWeaponGoal_Evaluator(RocketLauncherBias,
type_rocket_launcher));

PORADA. Arbitraż celowy jest zasadniczo procesem algorytmicznym zdefiniowanym przez kilka liczb. W rezultacie nie jest sterowany przez logikę (jak FSM), ale przez dane. Jest to niezwykle korzystne, ponieważ wszystko, co musisz zrobić, aby zmienić zachowanie, to poprawienie liczb, które wolisz przechowywać w pliku skryptu, aby inni członkowie Twojego zespołu mogli z nimi z łatwością eksperymentować.
Pamięć stanu

Charakter złożonych celów (LIFO) automatycznie nadaje się do agentów z pamięcią, umożliwiając im tymczasową zmianę zachowania poprzez wypchnięcie nowego celu (lub celów) na przód listy podrzędnych celów. Gdy tylko nowy cel zostanie osiągnięty, pojawi się na liście, a agent wznowi wszystko, co wcześniej robił. Jest to bardzo potężna funkcja, którą można wykorzystać na wiele różnych sposobów. Oto kilka przykładów.

Przykład pierwszy - automatyczne wznawianie przerwanych czynności

Wyobraź sobie, że Eric, który jest w drodze do kuźni ze złotem w kieszeni, zostaje złapany przez złodzieja z nożem Rambo. Dzieje się to tuż przed osiągnięciem trzeciego punktu na drodze, którą podąża. Lista subgoal jego mózgu przypomina w tym miejscu



Eric nie spodziewał się, że tak się stanie, ale na szczęście programista AI stworzył cel, aby poradzić sobie z tego rodzaju sprawą, zwaną DefendAgainst-Attacker. Cel ten zostaje przesunięty na przód jego listy podrzędnej i pozostaje aktywny, dopóki złodziej nie ucieknie lub nie zostanie zabity przez Erica.



Wspaniałą rzeczą w tym projekcie jest to, że gdy DefendAgainstAttacker jest usatysfakcjonowany i usunięty z listy, Eric automatycznie wznawia śledzenie krawędzi do punktu trzeciego. Niektórzy z was zapewne będą myśleć: "Ach, ale co, jeśli ścigając złodzieja, Eric straci wzrok z punktu trzeciego?" To fantastyczna cecha tego projektu. Ponieważ cele mają wbudowaną logikę do wykrywania awarii i ponownego tworzenia planu, jeśli cel nie powiedzie się, projekt przesuwa się w górę hierarchii do momentu znalezienia elementu nadrzędnego, który jest w stanie ponownie zaplanować cel.

Przykład drugi - negocjowanie przeszkód na specjalnej ścieżce

Wiele projektów gier wymaga od agentów pokonywania jednego lub więcej rodzajów przeszkód na drodze, takich jak drzwi, windy, mosty zwodzone i ruchome platformy. Często wymaga to od agenta wykonania krótkiej sekwencji działań. Na przykład, aby skorzystać z windy, agent musi znaleźć przycisk, który go wywołuje, podejdź do przycisku, naciśnij go, a następnie cofnij się i stań przed drzwiami, aż winda się pojawi. Korzystanie z ruchomej platformy jest podobnym procesem: agent musi podejść do mechanizmu, który działa na platformie, naciśnij / pociągnij ją, podejdź do miejsca wsiadania, poczekaj na platformę, a na koniec wejdź na platformę i poczekaj, aż dojdzie do celu.



Te "przeszkody" powinny być przejrzyste dla planisty ścieżki, ponieważ nie stanowią one przeszkód dla ruchu agenta. Oczywiście negocjowanie ich wymaga czasu, ale może to znaleźć odzwierciedlenie w kosztach nawigacyjnych. Aby agenci mogli poradzić sobie z takimi przeszkodami, przechodząca przez nie krawędź wykresu musi być opatrzona adnotacjami odzwierciedlającymi ich rodzaj. Cel FollowPath może następnie sprawdzić te informacje i upewnić się, że właściwy typ celu jest wypychany na początek listy celów agenta, gdy taka krawędź zostanie napotkana. Tak jak w poprzednim przykładzie, agent będzie realizował ten nowy cel cząstkowy, dopóki nie zostanie ukończony, a następnie wznowi wszystko, co robił wcześniej. Aby zilustrować tę zasadę, dodałem wsparcie przy negocjowaniu przesuwnych drzwi do repertuaru botów Raven. Drzwi przesuwne są otwierane poprzez dotknięcie "przycisku" znajdującego się w pobliżu drzwi (po jednym z każdej strony). Kiedy drzwi są dodawane w edytorze map, wszelkie krawędzie wykresu przekraczające granicę drzwi są oznaczane flagą idzie_through_door. Jeśli bot napotka krawędź oznaczoną w ten sposób (ponieważ są one ściągane ze ścieżki w Goal_FollowPath :: Activate), cel NegotiateDoor jest dodawany do listy podrzędnej w taki sposób:

void Goal_FollowPath::Activate()
{
// uzyskać odniesienie do następnej krawędzi
const PathEdge& edge = m_Path.front();
switch(edge.GetBehaviorFlags())
{
case NavGraphEdge::goes_through_door:
{
//dodaj cel, który jest w stanie obsłużyć otwieranie drzwir
AddSubgoal(new Goal_NegotiateDoor(m_pOwner, edge));
}
break;
//etc

Cel NegotiateDoor kieruje bota poprzez sekwencję działań wymaganych, aby otworzyć i przejść przez drzwi. Jako przykład rozważmy przypadek bota pokazanego na rysunku, którego ścieżka wiedzie wzdłuż krawędzi AB, która jest zasłonięta przesuwanymi drzwiami



Aby przejść przez przesuwane drzwi, bot musi wykonać następujące kroki

1. Uzyskaj listę przycisków otwierających drzwi (b1 i b2).
2. Z listy wybierz najbliższy przycisk nawigacyjny (b1).
3. Zaplanuj i podążaj ścieżką do przycisku b1 (przycisk uruchomi się, otwierają się drzwi).
4. Zaplanuj i podążaj ścieżką do węzła A.
5. Przejdź przez krawędź AB.

Goal_NegotiateDoor rozwiązuje każdy z tych kroków w metodzie Activate, dodając podzakresy niezbędne do wykonania zadania. Listing pomoże to wyjaśnić

void Goal_NegotiateDoor::Activate()
{ m_iStatus = active;
// jeśli ten cel zostanie ponownie aktywowany, mogą istnieć pewne podzadania, które
// należy usunąć
RemoveAllSubgoals();
// uzyskać pozycję najbliższego przełączalnego przełącznika
Vector2D posSw = m_pOwner->GetWorld()->GetPosOfClosestSwitch(m_pOwner->Pos(),
m_PathEdge.GetDoorID());
// ponieważ cele są * wypychane * na przód listy podrzędnych celów, które muszą
// być dodany w odwrotnej kolejności.
// po pierwsze, cel przejścia przez krawędź, która przechodzi przez drzwi
AddSubgoal(new Goal_TraverseEdge(m_pOwner, m_PathEdge));
// następnie cel, który przeniesie bota na początek krawędzi, że
// przechodzi przez drzwi
AddSubgoal(new Goal_MoveToPosition(m_pOwner, m_PathEdge.GetSource()));
// wreszcie cel, który skieruje bota do położenia przełącznika
AddSubgoal(new Goal_MoveToPosition(m_pOwner, posSw));
}

Możesz zobaczyć roboty Ravena poruszające się po drzwiach, uruchamiając Raven i ładując mapę Raven_DM1_With_Doors.map. Używa rzadkiego wykresu nawigacyjnego, dzięki czemu możesz wyraźnie zobaczyć, jak działa cel NegotiateDoor.

Kolejkowanie poleceń

Gry strategiczne w czasie rzeczywistym ogromnie się skomplikowały w ciągu ostatnich kilku lat. Zwiększono nie tylko liczbę NPC, którą gracz jest w stanie kontrolować, ale także liczbę poleceń, które może poinstruować NPC. Wymagało to kilku ulepszeń interfejsu użytkownika, jednym z nich jest zdolność gracza do kolejkowania rozkazów NPC - coś, co stało się znane jako kolejkowanie poleceń (lub budowanie w kolejce). Jednym z pierwszych zastosowań kolejkowania było ustawienie punktów trasy, którymi NPC podążałby ścieżką. Aby to zrobić, gracz przytrzymuje klawisz, klikając mapę, aby utworzyć serię punktów trasy. NPC przechowuje punkty w strukturze danych FIFO (pierwsze wejście, pierwsze wyjście) i podąża za nimi w kolejności, zatrzymując się, gdy kolejka jest pusta.



Projektanci szybko zdali sobie sprawę, że przy niewielkich modyfikacjach użytkownik może również przypisać punkty patrolowe NPC. Jeśli punkty drogi zostaną przypisane przez gracza jako punkty patrolowania (przytrzymując inny klawisz podczas klikania), są one zwracane na tył kolejki, gdy dotrze do nich NPC. W ten sposób NPC będzie nieskończenie krążył przez punkty patrolowania, dopóki nie otrzyma instrukcji.



Bardzo krótko po tej innowacji zdano sobie sprawę, że wektory pozycjonujące mogą nie tylko stać w kolejce, ale także każdy rodzaj polecenia. Następnie, zamiast wydawać tylko jedno rozkaz na raz, po prostu przytrzymując klawisz podczas wybierania rozkazów, gracz może kolejkować wiele poleceń. Na przykład NPC może zostać poinstruowany, aby zebrał trochę złota, a następnie zbudował koszary, a następnie zaatakował wrogą jednostkę. Po wydaniu rozkazów gracz może skoncentrować się na innym miejscu, mając pewność, że NPC wykona rozkazy. Kolejkowanie poleceń znacznie skraca czas, jaki gracz musi poświęcić na mikrozarządzanie i zwiększa czas dostępny na przyjemniejsze aspekty gry. Dlatego stał się nieodzowną cechą gatunku RTS. Na szczęście dzięki złożonej architekturze celów ta funkcja jest niezwykle łatwa do wdrożenia. Wszystko, co musisz zrobić, to pozwolić klientom dodawać cele z tyłu listy podrzędnej oprócz frontu. Otóż to! Pięć minut pracy i kolejka poleceń. Możesz obserwować kolejkowanie poleceń w akcji w Raven. Niestety, w przeciwieństwie do RTS, Raven nie ma wielu interesujących poleceń, ale możesz ustawić w kolejce wiele celów MoveToPosition, przytrzymując klawisz "Q" podczas klikania mapy. Zaimplementowałem to, dodając metodę QueueGoal_ MoveToPosition do Goal_Think i trochę dodatkowego kodu do wywołania tej metody, jeśli gracz kliknie mapę, przytrzymując odpowiedni klawisz. Jeśli zwolnisz klawisz "Q" i ponownie klikniesz mapę prawym przyciskiem myszy, kolejka zostanie wyczyszczona i zastąpiona jednym nowym celem. Byłoby to jednak równie łatwe do wdrożenia z dowolnym wybranym przez ciebie celem, ponieważ kolejkowanie samo się zajmuje.

Korzystanie z zachowania kolejki do skryptu

Kolejną zaletą przekształcenia listy podrzędnej w kolejkę jest to, że umożliwia ona pisanie scenariuszy akcji liniowych bez większych trudności. Na przykład możesz utworzyć takie zachowanie:

•  Gracz wchodzi do pokoju i pojawia się widmowa postać, która unosi się do skrzyni ustawionej w rogu, otwiera skrzynię, wyjmuje zwój, płynie z powrotem do gracza i wręcza mu zwój.
•  Gracz wchodzi do holu hotelowego ze szklanym sufitem. Po krótkim czasie spędzonym w pokoju słychać helikopter. Kilka sekund później sufit rozbija się na milion odłamków i widać, jak kilku uzbrojonych mężczyzn w czerni zjeżdża z helikoptera. Kiedy uderzą o podłogę, rozpraszają się, każdy znajduje osłonę w osobnym miejscu i zaczynają strzelać do gracza.
•  Gracz znajduje starą mosiężną lampę. Pociera ją i pojawia się dżin. Dżin mówi "Pójdź za mną" i prowadzi gracza do wejścia do tajnego tunelu, gdzie natychmiast znika w kłębach dymu.

Aby to zrobić, musisz upewnić się, że zdefiniowałeś cel dla każdego kroku sekwencji oraz wyzwalacze wymagane do aktywacji skryptu. Ponadto musisz udostępnić odpowiedni kod C ++ w swoim języku skryptowym. Na przykład, aby napisać trzeci przykład z poprzedniej listy w Lua, musisz wykonać te zadania.

1. Utwórz trzy cele:

* Cel SayPhrase, który spowoduje wyświetlenie tekstu na ekranie określony czas.
* Cel LeadPlayerToPosition. Jest to podobne do celu MoveTo-Position widocznego w Raven, z tym że ma dodatkową logikę, aby upewnić się, że dżin nie traci z oczu gracza podczas prowadzenia go do tajnego tunelu.
* Cel VanishInPuffOfSmoke, który usunie instancję dżina ze świata gry i pozostawi po sobie kłębek dymu.

2. Utwórz wyzwalacz, który jest aktywowany, gdy gracz wykonuje akcję "pocierać" na konkretnym obiekcie "lampy". Po aktywacji spust powinien wywołać odpowiednią funkcję Lua.
3. Odsłoń Luę odpowiednie części architektury gry. Idealnie byłoby napisać skrypt, który wygląda trochę tak:

function AddGenieTourGuide(LampPos, TunnelPos, Player)
--create an instance of a genie at the position of the lamp
genie = CreateGenie(LampPos)
--first welcome the player, text stays on screen for 2 seconds
genie:SayPhrase("Welcome oh great "..Player:GetName().. "!", 2)
--order the player to follow the genie. text stays on screen for
--3 seconds
genie:SayPhrase("Follow me for your three wishes", 3)
--lead the player to the tunnel entrance
genie:LeadPlayerToPosition(Player, TunnelPos)
--vanish
genie:VanishInPuffOfSmoke
end

Dlatego musisz ujawnić metodę C ++, która tworzy dżina, dodaje go do świata gry i zwraca wskaźnik do niego, a także metody dodawania odpowiednich celów do kolejki bramek dżina.

Podsumowując

Przedstawiono elastyczną i wydajną architekturę agentów zorientowanych na cele. Nauczyłeś się, jak zachowanie agenta może być modelowane jako zestaw strategii wysokiego poziomu, z których każda składa się z zagnieżdżonej hierarchii celów złożonych i atomowych. Dowiedziałeś się także, jak agenci mogą rozstrzygać między tymi strategiami, aby wybrać najbardziej odpowiednią do realizacji w danym stanie gry. Chociaż łączy wiele podobieństw, ten typ architektury jest znacznie bardziej wyrafinowany niż projekt oparty na stanie i wymaga pewnej wprawy, zanim będzie można go bezpiecznie używać. Jak zwykle zakończę ten rozdział kilkoma pomysłami, z którymi możesz się pobawić, aby pomóc Ci lepiej zrozumieć.

Praktyka czyni mistrza

1. Przyzwoici ludzcy gracze FPS odczuwają "wyczucie", kiedy dany przedmiot zaraz się odrodzi. Rzeczywiście, niektórzy gracze w deathmatch nie mogą bawić się budzikiem z boku swojego monitora! Jednak roboty Raven obecnie nie mają pojęcia, kiedy przedmiot się zbliża odrodzić się. Utwórz warunek zakończenia wyszukiwania Dijkstry algorytm, który oblicza, czy nieaktywny (niewidoczny) typ przedmiotu odrodzi się w czasie potrzebnym do jego osiągnięcia, umożliwiając w ten sposób botowi go wyprzedzić.
2. Boty Raven nie mają strategii obronnej. W tej chwili próbują po prostu wytropić typ przedmiotu, jeśli nie czują się wystarczająco silni, aby zaatakować i mają nadzieję, że to wyprowadzi ich z drogi. Gdy zobaczysz wersję demonstracyjną, zauważysz, że takie zachowanie często wpędza ich w kłopoty, ponieważ nie próbują unikać strzałów podczas ścigania przedmiotu. Napisz logikę i wszelkie dodatkowe cele wymagane, aby umożliwić botom wykrywanie takich sytuacji i unikanie ich z boku na bok, wciąż ścigając typ przedmiotu.
3. Dodaj skrypt Raven do postaci i stwórz jedną lub dwie sekwencje skryptowe. To świetne ćwiczenie, które wzmocni wiele rzeczy, których nauczyłeś się do tej pory. Nie musisz pisać scenariuszy w żaden skomplikowany sposób. Na przykład, możesz zrobić coś podobnego do opisanego wcześniej przykładu dżina. Utwórz skrypt z punktu widzenia gracza: natura Kruka, ale wiesz, co mam na myśli), zatrzymuje się przed graczem, mówi "Follow Me", a następnie prowadzi gracza w losowe miejsce.