Raven : OmówienieProgramownie A.I. Gier

Raven : Omówienie

Tu znajdziesz przegląd gry o nazwie Raven. Raven zostanie wykorzystany jako ramy, w których zostaną zaimplementowane wszystkie pozostałe techniki (oprócz większości tych, których już się nauczyłeś). Najpierw poświęcisz trochę czasu na zapoznanie się z architekturą gry, a następnie podsumowanie elementów tworzących sztuczną inteligencję.

Gra

Raven to proste, ale złożone środowisko gry 2D wystarczająco, aby odpowiednio zademonstrować techniki opisane przez nas. Typowa mapa Ravena składa się z szeregu pokoi i korytarzy, kilku punktów odradzania, z których generowane są agenty ("boty") oraz przedmiotów, takich jak pakiety zdrowia lub broń, które boty mogą podnieść i użyć.



Rozgrywka jest podobna do deathmatchu w stylu Quake. Rozpoczynając grę, odradza się kilka botów kontrolowanych przez AI, które biegają po mapie, próbując wykonać jak najwięcej zabójstw i zbierając broń i zdrowie w razie potrzeby. Jeśli bot zostanie zabity, natychmiast odradza się z pełnym zdrowiem z losowego punktu odradzania, a pozycja, w której został zabity, jest oznaczona "grobem" na kilka sekund. Bota można wybrać, klikając go prawym przyciskiem myszy. Po wybraniu wokół bota zostanie narysowane czerwone kółko, a na ekranie zostaną wyświetlone dodatkowe informacje związane ze sztuczną inteligencją, w zależności od opcji ustawionych w menu. Kliknij wybranego bota prawym przyciskiem myszy, a będziesz go "posiadał" - będzie on pod twoją kontrolą. Opętany robot jest otoczony niebieskim kolorem i można go przenieść, klikając prawym przyciskiem myszy tę część mapy, do której ma się udać. AI nawigacji bota automatycznie zapewni pomoc, planując najkrótszą drogę do wybranej lokalizacji. Celem bota kontroluje mysz. Ponieważ bot może celować niezależnie od swojego kierunku ruchu, opętany bot zawsze będzie zwrócony w kierunku kursora myszy. Kliknięcie lewym przyciskiem spowoduje wystrzelenie bieżącej broni bota w kierunku kursora myszy. (W przypadku broni dystansowej, takiej jak wyrzutnia rakiet, celem jest pozycja kursora.) Możesz zmienić broń (pod warunkiem, że bot ma więcej niż jedną), naciskając klawisze "1" na "4". Bot jest zwalniany z twojej kontroli przez kliknięcie prawym przyciskiem myszy innego bota lub przez naciśnięcie klawisza "X".

UWAGA. Chociaż podczas gry możesz wyraźnie zobaczyć wszystkie inne boty, sztuczna inteligencja każdego bota jest w stanie zobaczyć tylko te boty, które znajdują się w jego polu widzenia i nie są zasłonięte przez ściany. To sprawia, że konstrukcja AI jest znacznie bardziej interesująca.

Przegląd architektury gry

W tej sekcji przeanalizujemy kluczowe klasy, które składają się na środowisko gry. Rysunek pokazuje przegląd wzajemnych zależności między obiektami wysokiego poziomu.

Klasa Raven_Game

Klasa Raven_Game jest centrum projektu. Ta klasa posiada instancję Raven_Map, pojemnik botów i pojemnik wszelkich aktywnych pocisków (rakiety, ślimaki itp.). Między innymi klasa Raven_Game ma metody ładowania map i powiązanych z nimi wykresów nawigacyjnych, aktualizowania i renderowania elementów gry i geometrii, wysyłania zapytań do świata za pomocą żądań widoczności oraz obsługi danych wejściowych użytkownika. Poniżej znajduje się częściowa lista deklaracji klasy Raven_Game. Rzuć okiem, aby się z tym zapoznać.

class Raven_Game
{
private:
Raven_Map* m_pMap;
std::list m_Bots;
// użytkownik może wybrać bota do sterowania ręcznego. Ten członek
// trzyma wskaźnik do tego bota
Raven_Bot* m_pSelectedBot;
// ta lista zawiera wszystkie aktywne pociski (ślimaki, rakiety,
// śrutówki itp.)
std::list m_Projectiles;
/* WYJĄTKOWO SZCZEGÓŁOWE POMINIĘCIE DLA JASNOŚCI */
public:
// Podejrzani
void Render();
void Update();
// ładuje środowisko z pliku
bool LoadMap(const std::string& FileName);
// zwraca true, jeśli bot wielkości BoundingRadius nie może przejść z A do B
// bez wpadania na geometrię świata
bool isPathObstructed(Vector2D A, Vector2D B, double BoundingRadius = 0)const;
// zwraca wektor wskaźników do botów w FOV danego bota
std::vector GetAllBotsInFOV(const Raven_Bot* pBot)const;
// zwraca true, jeśli drugi bot nie jest zasłonięty przez ściany i na polu
// widok pierwszego.
bool isSecondVisibleToFirst(const Raven_Bot* pFirst,
const Raven_Bot* pSecond)const;
/* WYJĄTKOWO SZCZEGÓŁOWE POMINIĘCIE DLA JASNOŚCI */
};

WSKAZÓWKA. Należy pamiętać, że GetAllBotsInFOV nie ogranicza liczby botów zwracanych przez metodę. Nie jest to konieczne w przypadku wersji demonstracyjnej, ale w przypadku gier, w których często pojawiają się dziesiątki, a nawet setki innych agentów, dobrym pomysłem jest ograniczenie liczby do n najbliższej, jaką agent może zobaczyć.

Mapa Raven

Klasa Raven_Map posiada kontenery wszystkich obiektów tworzących geometrię świata gry - ściany, wyzwalacze, punkty odradzania itp. - a także instancję grafu nawigacyjnego mapy. Te elementy są tworzone po otwarciu pliku w formacie mapy Ravena. Po uruchomieniu Raven domyślna mapa (Raven_DM1) i odpowiadający jej wykres nawigacyjny są odczytywane z pliku. Liczba losowych botów Raven jest następnie tworzona w losowo wybranych, niezajętych punktach odradzania.

UWAGA. Parametry Raven są przechowywane w pliku skryptu Lua params.lua. Dostęp do skryptów jest wygodny dzięki zastosowaniu klasy singletonowej Raven_Scriptor, która z kolei wywodzi się z klasy Scriptor. Ta klasa jest po prostu enkapsulacją wszystkich powszechnie używanych metod dostępu do zmiennych Lua, takich jak LuaPopNumber i LuaPopString. Jeśli potrzebujesz dodatkowych wyjaśnień, sprawdź plik common / script / Scriptor.h. Oto częściowa lista deklaracji Raven_Map.

class Raven_Map
{
public:
typedef NavGraphNode*> GraphNode;
typedef SparseGraph NavGraph;
typedef TriggerSystem > Trigger_System;
private:
// ściany, które składają się na architekturę bieżącej mapy
std::vector m_Walls;
// wyzwalacze to obiekty, które definiują region przestrzeni. Kiedy bot Ravena
// wchodzi w ten obszar, "wyzwala" zdarzenie. To wydarzenie może być cokolwiek
// od zwiększenia zdrowia bota do otwarcia drzwi lub poproszenia o windę.
Trigger_System m_TriggerSystem;
// zawiera wiele pozycji odradzania. Kiedy instancja bota jest tworzona
// pojawi się w losowo wybranym punkcie wybranym z tego wektora
std::vector m_SpawnPoints;
// towarzyszący wykres nawigacyjny tej mapy
NavGraph* m_pNavGraph;
/* DODATKOWE SZCZEGÓŁY POMINIĘTO */
public:
Raven_Map();
~Raven_Map();
void Render();
// DODATKOWE SZCZEGÓŁY POMINIĘTO
bool LoadMap(const std::string& FileName);
void AddSoundTrigger(Raven_Bot* pSoundSource, double range);
double CalculateCostToTravelBetweenNodes(unsigned int nd1,
unsigned int nd2)const;
void UpdateTriggerSystem(std::list& bots);
/* DODATKOWE SZCZEGÓŁY POMINIĘTO */
};

Broń Raven

Dostępne są cztery bronie.:

•  Blaster: Jest to domyślna broń bota. Wystrzeliwuje zielone strzały elektryczności z prędkością trzech na sekundę. Ta broń automatycznie się ładuje, więc nigdy nie zabraknie jej amunicji. Zadaje tylko jedną jednostkę obrażeń na trafienie.
•  Strzelba: Strzelbę można wystrzelić tylko raz na sekundę. Każdy nabój zawiera dziesięć kulek, które rozchodzą się po opuszczeniu broni. Oznacza to, że strzelba jest znacznie bardziej celna i zabójcza na bliskich średnich odległościach niż na dalekim dystansie. Każda kula strzału zadaje jedną jednostkę obrażeń.
•  Wyrzutnia rakiet: Wyrzutnia rakiet ma prędkość wystrzeliwania 1,5 rakiety na sekundę. Rakiety lecą dość wolno i eksplodują przy zderzeniu. Każda istota złapana w promień wybuchu rakiety odniesie dziesięć obrażeń. Ponieważ rakiety poruszają się dość wolno i można je łatwo uniknąć, wyrzutnię rakiet najlepiej stosować jako broń średniego zasięgu.
•  The Railgun: Railgun strzela pociskami w tempie 1 na sekundę. Railgun podróżuje niemal natychmiast do celu, tworząc tę broń idealną do strzelania z dystansu i strzelania z dużej odległości. (Strzały Railguna są zatrzymywane tylko przez ściany, więc jeśli trafi kilka botów stojących w linii, Railgun przeniknie wszystkie).

