Nastoletni
Programiści

Logo Nastoletnich Programistów

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.

Logika GTA San Andreas

Źródło: http://www.memecenter.com/fun/509528/gta-sa-logic

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ę.
Poruszanie postaci za pomocą WSAD

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.

Debugowanie w Gothic 2 NK

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 ;)

Zobacz wszystkie posty tego autora →

Komentarze

  • Condzi

    Świetny artykuł, czekam na więcej. Co będzie w następnym wpisie?

    • Paweł Syska

      Dodamy sobie prostą fizykę. Mam na myśli proste przeszkody, które będą blokowały ruch. Następnie będziemy generowali informacje o nawigacji na mapie. Prawdopodobnie w trzeciej części rozpoczniemy wyszukiwanie drogi w świecie 2D – możliwe, że oprzemy się na algorytmie, nad którym pracuje mój kumpel i możliwe, że napisze on o tym artykuł. Także, już w ciągu 2-3 dni można spodziewać się kolejnej części.