Sztuczna inteligencja w grach #01 – wstęp, zmysł wzroku oraz prosty debug-mode.
Sztuczna inteligencja w grach jest nieodłącznym elementem od dawien dawna. Jest ona mózgiem każdego wroga i NPC-ta. Kiedy programujemy A.I. mamy ogromne pole do popisu. Rozbudowywanie jej nie jest trudne i daje świetne efekty. Jest to element, który łatwo dostrzec i dzięki temu ludzie doceniają Twoją pracę jeszcze bardziej. W różnych grach można spotkać przeróżny poziom jakości zachowań postaci sterowanych komputerem. To jak dobre A.I. będą mieli wrogowie, często ma największy wpływ na końcową ocenę gry.
Niektóre z tytułów wręcz są popularne ze swojej prymitywnej S.I.:
Policjanci w GTA.
Wbrew popularnej opinii sztuczna inteligencja policjantów w San Andreas nie jest aż taka prosta, jednak można by było ją trochę doszlifować. W tym artykule rozpoczniemy tworzenie podstawowego zmysłu dla przeciwników – zmysłu wzroku i wprowadzimy sobie prosty interfejs do debuggingu.
Zaczniemy od kodu finalnego z mojego poprzedniego artykułu: Jak programować gry? Najważniejsze elementy w grach 2D. Kod źródłowy z tamtego artykułu znajdziecie tutaj: GitHub
1. Przygotowanie sterowania i postaci pod grę.
Rozpoczniemy od stworzenia klasy gracza oraz klasy przeciwnika, które nazwiemy odpowiednio
CPlayer
oraz
CEnemy
. Następnie stworzymy prosty kontroler dla obydwu, a dodatkowo w kontrolerze gracza dodamy możliwość poruszania się.
Podobny schemat realizowałem już w filmie o tworzeniu bombermana, jednak przewiduję, że prawdopodobnie go nie obejrzałeś (bo komu chciałoby się oglądać w całości film o tworzeniu gry trwający 2 godziny). Z tego względu jeszcze raz wytłumaczę, co zrobiliśmy:
I) Utworzyliśmy klasę gracza i jego kontrolera:
-
CPlayer
– klasa gracza -
CPlayerController
– klasa kontrolera gracza zawierająca sterowanie
Opisałem kod tak, by było dokładnie widać co krok po kroku trzeba wykonać.
Plik nagłówkowy
Player.hpp
:
Plik źródłowy
Player.cpp
:
II) Utworzyliśmy klasę wroga i jego kontrolera. Póki co nasze A.I. zostawimy w spokoju. Zajmiemy się nim w następnej części tego artykułu.
-
CEnemy
– klasa wroga -
CEnemyAIController
– klasa kontrolera S.I. wroga
Plik nagłówkowy
Enemy.hpp
:
Plik źródłowy
Enemy.cpp
:
III) Wczytaliśmy potrzebne tekstury przy uruchamianiu gry:
IV) Stworzyliśmy gracza i jednego wroga, a następnie dodaliśmy ich do poziomu:
2. Utworzenie zmysłu wzroku.
Sztuczna inteligencja w grach jest oparta o różne zmysły np.: zmysł wzroku, zmysł słuchu czy nawet czasem zmysł węchu. Najprostszym do zaimplementowania jest zmysł wzroku, który jak zapewne łatwo jest się domyślić, będzie odpowiadał za widzenie. Będzie on działał w ten sposób, że będzie sprawdzał, czy jest coś interesującego w zasięgu wzroku oraz w obszarze widoku (kąt widzenia zrobimy łatwy do dostosowania) i jeśli coś wykryje, to zwróci to w wyniku.
Na początku, jako że wszystkie zmysły będą miały kilka wspólnych metod i własności, utworzymy sobie klasę bazową. Nazwałem ją IAISense (w swoich projektach przedrostek „I” dodaje do klas bazowych, dostarczających swego rodzaju interfejs do późniejszego rozbudowywania, jednak takich, które nie mogą być bezpośrednio używane). Do implementacji stworzymy sobie osobne pliki –
AISense.hpp
i
AISense.cpp
.
Teraz, zanim przejdziemy do kodu, zastanówmy się, co będzie musiał mieć każdy zmysł. Po chwili rozmyślań doszedłem do wniosku, że:
- zmysł musi wiedzieć, kto jest jego właścicielem, gdyż każdy z nich musi znać np. pozycje właściciela;
- zmysł musi posiadać jakąś listę postrzeganych aktorów;
- zmysł musi dostarczać możliwość uaktualnienia postrzeganych aktorów (np. gdy ktoś wejdzie do pola widzenia albo wyjdzie z niego).
Będąc świadomym tego, do czego dążymy, utworzyłem następującą klasę bazową:
Warto bardzo dokładnie przeanalizować komentarze, szczególnie ten przy metodzie QueryActors, bo jest to kluczowa metoda tej klasy. Teraz do zaimplementowania został nam tak naprawdę tylko konstruktor (zwróć uwagę, że
QueryActors
jest metodą czysto wirtualną,
GetSensedActors
jest zdefiniowana jako
inline
).
Kod konstruktora jest bardzo prosty:
Pokusiłem się tutaj o referencje na klasę
IPawn
(lub bazową) właściciela. Takie rozwiązanie wymusza podanie go (gdybyśmy użyli wskaźników, ktoś mógłby podać np. pusty wskaźnik), przez co reszta kodu, która się do niego odnosi, zadziała poprawnie.
Następnym etapem będzie stworzenie zmysłu wzroku. Aby mieć jakiś wgląd w sposób jego działania, spójrz na poniższy obraz:
Zatem mamy do stworzenia kolejną klasę, która będzie zawierała kąt i zasięg widzenia oraz implementacje metody
QueryActors
, która sprawdzi, czy aktorzy są polu widzenia. Zdecydowałem, że umieszczę tę klasę również w plikach
AISense.hpp
i
AISense.cpp
by nasz projekt nie miał za chwile 40 plików.
Tak oto wygląda ta klasa:
Tym razem do zaimplementowania mamy konstruktor oraz trzy metody. Może zajmijmy się najpierw konstruktorem:
Nie wrzuciłem ustawienia
m_sightDistance
i
m_sightAngle
do listy inicjalizacyjnej, ze względu na to, że mamy od tego odpowiednie funkcje, które zapobiegają wprowadzeniu nieodpowiednich danych.
Teraz spójrzmy dalej do metod
SetSightDistance
i
SetSightAngle
. Odpowiednie środki bezpieczeństwa są tutaj wymagane. Nie chcemy przecież mieć ujemnego zasięgu lub kąta widzenia > 180 stopni. Warto również zauważyć, że podane funkcje zwracają
true
,
jeśli poprawnie ustawiono wartość a
false
,
jeśli wartość była niepoprawna. Nie będziemy póki co z tego korzystać, ale warto mieć coś takiego zaimplementowanego – może niedługo się przyda 🙂
Przyszedł czas na kluczowy moment. Teraz zajmiemy się całą logiką zmysłu. Pomyślmy, co powinien on krok po kroku zrobić:
- pobrać listę wszystkich aktorów ze sceny;
-
każdy aktor powinien być sprawdzony pod niżej wypisanymi kryteriami. Jeśli wszystkie z nich zostaną spełnione, dodajemy go do wynikowej listy aktorów;
- aktor nie jest właścicielem tego zmysłu;
- aktor znajduje się w odległości mniejszej niż zasięg wzroku;
- kąt, pod którym znajduje się aktor względem właściciela, jest mniejszy niż kąt widzenia;
- funkcja filtrująca pozwoliła aktorowi na dodanie do wyników.
- zwrócić końcową listę aktorów.
Problemem jednak jest to, że w poprzednim artykule nie stworzyliśmy sobie metody, dzięki której zyskamy dostęp do listy aktorów ze sceny. Dlatego właśnie potrzebna jest nam poniższa funkcja, którą dodałem do klasy
CLevel
:
Na tych założeniach zbudowałem taką funkcję. Polecam przeanalizować po kolei każdy etap jej działania, gdyż właśnie jesteśmy w punkcie kulminacyjnym tego artykułu.
Uff… już najtrudniejsze za nami. Teraz możemy dodać już zmysł wzroku do wroga.
CSightSense m_sightSense; // Zmysl wzroku wroga
Należy również pamiętać o prawidłowym wywołaniu konstruktora zmysłu z poziomu listy inicjalizacyjnej. Następnie, aby nasz zmysł „działał” musimy go ciągle uaktualniać. Do tego posłuży nam metoda
IActor::Update
, którą sobie przeładujemy. Musimy jednak pamiętać, że metoda ta również ma swoją implementację w klasie bazowej
IPawn
, więc musimy umieścić też odwołanie do implementacji bazowej:
Co nam jednak z tego, że tak się narobiliśmy, a wciąż nie widzimy efektów? Właśnie dlatego teraz zaimplementujemy…
3. Widok informacji dla debuggingu
Wyszukiwanie błędów w grach jest bardzo uciążliwe, jeśli przed sobą mamy same cyferki i nic konkretnego. Dlatego właśnie wiele gier dodaje sobie prosty panel debugowania „in-game”. Tym właśnie się teraz zajmiemy.
Na samym początku już wiemy, że potrzebna będzie nam funkcja generująca kształt wycinka koła. Jako że SFML sam jej nie dostarcza, postanowiłem napisać ją sam. Stworzyłem więc plik z deklaracją
SFMLShapes.hpp
oraz plik źródłowy
SFMLShapes.cpp
. Możliwe, że w przyszłości potrzebne będzie nam więcej własnych kształtów i wtedy umieścimy ich generowanie również w tych plikach.
Nazwałem ten kształt Pie (odnosi się do angielskiej nazwy ciasta, ponieważ nasz kształt to jakby wycinek ciasta 😉 ).
Powyższa funkcja nie jest w pełni funkcjonalna, jednak wystarcza do podstawowych zastosowań. Jej mankamentem jest to, że używa
sf::ConvexShape
(convex – wypukły). Może to spowodować niechciany efekt przy wyświetlaniu wycinka o kącie > 90 stopni. Jeśli ktoś ma na tyle ochoty, żeby się z tym bawić, to polecam do tego użyć
sf::VertexArray
.
Teraz już możemy wykorzystać ten generator. Pamiętamy jeszcze klasę
IAISense
? Każdy zmysł będzie mógł się popisać jakimś fajnym symbolem przy debuggingu, dlatego też utworzymy metodę dla tej klasy, która będzie rysowała takie symbole na ekranie.
Metoda ta, nie jest metodą czysto wirtualną, bo zmysł ma mieć możliwość wyświetlenia debug info, ale nie jest do tego zmuszany.
Teraz czym do diabła jest to
static bool DebugMode;
?
Dobrym pomysłem jest posiadanie jakiegoś przełącznika, którym będziemy sterowali, by albo włączyć tryb testowy, albo go wyłączyć. Statyczna zmienna
DebugMode
jest właśnie takim przełącznikiem. Oczywiście pamiętamy o tym, że taką zmienną statyczną trzeba też zainicjalizować. Najprościej będzie zrobić to na początku pliku źródłowego:
bool IAISense::DebugMode = true;
Mamy już bazę, to teraz trzeba zaimplementować wyświetlanie informacji trybu testowego dla zmysłu wzroku. Zrobiłem to tak:
Weźmy głęboki oddech… w tym miejscu artykuł ten możemy zakończyć jedną linijką, którą dodamy, by nasz debug info mógł się w ogóle wyświetlić. Jak zapewne się już domyśliłeś, umieścimy ją w metodzie wyświetlającej wroga na ekranie.
m_sightSense.DrawDebug();
4. Podsumowanie
W tym artykule zbudowaliśmy sobie podstawę pod dalsze rozwijanie modułu sztucznej inteligencji. Dodaliśmy również możliwość wyświetlania podstawowych informacji debuggingu. Kod z tego artykułu znajdziesz tutaj: GitHub . Teksturki można pobrać tutaj: Mega .
Paweł Syska
Zajmuję się programowaniem w C++ od początku 2009 roku, oprócz tego poznałem w tym czasie kilka innych języków, jednak wciąż pozostaje wierny temu pierwszemu ;)
Komentarze