Bot Raven rozpoczyna każdą grę z niszczycielem i zdobywa inne rodzaje broni, lokalizując je na mapie i przebiegając nad nimi. Jeśli bot ma już broń typu, na którą przejechał, do ekwipunku dodaje się tylko amunicję do broni. Każdy z rodzajów broni dziedziczy po klasie Raven_Weapon. Publiczny interfejs klasy wygląda następująco:

class Raven_Weapon
{
public:
Raven_Weapon(unsigned int TypeOfGun
unsigned int DefaultNumRounds,
unsigned int MaxRoundsCarried,
double RateOfFire,
double IdealRange,
double ProjectileSpeed,
Raven_Bot* OwnerOfGun);
virtual ~Raven_Weapon(){}
// ta metoda celuje bronią w dany cel, obracając jej broń w
// kierunku zwróconym przez właściciela (ograniczony przez szybkość obrotu bota). To
// zwraca true, jeśli broń jest skierowana bezpośrednio w cel
bool AimAt(Vector2D target)const;
// wyrzuca pocisk z broni w danej pozycji docelowej
// (pod warunkiem, że broń jest gotowa do rozładowania … każda broń ma swoją
// własna szybkostrzelność)
virtual void ShootAt(Vector2D target) = 0;
// każda broń ma swój własny kształt i kolor
virtual void Render() = 0;
// ta metoda zwraca wartość reprezentującą celowość użycia
//broń. Jest to wykorzystywane przez AI do wyboru najbardziej odpowiedniej broni do gry // w obecnej sytuacji bota. Ta wartość jest obliczana przy użyciu logiki rozmytej.
virtual double GetDesirability(double DistToTarget)=0;
// zwraca maksymalną prędkość pocisku wystrzeliwanego przez tę broń
double GetProjectileSpeed()const;
int NumRoundsRemaining()const;
void DecrementNumRounds();
void IncrementRounds(int num);
// zwraca wyliczoną wartość reprezentującą typ pistoletu
unsigned int GetTypeOfGun()const;
};

Wyzwalacze

Wyzwalacz to obiekt, który definiuje warunek, który po spełnieniu przez agenta generuje akcję (jest wyzwalany). Wiele wyzwalaczy wykorzystywanych w grach komercyjnych ma tę właściwość, że są one wyzwalane, gdy jednostka gry wchodzi w obszar wyzwalania: z góry określony obszar przestrzeni, który jest dołączony do wyzwalacza. Regiony te mogą mieć dowolny dowolny kształt, ale zwykle są okrągłe lub prostokątne w środowiskach 2D oraz kuliste, sześcienne lub cylindryczne w środowiskach 3D. Wyzwalacze są niezwykle przydatnym narzędziem zarówno dla projektantów gier, jak i programistów AI. Możesz ich używać do tworzenia różnego rodzaju zdarzeń i zachowań. Na przykład wyzwalacze ułatwiają wykonywanie takich czynności:

•  Postać z gry wędruje ponurym korytarzem. Wchodzi na wrażliwą na nacisk płytkę i uruchamia mechanizm, który wbija mu czterdzieści gwoździ w jamę brzuszną. (To jeden z najbardziej oczywistych zastosowań wyzwalacza).
•  Zastrzelisz strażnika. Kiedy umiera, do gry dodawany jest wyzwalacz, który ostrzega innych strażników przed ciałem, jeśli wędrują w określonej odległości od niego.
•  Postać z gry strzela z pistoletu. Do gry dodano wyzwalacz, który ostrzega każdą inną postać w określonym promieniu hałasu.
•  Dźwignia na ścianie jest realizowana jako spust. Jeśli agent go pociągnie, otwiera drzwi.
•  Zaimplementowałeś układankę w jednym rogu pokoju, ale uważasz, że kilku graczy będzie miało problem z jej rozwiązaniem. Jako pomoc możesz dołączyć wyzwalacz do układanki, która aktywuje się, jeśli gracz stanie blisko niej więcej niż trzy razy. Po aktywacji spust wyzwala jakiś system podpowiedzi, aby pomóc graczowi rozwiązać zagadkę.
•  Troll uderza kolcem ogona w głowę kolczastym kijem. Ogr ucieka, ale krwawi. Gdy każda kropla krwi spada na ziemię, pozostawia wyzwalacz. Troll może następnie ścigać ogra, podążając śladem krwi.

Raven korzysta z kilku rodzajów wyzwalaczy. Hierarchię klas podano na rysunku



Warto poświęcić trochę czasu na szczegółowe zbadanie każdego z tych obiektów. Najpierw rzućmy okiem na klasę TriggerRegion.

TriggerRegion

Klasa TriggerRegion definiuje metodę isTouching, którą muszą implementować wszystkie regiony wyzwalające. Funkcja isTouching zwraca wartość true, jeśli element o danym rozmiarze i pozycji zachodzi na region wyzwalający. Każdy typ wyzwalacza ma instancję TriggerRegion i wykorzystuje metodę isTouching w celu ustalenia, kiedy należy ją uruchomić. Oto jego deklaracja:

class TriggerRegion { public: virtual ~TriggerRegion(){} virtual bool isTouching(Vector2D EntityPos, double EntityRadius)const = 0; }; A oto przykład konkretnego regionu wyzwalającego, który definiuje okrągły obszar przestrzeni:

class TriggerRegion_Circle : public TriggerRegion
{
private:
// centrum regionu
Vector2D m_vPos;
// promień regionu
double m_dRadius;
public:
TriggerRegion_Circle(Vector2D pos,
double radius):m_dRadius(radius),
m_vPos(pos)
{}
bool isTouching(Vector2D pos, double EntityRadius)const
{
// odległości obliczone w przestrzeni do kwadratu
return Vec2DDistanceSq(m_vPos, pos)
(EntityRadius + m_dRadius)*(EntityRadius + m_dRadius);
}
};

Jak widać, metoda isTouching zwróci wartość true, gdy encja pokryje się z okręgiem zdefiniowanym przez region.

Trigger

Klasa Trigger jest klasą podstawową, z której pochodzą wszystkie inne typy wyzwalaczy. Ma dwie metody, które muszą zostać zaimplementowane przez wszystkie klasy potomne: Try and Update. Metody te nazywane są każdą iteracją pętli aktualizacji gry. Aktualizacja aktualizuje stan wewnętrzny wyzwalacza (jeśli istnieje). Spróbuj przetestować, czy jednostka przekazana do niego jako parametr nakłada się na region wyzwalający i odpowiednio podejmuje działania. Deklaracja wyzwalacza jest prosta. Oto listing:

template < class entity_type >
class Trigger : public BaseGameEntity
{
private:
// Każdy wyzwalacz ma region wyzwalacza. Jeśli jednostka jest objęta tym
// regionem wyzwalacz jest aktywowany
TriggerRegion* m_pRegionOfInfluence;
// jeśli to prawda, wyzwalacz zostanie usunięty z gry na stronie // następna aktualizacja
bool m_bRemoveFromGame;
// wygodnie jest móc dezaktywować niektóre typy wyzwalaczy
// na wydarzeniu. Dlatego wyzwalacz można uruchomić tylko wtedy, gdy ta
// wartość jest prawdziwa (wyzwalacze odradzania dobrze to wykorzystują)
bool m_bActive;
// niektóre typy wyzwalaczy są powiązane z węzłem graficznym. Umożliwia to
// komponent AI znajdujący ścieżkę do przeszukiwania grafu nawigacyjnego w poszukiwaniu określonego
// typu wyzwalacza.
int m_iGraphNodeIndex;
protected:
void SetGraphNodeIndex(int idx){m_iGraphNodeIndex = idx;}
void SetToBeRemovedFromGame(){m_bRemoveFromGame = true;}
void SetInactive(){m_bActive = false;}
void SetActive(){m_bActive = true;}
// zwraca true, jeśli jednostka podana przez pozycję i promień ograniczający to
// nakładanie się na region wyzwalający
bool isTouchingTrigger(Vector2D EntityPos, double EntityRadius)const;
// klasy potomne używają jednej z tych metod, aby dodać region wyzwalający
void AddCircularTriggerRegion(Vector2D center, double radius);
void AddRectangularTriggerRegion(Vector2D TopLeft, Vector2D BottomRight);
public:
Trigger(unsigned int id);
virtual ~Trigger();
// kiedy to się nazywa, wyzwalacz określa, czy jednostka znajduje się w
// obszar wpływu wyzwalacza. Jeśli tak, wyzwalaczem będzie
// wyzwalane i zostaną podjęte odpowiednie działania.
virtual void Try(entity_type*) = 0;
// wywołał każdy krok aktualizacji gry. Ta metoda aktualizuje wszelkie wewnętrzne
// podać, że wyzwalacz może mieć
virtual void Update() = 0;
int GraphNodeIndex()const{return m_iGraphNodeIndex;}
bool isToBeRemoved()const{return m_bRemoveFromGame;}
bool isActive(){return m_bActive;}
};

Wyzwalacze mają zmienną składową m_iGraphNodeIndex, ponieważ czasami przydatne jest łączenie niektórych typów wyzwalaczy z węzłem wykresu nawigacyjnego. Na przykład w Raven typy przedmiotów, takie jak zdrowie i broń, są implementowane jako specjalny typ wyzwalacza zwanego wyzwalaczem dawcy. Ponieważ wyzwalacze dawcy są powiązane z węzłem grafu, narzędzie planowania ścieżki może łatwo przeszukiwać nawigator pod kątem określonego typu elementu, na przykład najbliższej instancji elementu zdrowia, gdy bot ma mało zdrowia.

Odradzanie wyzwalaczy

Klasa Trigger_Respawning wywodzi się z Trigger i definiuje wyzwalacz, który staje się nieaktywny przez pewien okres czasu po tym, jak zostanie wyzwolony przez byt. Ten rodzaj wyzwalacza jest wykorzystywany w Raven do implementacji typów przedmiotów, które bot może "podnieść", takich jak zdrowie lub broń. W ten sposób przedmiot może zostać odrodzony (ponownie wyświetlony) w pierwotnej lokalizacji przez pewien czas po jego odebraniu.

template < class entity_type >
class Trigger_Respawning : public Trigger< entity_type >
{
protected:
// Gdy bot znajdzie się w obszarze wpływu tego wyzwalacza, zostaje on wyzwolony
// ale następnie staje się nieaktywny na określony czas. Te wartości
// kontroluj czas wymagany do przejścia wyzwalacza w
// znów aktywny.
int m_iNumUpdatesBetweenRespawns;
int m_iNumUpdatesRemainingUntilRespawn;
// ustawia wyzwalacz na nieaktywny dla m_iNumUpdatesBetweenRespawns
// zaktualizuj kroki
void Deactivate()
{
SetInactive();
m_iNumUpdatesRemainingUntilRespawn = m_iNumUpdatesBetweenRespawns;
}
public:
Trigger_Respawning(int id);
virtual ~Trigger_Respawning();
// do wdrożenia przez klasy potomne
virtual void Try(entity_type*) = 0;
// nazywa się to każdym kliknięciem gry, aby zaktualizować wewnętrzny stan wyzwalacza
virtual void Update()
{
if ( (--m_iNumUpdatesRemainingUntilRespawn <= 0) && !isActive())
{
SetActive();
}
}
void SetRespawnDelay(unsigned int numTicks);
};

UWAGA. Ponieważ Raven wykorzystuje stałą częstotliwość aktualizacji, wyzwalacze używają kroków aktualizacji jako reprezentacji czasu (każdy krok aktualizacji to jedna jednostka czasu). Jeśli jednak zdecydujesz się zaimplementować zmienną częstotliwość aktualizacji systemu wyzwalacza, pamiętaj o zaprogramowaniu metody aktualizacji wyzwalacza tak, aby korzystała z różnicy czasu między aktualizacjami.

Wyzwalacze dawców

Przedmioty związane ze zdrowiem i bronią w Raven są realizowane za pomocą rodzaju wyzwalacza zwanego dawcą. Ilekroć jednostka wkracza w region spustowy dawcy-wyzwalacza, "otrzymuje" odpowiedni przedmiot. Dawcy zdrowia w oczywisty sposób zwiększają zdrowie bota, a dawcy broni dostarczają botowi instancję typu broni, którą reprezentują. Innym sposobem patrzenia na to jest to, że bot "podnosi" przedmiot reprezentowany przez spust. Aby umożliwić odrodzenie się przedmiotów zdrowia i broni po ich podniesieniu przez bota, wyzwalacze dziedziczą po klasie Trigger_Respawning.

Dawcy broni

Oto deklaracja klasy Trigger_WeaponGiver.

class Trigger_WeaponGiver : public Trigger_Respawning< Raven_Bot >
{
private:
/* ODATKOWE SZCZEGÓŁY POMINIĘTE */
public:
// ten typ wyzwalacza jest tworzony podczas odczytu pliku mapy
Trigger_WeaponGiver(std::ifstream& datafile);
// jeśli zostanie wyzwolony, ten wyzwalacz wywoła metodę PickupWeapon w
//nerw. PickupWeapon uruchomi broń odpowiedniego typu.
void Try(Raven_Bot*);
// rysuje symbol przedstawiający rodzaj broni w miejscu spustu
void Render();
/* ODATKOWE SZCZEGÓŁY POMINIĘTE */
};

Metoda Try jest zaimplementowana w następujący sposób:

{
if (isActive() && isTouchingTrigger(pBot->Pos(), pBot->BRadius()))
{
pBot->PickupWeapon( EntityType() );
Deactivate();
}
}

Jeśli wyzwalacz jest aktywny, a bot nakłada się na region wyzwalacza, wywoływana jest metoda Raven_Bot :: PickupWeapon. Ta metoda tworzy instancję broni danego typu i dodaje ją (lub amunicję tylko wtedy, gdy jest już w posiadaniu) do ekwipunku bota. Wreszcie logika dezaktywuje wyzwalacz. Wyzwalacz pozostanie dezaktywowany przez określony czas, zanim zostanie ponownie aktywowany. Po dezaktywacji wyzwalacz nie będzie renderowany.

Dawcy Zdrowia

Wyzwalacze zdrowia są wdrażane bardzo podobnie.

void Trigger_HealthGiver::Try(Raven_Bot* pBot)
{
if (isActive() && isTouchingTrigger(pBot->Pos(), pBot->BRadius()))
{
pBot->IncreaseHealth(m_iHealthGiven);
Deactivate();
}
}

Jak widać, jest to prawie taki sam kod, jak poprzednio, z tym wyjątkiem, że tym razem zdrowie bota wyzwalającego jest zwiększone.

Wyzwalacze o ograniczonym okresie użytkowania

Czasami potrzebny jest wyzwalacz o ustalonej długości życia - taki, który pozostaje w środowisku przez pewną liczbę kroków aktualizacji, zanim zostanie automatycznie usunięty. Trigger_LimitedLifetime zapewnia taki obiekt.

template < class entity_type >
class Trigger_LimitedLifetime : public Trigger
{
protected:
// czas życia tego wyzwalacza w krokach aktualizacji
int m_iLifetime;
public:
Trigger_LimitedLifetime(int lifetime);
virtual ~Trigger_LimitedLifetime(){}
// dzieci z tej klasy powinny zawsze upewnić się, że jest to wywoływane z poziomu
// własną metodę aktualizacji
virtual void Update()
{
// jeśli upłynie okres ważności licznika, ustaw ten wyzwalacz, aby został usunięty z
// gry
if (--m_iLifetime <= 0)

{
SetToBeRemovedFromGame();
}
}
// do wdrożenia przez klasy potomne
virtual void Try(entity_type*) = 0;
};

Dźwiękowy wyzwalacz powiadomienia jest dobrym przykładem tego, w jaki sposób używane są wyzwalacze o ograniczonej długości życia.

Wyzwalacze powiadomień dźwiękowych

Ten typ wyzwalacza jest używany w Raven do powiadamiania innych jednostek o dźwiękach wystrzału. Za każdym razem, gdy strzelana jest broń, tworzony jest Trigger_SoundNotify i pozostawiany w miejscu strzału. Ten typ wyzwalacza ma okrągły obszar wyzwalania o promieniu proporcjonalnym do głośności broni. Wywodzi się z Trigger_LimitedLifetime i ma być aktywny tylko dla jednej aktualizacji wyzwalacza bota. Kiedy bot wyzwala ten typ wyzwalacza, wysyła do niego komunikat informujący, który bot wydał dźwięk.

class Trigger_SoundNotify : public Trigger_LimitedLifetime< Raven_Bot >
{
private:
// wskaźnik do bota, który wydał ten dźwięk
Raven_Bot* m_pSoundSource;
public:
Trigger_SoundNotify(Raven_Bot* source, double range);
void Trigger_SoundNotify::Try(Raven_Bot* pBot)
{
// czy ten bot znajduje się w zasięgu tego dźwięku
if (isTouchingTrigger(pBot->Pos(), pBot->BRadius()))
{
Dispatcher->DispatchMsg(SEND_MSG_IMMEDIATELY,
SENDER_ID_IRRELEVANT,
pBot->ID(),
Msg_GunshotSound,
m_pSoundSource);
}
}
};

Zarządzanie wyzwalaczami: klasa TriggerSystem

Klasa TriggerSystem jest odpowiedzialna za zarządzanie kolekcją wyzwalaczy. Klasa Raven_Map jest właścicielem instancji TriggerSystem i rejestruje każdy wyzwalacz w systemie podczas jego tworzenia. System wyzwalaczy zajmuje się aktualizacją i renderowaniem wszystkich zarejestrowanych wyzwalaczy i usuwa wyzwalacze w miarę upływu ich okresu użytkowania. Oto źródło TriggerSystem. Wymieniłem ciała metod UpdateTrigger i TryTrigger, abyś mógł dokładnie zobaczyć, jak działają.

template < class trigger_type >
class TriggerSystem
{
public:
typedef std::list TriggerList;
private:
// pojemnik wszystkich wyzwalaczy
TriggerList m_Triggers;
// ta metoda dokonuje iteracji przez wszystkie wyzwalacze obecne w systemie i
// wywołuje metodę aktualizacji, aby ich stan wewnętrzny mógł być
// zaktualizowany w razie potrzeby. Usuwa również wszelkie wyzwalacze z systemu, który // ustawia wartość pola m_bRemoveFromGame na true.
void UpdateTriggers()
{
TriggerList::iterator curTrg = m_Triggers.begin();
while (curTrg != m_Triggers.end())
{
// usunąć spust, jeśli nie żyje
if ((*curTrg)->isToBeRemoved())
{
delete *curTrg;
curTrg = m_Triggers.erase(curTrg);
}
else
{
// zaktualizuj ten wyzwalacz
(*curTrg)->Update();
++curTrg;
}
}
}
// ta metoda iteruje przez kontener jednostek przekazywany jako
// parametr i przekazuje każdy z nich do metody Try każdego podanego wyzwalacza // jednostka żyje i jest gotowa na aktualizację wyzwalacza.
template
void TryTriggers(ContainerOfEntities& entities)
{
// przetestuj każdą jednostkę pod kątem wyzwalaczy
ContainerOfEntities::iterator curEnt = entities.begin();
for (curEnt; curEnt != entities.end(); ++curEnt)
{
// jednostka musi być gotowa na następną aktualizację wyzwalacza i musi być
// żywy, zanim zostanie przetestowany dla każdego wyzwalacza.
if ((*curEnt)->isReadyForTriggerUpdate() && (*curEnt)->isAlive())
{
TriggerList::const_iterator curTrg;
for (curTrg = m_Triggers.begin(); curTrg != m_Triggers.end(); ++curTrg)
{
(*curTrg)->Try(*curEnt);
}
}
}
}
public:
˜TriggerSystem()
{
Clear();
}
// powoduje to usunięcie wszystkich bieżących wyzwalaczy i opróżnia listę wyzwalaczy
void Clear();
// Ta metoda powinna być nazywana na każdym etapie aktualizacji gry. Najpierw będzie // zaktualizuj wewnętrzny stan wyzwalaczy, a następnie wypróbuj każdą jednostkę przeciwko
// każdy aktywny wyzwalacz, aby sprawdzić, czy należy go uruchomić.
template
void Update(ContainerOfEntities& entities)
{
UpdateTriggers();
TryTriggers(entities);
}
// służy do rejestrowania wyzwalaczy w TriggerSystem (TriggerSystem
// zajmie się uporządkowaniem pamięci używanej przez wyzwalacz)
void Register(trigger_type* trigger);
// niektóre wyzwalacze muszą zostać zrenderowane (jak na przykład wyzwalacze)
void Render();
const TriggerList& GetTriggers()const{return m_Triggers;}
};

Okej, to powinno wystarczyć do wglądu w ramy gry Raven. Przyjrzyjmy się teraz projektowi sztucznej inteligencji bota.

Zagadnienia dotyczące projektowania AI

Do projektowania sztucznej inteligencji botów Raven podchodzimy w zwykły sposób: zastanówmy się, jakie zachowanie jest wymagane od botów, aby odnieść sukces w ich środowisku i rozłóż to zachowanie na listę komponentów, które jesteśmy w stanie zaimplementować i koordynować. Jestem pewien, że grałeś lub obserwowałeś, jak ktoś gra w deathmatch podobny do Quake'a, więc zastanówmy się nad tym doświadczeniem i zobaczmy, jakie obserwacje można powiedzieć o tym, jak człowiek gra w tego rodzaju grę. Dwie oczywiste wymagane umiejętności to umiejętność poruszania się oraz umiejętność celowania i strzelania z broni w innych graczy. Nie jest tak od razu oczywiste, że jeśli obserwujesz doświadczonych graczy, zauważysz, że prawie zawsze celują i strzelają do wroga (pod warunkiem, że jeden znajduje się w ich pobliżu), niezależnie od tego, czy atakują, czy bronią się, i niezależnie od kierunku, w którym się znajdują. w ruchu. Na przykład, mogą oni atakować z boku na bok lub uciekać do tyłu, jednocześnie stawiając ogień obronny. Wyciągniemy wskazówkę z tej obserwacji i zaimplementujemy elementy obsługi broni i ruchu AI, aby działały one niezależnie od siebie. Jakich umiejętności związanych z ruchem będzie potrzebować AI? Oczywiste jest, że bot powinien być w stanie poruszać się w dowolnym kierunku, unikając jednocześnie ścian i innych botów. Widzimy także, że konieczne jest zaimplementowanie pewnego rodzaju algorytmu wyszukiwania, aby umożliwić AI zaplanowanie ścieżek do określonych lokalizacji lub przedmiotów.

Co z obsługą broni? Jakie decyzje związane z bronią

Czy gracz musi coś zrobić? Po pierwsze, gracz musi zdecydować, która broń najlepiej pasuje do bieżącej sytuacji. W Raven są cztery rodzaje broni: blaster, strzelba, wyrzutnia rakiet i karabin. Każda z tych broni ma zalety i wady. Na przykład strzelba jest niszcząca, gdy w pobliżu znajduje się wróg, ale ze względu na sposób, w jaki strzał rozchodzi się na zewnątrz, gdy odsuwa się od lufy pistoletu, staje się znacznie mniej skuteczny z odległością. Wyrzutnia rakiet jest świetna na średnim dystansie, ale używanie jej z bliska jest niebezpieczne ze względu na efekt rozbicia od wybuchu. Każda wdrożona sztuczna inteligencja musi być w stanie rozważyć zalety i wady każdej broni i odpowiednio ją wybrać. Gracz musi również być w stanie skutecznie wycelować wybraną broń. W przypadku broni, która wystrzeliwuje pociski o dużej prędkości, takiej jak karabin i strzelba, gracz musi celować bezpośrednio w pozycję wroga, ale w przypadku broni, która wystrzeliwuje pociski wolniej poruszające się, takie jak blaster lub wyrzutnia rakiet, gracz musi być w stanie przewidzieć ruch wroga i odpowiednio celować. Bot AI musi być w stanie zrobić to samo. Często w tego rodzaju grach gracz zmierzy się z wieloma przeciwnikami. Jeśli widocznych jest dwóch lub więcej wrogów, gracz musi zdecydować, na którego z nich celować. W rezultacie każda zaprojektowana przez nas sztuczna inteligencja musi być w stanie wybrać jeden cel z grupy. To prowadzi nas do kolejnej kwestii: percepcji. Ludzcy gracze wybierają cele spośród przeciwników postrzeganych przez ich zmysły. W Raven obejmuje to widocznych przeciwników i przeciwników, którzy są wystarczająco głośni, aby je usłyszeć. Ponadto ludzie używają również pamięci krótkotrwałej do śledzenia botów, które ostatnio napotkali; ludzcy gracze nie zapominają natychmiast o przeciwnikach, którzy niedawno wyszli ze swojego zasięgu sensorycznego. Na przykład, jeśli gracz goni za celem, który następnie znika za rogiem, będzie gonił za celem, nawet jeśli go nie widać. Aby być przekonującym, każda sztuczna inteligencja musi również wykazywać podobne zdolności sensoryczne. Oczywiście wszystkie wymienione dotychczas umiejętności działają na dość niskim poziomie. W wielu grach tego typu po prostu nie wystarczy biegać po mapie losowo, strzelając do wrogów tylko wtedy, gdy się na nich natkną. Przyzwoita sztuczna inteligencja musi być w stanie zastanowić się nad swoim stanem i stanem otaczającego go świata oraz wybrać działania, które jego zdaniem pomogą poprawić jego stan. Na przykład bot powinien być w stanie rozpoznać, kiedy zaczyna mu brakować zdrowia, i opracować plan lokalizacji i nawigowania do elementu zdrowia. Jeśli bot walczy z wrogiem, ale brakuje mu amunicji, powinien być w stanie rozważyć możliwość przerwania walki w celu zlokalizowania kilku dodatkowych rakiet. Dlatego należy zaimplementować pewien rodzaj logiki decyzyjnej wysokiego poziomu.

Implementacja AI

Aby nasycić bota iluzją inteligencji, musimy zastosować całkiem spory wykaz umiejętności i zdolności. Przejrzyjmy je i omówmy, w jaki sposób każdy z nich jest implementowany przez sztuczną inteligencję Raven

Podejmowanie decyzji

W procesach decyzyjnych boty Raven używają architektury opartej na arbitrażu celów. Zachowanie niezbędne do wygrania gry przez bota jest podzielone na kilka celów wysokiego poziomu, takich jak "atak", "znalezienie zdrowia" lub "gonić cel". Cele można zagnieżdżać i cele wysokiego poziomu składają się z co najmniej dwóch celów cząstkowych. Na przykład cel "znajdź zdrowie" składa się z podzadań "znajdź ścieżkę do najbliższego aktywnego elementu zdrowia" i "podążaj ścieżką do przedmiotu". Z kolei cel "podążaj ścieżką" można rozłożyć na kilka celów typu "przejście do pozycji". Za każdym razem, gdy składnik decyzyjny sztucznej inteligencji bota jest aktualizowany, każdy z celów wysokiego poziomu jest oceniany pod kątem jego przydatności, biorąc pod uwagę obecny status bota, a jako bieżący cel wybierany jest ten z najwyższym wynikiem. Następnie bot rozłoży ten cel na składowe podzadania i spróbuje zaspokoić każdy z nich z kolei.

Ruch

W przypadku ruchu niskiego poziomu boty Raven korzystają z zachowań sterujących polegających na szukaniu, przybyciu, wędrowaniu, unikaniu ścian i separacji. Nie ma wykrycia kolizji ani reakcji między botami a geometrią świata; boty polegają wyłącznie na zachowaniach polegających na unikaniu ścian i sterowaniu separacją podczas negocjowania swojego środowiska. (Nie zalecam, abyś stosował to podejście w swoich własnych projektach - Twoja gra prawdopodobnie będzie wymagała znacznie surowszego wykrywania kolizji - ale jest odpowiednia dla wersji demonstracyjnych towarzyszących tej książce. Jest to również dość dobra demonstracja tego, jak skuteczne są zachowania kierownicze może być zastosowany prawidłowo.) Zachowania sterujące są realizowane w zwykły sposób opisany w poprzednich rozdziałach. Klasa Raven_Bot dziedziczy po MovingEntity i tworzy instancję własnego wystąpienia znanego obiektu zachowania sterującego. Komponenty AI, które wpływają na ruch bota, używają interfejsu do tego wystąpienia, aby kontrolować ruch bota.

Planowanie ścieżki

Boty w Raven muszą być w stanie planować ścieżki przez otoczenie, aby przejść do miejsca docelowego lub w kierunku instancji przedmiotu gry, takiego jak broń lub zdrowie. Aby ułatwić ten proces, każdy bot posiada dedykowaną klasę planowania ścieżek, której jego składnik decyzyjny może używać do żądania ścieżek.

Postrzeganie

W przypadku wielu gatunków gier (ale nie wszystkich) dokładne modelowanie percepcji jest jednym z kluczy do utrzymania złudzenia inteligencji, ponieważ świadomość agenta dotycząca jego środowiska powinna być spójna z jego ucieleśnieniem. Jeśli postać z gry ma dwoje oczu i dwoje uszu usytuowanych na głowie w podobny sposób jak człowiek, powinna odpowiednio postrzegać swoje otoczenie. Nie oznacza to, że musimy modelować widzenie stereo i słyszenie, ale najważniejsze jest, aby w grze tego typu logika decyzyjna agenta była spójna z tym, co powinna i nie powinna być w stanie dostrzec w swoim horyzoncie sensorycznym. Jeśli pojawi się jakaś niespójność, gracz będzie rozczarowany, a jego przyjemność z gry zostanie znacznie zmniejszona. Na przykład jestem pewien, że większość z nas widziała zachowania podobne do poniższych.

•  Podchodzisz do bota w ciszy od tyłu, ale natychmiast się obraca wokół (może słyszy mrugnięcie) i fragmentuje jelito pistoletem łańcuchowym.

•  Biegniesz i chowasz się. Wróg nie może wiedzieć, że zamknąłeś się w maleńkim pomieszczeniu magazynowym, ale mimo to przechodzi bezpośrednio do twojej lokalizacji, otwiera drzwi i rzuca granatem do środka.

•  Zauważysz dwóch strażników w wieży strażniczej. Zamiatają ziemię potężnym reflektorem, ale zauważasz ścieżkę do podstawy wieży, która zawsze jest w ciemności. Czołgasz się spokojnie i pewnie swoją wybraną trasą. Reflektor nigdy nie zbliża się do ciebie, ale jest jednym z nich , strażnicy krzyczą "Achtung" i strzelają ci w tyłek.

Tego rodzaju zdarzenia występują, ponieważ programista zapewnił AI całkowity dostęp do danych gry, dzięki czemu agenci otrzymali dar wszechmocy sensorycznej. Zrobił to, ponieważ było łatwiej, lub ponieważ nie miał czasu na oddzielenie prawdy od percepcji, a może po prostu dlatego, że nie zastanawiał się. W każdym razie jest to duże "nie-nie" dla graczy. Stracą zainteresowanie grą, ponieważ będą wierzyć, że AI oszukuje (co oczywiście występuje).

UWAGA. Ten typ modelowania sensorycznego nie jest tak ważny w przypadku gier typu RTS, w których obciążenie procesorem / pamięcią związane z implementacją takiego systemu dla setek agentów prawdopodobnie będzie wygórowane. Wątpliwe jest również, aby dzięki wdrożeniu takiego systemu wprowadzono znaczne usprawnienia w grze.

Aby uniknąć tych niespójności percepcyjnych, należy przefiltrować zmysł widzenia i słuchu agenta, aby zapewnić spójność z jego możliwościami wizualnymi i słuchowymi. Na przykład w grze, w której każdy bot musi wykazywać zdolności sensoryczne podobne do ludzkiego gracza, jeśli widok gracza jest ograniczony do 90 stopni, boty powinny mieć takie same ograniczenia. Jeśli wzrok gracza jest zasłonięty przez ściany i przeszkody, powinno to również dotyczyć botów. Jeśli gracz nie słyszy mrugania postaci lub słyszy dźwięki dalej poza pewnym zasięgiem, boty też nie powinny; a jeśli poziomy światła odgrywają ważną rolę w grze, bot nie powinien widzieć w ciemności (chyba że nosi oczywiście gogle noktowizyjne). Innym rodzajem problemu związanego z percepcją, często spotykanym w grach komputerowych, jest to, co lubię nazywać selektywną niewiedzą sensoryczną: niezdolność agentów do wyczuwania określonych rodzajów zdarzeń lub bytów. Oto kilka typowych przykładów.

•  Wchodzisz do pokoju. W oddali są dwa trolle odwrócone do ciebie plecami. Są wystarczająco blisko, abyś mógł zrozumieć ich mruczenie. Rozmawiają o obiedzie. Wilk wyskakuje z ciemności po lewej stronie, zaskakując cię. Zabijasz ogra, uwalniając swoje największe i najgłośniejsze zaklęcie: Death By Thunder Cannon. Ogr wybucha wspaniale w ryczącej eksplozji apokaliptycznej , ale dwa trolle tego nie słyszą - po prostu dyskutują o zaletach sosu miętowego z pieczoną jagnięciną.
•  Dźgasz nazistowskiego strażnika w plecy. Gdy osuwa się na podłogę, słyszysz zbliżających się strażników, więc ty wślizgujesz się w ciemny kąt. Strażnicy wchodzą do pokoju, a dłoń myszy napina się, gotowa na moment, kiedy zaczną rozglądać się za intruzem. Jednak strażnicy nie widzą ciała na podłodze, nawet gdy przechodzą tuż nad nim.
•  Znajdziesz się w walce hack and slash z upiornym wojownikiem. Niestety źle oceniłeś sytuację i otrzymujesz poważne kopnięcie. W desperacji odwracasz się i wybiegasz najbliższymi drzwiami, by odkryć, że gdy tylko znikniesz z oczu, wojownik zapomni o tobie.

Po raz kolejny złudzenie inteligencji zostaje przełamane, ponieważ postacie w grze nie zachowują się zgodnie z oczekiwaniami wynikającymi z ich zdolności percepcyjnych. W tych przykładach nie dzieje się tak dlatego, że agenci odbierają zbyt dużo informacji, ale raczej za mało. Ten ostatni przykład jest szczególnie interesujący, ponieważ pokazuje,że aby agent był przekonujący, musi zostać wyposażony w mechanizm symulujący pamięć krótkotrwałą. Bez pamięci krótkoterminowej agent nie jest w stanie rozważyć potencjalnych przeciwników, którzy leżą poza horyzontem sensorycznym. Może to spowodować spektakularnie głupie zachowanie. Na rycinie dwóch przeciwników - Zgrzytacz i Basher - znajduje się w polu widzenia Billy′ego, a on wybiera jednego, Basher, który będzie jego celem. Billy odwraca się do Bashera i strzela do niego.



Na nieszczęście dla Billy′ego, ponieważ jego programista nie obdarzył go żadną pamięcią krótkotrwałą, gdy tylko Zgrzytacz opuści pole widzenia, zostaje zapomniany. To daje Gnasherowi okazję, by podkraść się do Billy&pime;ego i odgryźć mu głowę.



Tego rodzaju sekwencji można łatwo uniknąć, jeśli agenci są w stanie zapamiętać to, co ostatnio wyczuli przez pewien czas. W Raven: zarządzanie, filtrowanie i zapamiętywanie zmysłów przez dane wejściowe jest enkapsulowane przez klasę Raven_SensoryMemory, której każdy bot posiada instancję. Ten obiekt zarządza std :: map MemoryRecords, która jest prostą strukturą danych, która wygląda następująco:

struct MemoryRecord
{
// rejestruje czas, w którym przeciwnik był ostatnio wykrywany (widziany lub słyszany). To
// służy do ustalenia, czy bot może "zapamiętać" ten rekord, czy nie.
// (jeśli CurrentTime () - dTimeLastSensed jest większy niż
// zakres pamięci bota, dane w tym rekordzie stają się niedostępne dla klientów)
double dTimeLastSensed;
// warto wiedzieć, jak długo widoczny jest przeciwnik. To
// zmienna jest oznaczana bieżącym czasem, ilekroć przeciwnik po raz pierwszy staje się
//widoczny. W takim razie łatwo jest obliczyć, ile czasu ma przeciwnik // był widoczny (CurrentTime - dTimeBecameVisible)
double dTimeBecameVisible;
// warto również wiedzieć, kiedy ostatni raz widziano przeciwnika
double dTimeLastVisible;
// wektor oznaczający pozycję, w której przeciwnik został ostatnio wykryty. To może
// być wykorzystywany do pomocy w wytropieniu przeciwnika, jeśli zniknie z pola widzenia Vector2D vLastSensedPosition;
// ustawione na true, jeśli przeciwnik znajduje się w polu widzenia właściciela
bool bWithinFOV;
// ustawione na true, jeśli nie ma przeszkody między przeciwnikiem a właścicielem,
// pozwalając na strzał.
bool bShootable;
};

Za każdym razem, gdy bot napotyka nowego przeciwnika, tworzona jest instancja MemoryRecord i dodawana do mapy pamięci. Po dokonaniu zapisu, za każdym razem, gdy odpowiedni przeciwnik zostanie usłyszany lub zobaczony, jego zapis jest aktualizowany odpowiednimi informacjami. Bot może korzystać z tej mapy pamięci, aby określić, których przeciwników wyczuwał ostatnio i odpowiednio zareagować. Ponadto, ponieważ każdy rekord pamięci buforuje informacje o widoczności, można uniknąć wielu obliczeń dotyczących pola widzenia. Zamiast żądać czasochłonnych żądań linii wzroku od obiektu świata gry, bot może w prosty i szybki sposób odzyskać wartość logiczną przechowywaną na mapie pamięci.

Deklaracja Raven_SensoryMemory jest następująca:

class Raven_SensoryMemory
{
private:
typedef std::map MemoryMap;
private:
// właściciel tego wystąpienia
Raven_Bot* m_pOwner;
// ten pojemnik służy do symulacji pamięci zdarzeń sensorycznych. MemoryRecord
// jest tworzony dla każdego przeciwnika w środowisku. Każdy rekord jest aktualizowany
// za każdym razem, gdy napotkasz przeciwnika. (kiedy jest widziane lub słyszane)
MemoryMap m_MemoryMap;
// bot ma zakres pamięci równoważny tej wartości. Gdy bot prosi o znak // lista wszystkich ostatnio wykrytych przeciwników, ta wartość służy do ustalenia, czy
// bot może zapamiętać przeciwnika lub nie.
double m_dMemorySpan;
// ta metoda sprawdza, czy istnieje pBot. Jeśli
// nie, nowy rekord MemoryRecord jest tworzony i dodawany do mapy pamięci // przez UpdateWithSoundSource & UpdateVision)
void MakeNewRecordIfNotAlreadyPresent(Raven_Bot* pBot);
public:
Raven_SensoryMemory(Raven_Bot* owner, double MemorySpan);
// ta metoda jest używana do aktualizacji mapy pamięci, ilekroć przeciwnik dokona gry // hałas
void UpdateWithSoundSource(Raven_Bot* pNoiseMaker);
// ta metoda dokonuje iteracji przez wszystkich przeciwników w świecie gry i
// aktualizuje zapisy tych, które znajdują się w FOV właściciela
void UpdateVision();
bool isOpponentShootable(Raven_Bot* pOpponent)const;
bool isOpponentWithinFOV(Raven_Bot* pOpponent)const;
Vector2D GetLastRecordedPositionOfOpponent(Raven_Bot* pOpponent)const;
double GetTimeOpponentHasBeenVisible(Raven_Bot* pOpponent)const;
double GetTimeSinceLastSensed(Raven_Bot* pOpponent)const;
double GetTimeOpponentHasBeenOutOfView(Raven_Bot* pOpponent)const;
// ta metoda zwraca listę wszystkich przeciwników, którzy mieli swoje
// rekordy zaktualizowane w ciągu ostatnich m_dMemorySpan sekund.
std::list GetListOfRecentlySensedOpponents()const;
};

Ilekroć wystąpi zdarzenie dźwiękowe, metoda UpdateWithSoundSource jest wywoływana ze wskaźnikiem do źródła dźwięku. UpdateVision jest wywoływany z Raven_Bot :: Aktualizacja z określoną częstotliwością. Razem te metody zapewniają, że zmysł słuchu i wzroku bota jest zawsze aktualny. Bot może następnie zażądać informacji z pamięci sensorycznej przy użyciu jednej z wymienionych metod, przy czym najbardziej interesującym jest GetListOfRecentlySensed-Opponents. Powoduje to iterację mapy pamięci i budowanie listy wszystkich przeciwników wykrytych w ostatniej pamięci. Oto jak wygląda metoda:

std::list
Raven_SensoryMemory::GetListOfRecentlySensedOpponents()const
{
// to zapisze wszystkich przeciwników, których bot może zapamiętać
std::list opponents;
double CurrentTime = Clock->GetCurrentTime();
MemoryMap::const_iterator curRecord = m_MemoryMap.begin();
for (curRecord; curRecord!=m_MemoryMap.end(); ++curRecord)
{
// jeśli ten bot został ostatnio zaktualizowany w pamięci, dodaj do listy
if ( (CurrentTime - curRecord->second.dTimeLastSensed) <= m_dMemorySpan)
{
opponents.push_back(curRecord->first);
}
}
return opponents;
}

Jak widać, jeśli dany rekord nie został zaktualizowany w ciągu ostatnich m_dMemorySpan sekund, nie jest dodawany do listy, a bot skutecznie zapomina o tym przeciwniku. Zapewnia to, że bot zapamięta przeciwnika na krótko po wykryciu, nawet jeśli zniknie z pola widzenia.

Wybór celu

Klasa, która obsługuje wybór celu, nazywa się Raven_TargetingSystem. Każdy Raven_Bot posiada instancję tej klasy i przekazuje do niej wybór celu. Deklaracja wygląda następująco:

class Raven_TargetingSystem
{
private:
// właściciel tego systemu
Raven_Bot* m_pOwner;
// bieżący cel (będzie pusty, jeśli nie zostanie przypisany cel)
Raven_Bot* m_pCurrentTarget;
public:
Raven_TargetingSystem(Raven_Bot* owner);
// za każdym razem, gdy ta metoda jest nazywana przeciwnikiem w sensorycznym odczuciu właściciela // pamięć jest sprawdzana, a najbliższa jest przypisywana do m_pCurrentTarget.
// jeśli nie ma przeciwników, którzy zaktualizowali swoje zapisy pamięci
// w zakresie pamięci właściciela, wówczas ustawiony jest bieżący cel
// do zera
void Update();
// zwraca true, jeśli istnieje aktualnie przypisany cel
bool isTargetPresent()const;
// zwraca true, jeśli cel znajduje się w polu widzenia właściciela
bool isTargetWithinFOV()const;
// zwraca true, jeśli między celem jest niezakłócona linia wzroku // i właściciel
bool isTargetShootable()const;
// zwraca pozycję, w której cel był ostatnio widziany. Zgłasza wyjątek, jeśli
// nie ma obecnie przypisanego celu
Vector2D GetLastRecordedPosition()const;
// zwraca czas, przez jaki cel znajdował się w polu widzenia
double GetTimeTargetHasBeenVisible()const;
// zwraca czas, przez jaki cel był niewidoczny
double GetTimeTargetHasBeenOutOfView()const;
// zwraca wskaźnik do celu. zero, jeśli nie ma prądu docelowego.
Raven_Bot* GetTarget()const;
// ustawia wskaźnik docelowy na null
void ClearTarget();
};

W określonym przedziale czasowym metoda aktualizacji systemu kierowania jest wywoływana z Raven_Bot :: Update. Aktualizacja uzyskuje listę ostatnio postrzeganych przeciwników z pamięci sensorycznej i wybiera jednego z nich jako aktualny cel. Kryterium wyboru stosowane przez boty Raven jest bardzo proste: Najbliższym przeciwnikiem jest aktualny cel. Działa to odpowiednio dla Raven, ale twoja gra może wymagać alternatywnych lub bardziej rygorystycznych kryteriów wyboru. Na przykład możesz chcieć zaprojektować metodę wyboru, która zawiera jedną lub więcej z następujących opcji:

•  Kąt odchylenia przeciwnika od kursu bota (innymi słowy, jest on tuż przed tobą)
•  Przeciwnik jest skierowany w stronę przeciwną (nie widzi cię - ukradkowy atak!)
•  Zasięg broni, którą ma przeciwnik (nie może mnie zdobyć)
•  Zasięg broni, którą nosi bot (nie mogę go zdobyć)
•  Wszelkie ulepszenia, których mogą używać przeciwnicy lub bot (jaki on jest twardy?)
•  Jak długo widoczny jest przeciwnik (prawdopodobnie wie o tym ja, jeśli wiem o nim)
•  Ile obrażeń zadał botowi w ostatnim czasie kilka sekund (to mnie wkurza!)
•  Ile razy przeciwnik został zabity przez bota (ha, ha!)
•  Ile razy bot został zabity przez przeciwnika (meanie!)

Obsługa broni

Boty Raven używają klasy Raven_WeaponSystem do zarządzania wszystkimi operacjami specyficznymi dla broni i ich rozmieszczania. Ta klasa posiada std :: mapę instancji broni, wpisaną w ich typ, wskaźnik do aktualnie posiadanej broni oraz zmienne oznaczające celność bota i czas reakcji bota. Te dwie ostatnie zmienne są wykorzystywane przez logikę celowania broni, aby uniemożliwić botowi trafienie w cel w 100 procentach przypadków lub strzelanie do przeciwnika, gdy tylko się pojawi. Jest to ważne, ponieważ jeśli sztuczna inteligencja działa zbyt dobrze, większość graczy szybko się sfrustruje i przestanie grać. Wartości te pozwalają testerom gry na dostosowanie poziomu umiejętności botów, dopóki nie stoczą trudnej bitwy, ale przegrywają częściej niż wygrywają. To, dla większości graczy, zapewni największą przyjemność z gry. Oprócz zmiennych składowych klasa ma metody dodawania broni, zmiany obecnej broni, celowania i strzelania z niej, a także wybierania najlepszej broni dla bieżącego stanu gry. Oto deklaracja do przejrzenia.

class Raven_WeaponSystem
{
private:
// mapa instancji broni wpisanych według typu
typedef std::map WeaponMap;
private:
Raven_Bot* m_pOwner;
// wskaźniki do broni, którą nosi bot (bot może nosić tylko jedno
// wystąpienie każdej broni)
WeaponMap m_WeaponMap;
// wskaźnik do broni, którą aktualnie trzyma bot
Raven_Weapon* m_pCurrentWeapon;
// to minimalny czas, jaki bot musi zobaczyć przeciwnika przed
// może na to zareagować. Ta zmienna służy do zapobiegania strzelaniu bota w stronę
// przeciwnik, gdy tylko stanie się widoczny.
double m_dReactionTime;
// za każdym razem, gdy wystrzeliwana jest bieżąca broń, przypadkowa ilość hałasu jest
// dodane do kąta strzału. Zapobiega to atakowaniu botów
// przeciwnik w 100% przypadków. Im niższa ta wartość, tym dokładniejsze
// cel bota będzie. Zalecane wartości wynoszą od 0 do 0,2 (wartość
// reprezentuje maksymalne odchylenie w radianach, które można dodać do każdego strzału).
double m_dAimAccuracy;
// czas, przez który bot będzie kontynuował celowanie w pozycję celu
// nawet jeśli cel zniknie z widoku.
double m_dAimPersistance;
// przewiduje, gdzie będzie cel, do chwili, gdy zajmie aktualną broń
// typ pocisku, aby do niego dotrzeć. Używany przez TakeAimAndShoot
Vector2D PredictFuturePositionOfTarget()const;
// dodaje losowe odchylenie do kąta strzału nie większe niż m_dAimAccuracy
// rads
void AddNoiseToAim(Vector2D& AimingPos)const;
public:
Raven_WeaponSystem(Raven_Bot* owner,
double ReactionTime,
double AimAccuracy,
double AimPersistance); ˜Raven_WeaponSystem();
/ustawia mapę broni za pomocą tylko jednej broni: blaster
void Initialize();
// metoda ta wycelowuje aktualną broń bota w cel (jeśli jest to strzałka) // cel) i, jeśli jest celowany prawidłowo, strzela rundą. (Nazywany każdym krokiem aktualizacji
// z Raven_Bot :: Update)
void TakeAimAndShoot()const;
// ta metoda określa najodpowiedniejszą broń do użycia, biorąc pod uwagę bieżącą
// stan gry. (Wywoływany co n kroków aktualizacji z Raven_Bot :: Update)
void SelectWeapon();
void SelectWeapon ();
// to doda broń określonego typu do ekwipunku bota.
// Jeśli bot ma już broń tego typu, dodawana jest tylko amunicja.
// (wywoływany przez wyzwalacza broni, aby dać botowi broń)
void AddWeapon(unsigned int weapon_type);
// zmienia bieżącą broń na jeden z określonego typu (pod warunkiem, że ten typ
// jest w posiadaniu bota)
void ChangeWeapon(unsigned int type);
//zwraca wskaźnik do bieżącej broni
Raven_Weapon* GetCurrentWeapon()const{return m_pCurrentWeapon;}
// zwraca wskaźnik do określonego typu broni (jeśli w ekwipunku, null, jeśli
// nie)
Raven_Weapon* GetWeaponFromInventory(int weapon_type);
//zwraca ilość amunicji pozostałej dla określonej broni
int GetAmmoRemainingForWeapon(unsigned int weapon_type);
double ReactionTime()const{return m_dReactionTime;}
};

Metoda SelectWeapon wykorzystuje logikę rozmytą, aby wybrać najlepszą broń do użycia w bieżącym stanie gry. Logika rozmyta to logika, która została poszerzona o prawdy częściowe. Innymi słowy, obiekt nie musi być ani członkiem zbioru, ani nie; z logiką rozmytą obiekt może być do pewnego stopnia członkiem zbioru. Każdy krok aktualizacji wywoływany jest z metody TakeAimAndShoot z Raven_Bot :: Update. Ta metoda najpierw sprawdza system celowania, aby upewnić się, że bieżący cel można strzelać (system celowania z kolei pobiera te informacje z pamięci sensorycznej bota) lub dopiero niedawno zniknął z pola widzenia. Ten ostatni warunek zapewnia, że bot będzie nadal celował swoją bronią w cel, nawet jeśli na krótko uchyli się za ścianą lub inną przeszkodą. Jeśli żaden z tych warunków nie jest spełniony, kierunek skierowania broni zostanie wyrównany z kierunkiem bota. Jeśli jeden z warunków jest spełniony, określa się najlepsze miejsce do wycelowania obecnej broni. W przypadku broni "natychmiastowego trafienia", takiej jak strzelba lub karabin, będzie to bezpośrednio przy celu. W przypadku broni strzelającej wolniej poruszającymi się pociskami, takiej jak wyrzutnia rakiet lub miotacz, metoda musi przewidzieć, gdzie będzie cel, zanim pocisk się do niego zbliży. Obliczenia te są podobne do obliczeń stosowanych przy zachowaniu kierowania w pościgu i są przeprowadzane za pomocą metody PredictFuturePositionOfTarget.

WSKAZÓWKA. W miarę, jak stoi kod Ravena, przewidywanie przyszłej pozycji celu do celowania bronią opiera się na jego prędkości chwilowej - prędkości, z jaką porusza się on w momencie obliczeń. Może to jednak dawać słabe wyniki, zwłaszcza jeśli cel bardzo się omija. Bardziej dokładną metodą jest pobranie średniej prędkości celu próbkowanej w ostatnich t krokach czasowych.

Po określeniu pozycji celowania logika obraca pozycję bota skierowaną w jego stronę i strzela do broni, pod warunkiem, że celuje ona prawidłowo i że cel jest widoczny dłużej niż czas wymagany do zareagowania bota. Cała ta logika jest znacznie jaśniejsza w kodzie, więc oto lista metod:

void Raven_WeaponSystem::TakeAimAndShoot()const
{
// celuj bronią tylko wtedy, gdy bieżący cel można strzelać lub jeśli ma tylko
// bardzo niedawno zniknął z pola widzenia (ten ostatni warunek to zapewnienie
// broń jest wycelowana w cel, nawet jeśli chwilowo uchyla się za ścianą
// lub inną ochroną)
if (m_pOwner->GetTargetSys()->isTargetShootable() ||
(m_pOwner->GetTargetSys()->GetTimeTargetHasBeenOutOfView()
m_dAimPersistance) )
{
// pozycja, na którą będzie celowana broń
Vector2D AimingPos = m_pOwner->GetTargetBot()->Pos();
// jeśli bieżąca broń nie jest pistoletem z natychmiastowym trafieniem, pozycja docelowa
// należy dostosować, aby uwzględnić przewidywane ruchy
// celu
if (GetCurrentWeapon()->GetType() == type_rocket_launcher ||
GetCurrentWeapon()->GetType() == type_blaster)
{
AimingPos = PredictFuturePositionOfTarget();
// jeśli broń jest wycelowana prawidłowo, między obrazem znajduje się linia wzroku
// bot i pozycja celowania, i było to widoczne już od dłuższego czasu
// niż czas reakcji bota, strzelaj z broni
if ( m_pOwner->RotateFacingTowardPosition(AimingPos) &&
(m_pOwner->GetTargetSys()->GetTimeTargetHasBeenVisible() >
m_dReactionTime) &&
m_pOwner->GetWorld()->isLOSOkay(AimingPos, m_pOwner->Pos()))
{
AddNoiseToAim(AimingPos);
GetCurrentWeapon()->ShootAt(AimingPos);
}
}
// nie musisz przewidywać ruchu, celuj bezpośrednio w cel

else
{
// jeśli broń jest wycelowana prawidłowo i była widoczna przez pewien okres
// dłużej niż czas reakcji bota, strzelaj z broni
if ( m_pOwner->RotateFacingTowardPosition(AimingPos) &&
(m_pOwner->GetTargetSys()->GetTimeTargetHasBeenVisible() >
m_dReactionTime) )
{
AddNoiseToAim(AimingPos);
GetCurrentWeapon()->ShootAt(AimingPos);
}
}
}
// brak celu do strzelania, więc obróć go tak, aby był równoległy do bota // kierunek kierunku
else
{
m_pOwner->RotateFacingTowardPosition(m_pOwner->Pos()+ m_pOwner->Heading());
}
}

Zwróć uwagę, jak przy przewidywaniu pozycji celowania należy wykonać test linii wzroku, aby upewnić się, że przewidywana pozycja nie jest zasłonięta przez ściany. Nie jest to konieczne, jeśli broń jest wycelowana bezpośrednio w cel, ponieważ LOS do pozycji docelowej jest buforowany, gdy rekord pamięci celu jest aktualizowany. Zauważ również, że bezpośrednio przed wystrzeleniem broni do pozycji celowania dodaje się trochę hałasu, aby 100% czasu nie trafić w cel.

WSKAZÓWKA. W przypadku niektórych gier dobrym pomysłem jest upewnienie się, że agent kontrolowany przez AI zawsze chybia za pierwszym razem, gdy strzela do gracza. Jest tak, ponieważ strzał ostrzeże gracza przed obecnością agenta, umożliwiając mu podjęcie odpowiednich działań bez natychmiastowego zranienia. Jest to szczególnie przydatne w scenariuszach, w których gracz często wchodzi do niezbadanych pokoi pełnych złych, ponieważ daje mu szansę na wycofanie się i podsumowanie sytuacji zamiast niespodziewanej rzezi. Ponadto, gdy celowo strzelasz, aby chybić, jeśli pocisk lub jego trajektoria jest łatwo widoczna (jak rakieta lub strzała), możesz zwiększyć emocje, upewniając się, że strzał przechodzi blisko i w polu widzenia gracza. Kolejna dobra wskazówka dotycząca celowania: jeśli zdrowie gracza jest bardzo niskie, zmniejsz celność wszystkich botów, które do niego strzelają. W ten sposób zyskuje szansę na niesamowitą regenerację, która znacznie poprawi jego wrażenia z gry. (Czuje się jak Aragorn w bitwie o Helm's Deep zamiast jak Paul Newman i Robert Redford w ostatnich minutach Butcha Cassidy'ego i Sundance Kid!)

Łącząc wszystko razem

Rycina poniższy pokazuje, jak wzajemnie powiązane są elementy AI omówione na kilku ostatnich stronach.



Zwróć uwagę, że obiekt Goal_Think nie ma bezpośredniej kontroli nad komponentami niskiego poziomu, takimi jak ruch i obsługa broni. Jego celem jest rozstrzyganie i zarządzanie przetwarzaniem celów wysokiego poziomu. Poszczególne cele wykorzystują komponenty niższego poziomu, gdy jest to wymagane. Wszystkie te komponenty są aktualizowane z określoną częstotliwością z metody Raven_Bot :: Update, więc myślę, że właśnie tam powinniśmy spojrzeć dalej.

Aktualizacja komponentów AI

Nie jest konieczne, aby wszystkie elementy AI bota były aktualizowane za każdym razem. Wiele składników wymaga bardzo dużo procesora, a ich aktualizacja w tym samym tempie byłaby szaleństwem. Zamiast tego badany jest każdy komponent, aby zobaczyć, jak krytyczny jest czas lub ile procesora wymaga, a częstotliwość aktualizacji jest odpowiednio przypisywana. Na przykład generalnie istotne jest, aby komponent ruchu AI był aktualizowany za każdym razem, aby poprawnie omijać przeszkody i ściany. Element taki jak wybór broni nie jest tak krytyczny czasowo, dlatego jego częstotliwość aktualizacji może występować znacznie wolniej; powiedzmy dwa razy na sekundę. Podobnie komponent pamięci sensorycznej bota, który sonduje świat gry w poszukiwaniu widocznych przeciwników, jest bardzo wymagający pod względem procesora ze względu na liczbę przeprowadzonych testów z linii widzenia. Z tego powodu odpytywanie jest ograniczone do niskiej częstotliwości - domyślnie cztery razy na sekundę - a wyniki są buforowane. To oczywiście nie jest nauka o rakietach. Często nie będziesz w stanie wiedzieć, jaka jest idealna częstotliwość aktualizacji, więc musisz dobrze zgadywać i dostosowywać, dopóki nie będziesz zadowolony z wyników. Boty Raven używają instancji obiektów Regulator do kontrolowania aktualizacji każdego z ich komponentów AI. Jest to prosta klasa, która jest tworzona za pomocą wymaganej częstotliwości aktualizacji i ma jedną metodę isReady, która zwraca wartość true, jeśli nadszedł czas, aby zezwolić na następną aktualizację. Deklaracja klasy wygląda następująco:

class Regulator
{
private:
// okres między aktualizacjami
double m_dUpdatePeriod;
// następnym razem regulator zezwoli na przepływ kodu
DWORD m_dwNextUpdateTime;
public:
Regulator(double NumUpdatesPerSecondRqd);
// zwraca true, jeśli bieżący czas przekracza m_dwNextUpdateTime
bool isReady();
};

Klasa Regulator automatycznie zapewnia rozłożenie aktualizacji na wiele etapów czasowych poprzez dodanie małego losowego przesunięcia (od 0 do 1 sekundy) do m_dwNextUpdateTime po utworzeniu wystąpienia. (Bez tego przesunięcia ten sam komponent wszystkich aktywnych agentów zostanie zaktualizowany na tym samym etapie).

WSKAZÓWKA. Korzystając z regulatorów, możliwe jest także wdrożenie pewnego rodzaju sztucznej inteligencji "obniżającej poziom szczegółowości" poprzez obniżenie szybkości aktualizacji niektórych elementów sztucznej inteligencji agentów, które są daleko od gracza i nie mają znaczenia dla jego bezpośredniego doświadczenia. Raven tego nie robi, ponieważ świat gry jest mały, ale możesz spróbować z tym pomysłem na własne gry. Klasa Raven_Bot tworzy kilka regulatorów i używa większości z nich w swojej metodzie aktualizacji w następujący sposób:

void Raven_Bot::Update()
{
// przetworzy aktualnie aktywny cel. Zauważ, że jest to wymagane, nawet jeśli bot
// jest pod kontrolą użytkownika. Wynika to z faktu, że cele są tworzone za każdym razem, gdy użytkownik
// klika obszar mapy, który wymaga żądania planowania ścieżki.
m_pBrain->Process();
// Oblicz siłę kierowania i zaktualizuj prędkość i pozycję bota
UpdateMovement();
// jeśli bot jest pod kontrolą AI
if (!isPossessed())
{
// zaktualizuj pamięć sensoryczną za pomocą dowolnego bodźca wzrokowego
if (m_pVisionUpdateRegulator->isReady())
{
m_pSensoryMem->UpdateVision();
}
// sprawdź wszystkich przeciwników w pamięci sensorycznej bota i wybierz jednego z nich // być bieżącym celem
if (m_pTargetSelectionRegulator->isReady())
{
m_pTargSys->Update();
}
// oceniaj i rozstrzygaj wszystkie możliwe cele na wysokim poziomie
if (m_pGoalArbitrationRegulator->isReady())
{
m_pBrain->Arbitrate();
}
// wybierz odpowiednią broń z broni obecnie znajdującej się w // inwentaryzacja
if (m_pWeaponSelectionRegulator->isReady())
{
m_pWeaponSys->SelectWeapon();
}
// ta metoda wycelowuje aktualną broń bota w bieżący cel
// i oddaje strzał, jeśli jest to możliwe
m_pWeaponSys->TakeAimAndShoot();
}
}

UWAGA Agregujemy instancje Regulator w klasie Raven_Bot, ponieważ dzięki temu ich użycie jest bardziej wyraźne. Możesz preferować, aby obiekty wymagające regulacji tworzyły własne instancje i używały ich do kontrolowania przepływu logiki w odpowiedniej metodzie (zwykle w metodzie aktualizacji).

Podsumowanie

Omówiono projektowanie sztucznej inteligencji dla agentów zdolnych do gry typu deathmatch. Chociaż twoje zrozumienie jest wciąż niepełne, widziałeś, jak AI agenta można rozłożyć na kilka małych, łatwych w zarządzaniu komponentów, które są w stanie komunikować się i współpracować, tworząc ujednolicone zachowanie. Pozostałe rozdziały dostarczą gipsu do uzupełnienia luk w twojej wiedzy.

Praktyka czyni mistrza

1. Jak dotąd boty Raven wyczuwają tylko przeciwników, których widzą lub słyszą. Jednak nadal nie są w stanie poczuć strasznego palenia i rozdzierania wrażenie, jak stalowa obudowa pocisku rozdziera ich ciało. Napisz kod, aby zaktualizować swój system sensoryczny, aby bot mógł wykryć, kiedy jest strzelany. Utwórz kolejne pole w strukturze MemoryRecord, aby zapisać obrażenia zadane przez każdego przeciwnika w ciągu ostatnich kilku sekund. Wartość tę można wykorzystać jako część kryterium wyboru celu.

2. Wypróbuj różne kryteria wyboru celu. Obserwuj, jak wpływają na rozgrywkę. Zmień kod, tak aby każdy bot używał unikalnego kryterium i rozegraj je ze sobą, aby zobaczyć, który z nich działa najlepiej.