Piłka Nożna A.I.Programownie A.I. Gier

Symulacja Piłki Nożnej

Projektowanie sztucznej inteligencji drużynowej, a zwłaszcza AI do gry w piłkę nożną, nie jest łatwe. Stworzenie agentów zdolnych do gry w coś podobnego do ich profesjonalnych ludzkich odpowiedników wymaga ciężkiej pracy. Wiele zaawansowanych technicznie drużyn z wybitnych uniwersytetów z całego świata rywalizuje w robotycznym turnieju piłkarskim Robocup od początku lat dziewięćdziesiątych. Chociaż ambitnym celem turnieju jest produkcja robotów zdolnych do wygrania Pucharu Świata do roku 2050 (nie żartuję), istnieje również symulacja turnieju piłkarskiego równoległego do robota, w którym rywalizują drużyny symulowanych piłkarzy na wirtualnej murawie . Wiele z tych drużyn korzysta z najnowocześniejszej technologii sztucznej inteligencji, z których większość została opracowana specjalnie dla piłki nożnej. Gdybyś miał wziąć udział w turnieju, usłyszałbyś, między okrzykami i jękami, zespoły dyskutujące o zaletach uczenia się rozmytego-Q, projektowaniu wykresów koordynacji dla wielu agentów i pozycjonowaniu strategicznym opartym na sytuacji. Na szczęście, jako programiści gier, nie musimy zajmować się wszystkimi szczegółami odpowiednio symulowanego środowiska piłkarskiego. Naszym celem nie jest zwycięstwo w Pucharze Świata, ale stworzenie agentów zdolnych do gry w piłkę nożną wystarczająco dobrze, aby zapewnić graczowi zabawne wyzwanie. Ta część poprowadzi Cię przez tworzenie agentów gier, którzy mogą grać w uproszczoną wersję piłki nożnej - Simple Soccer - wykorzystując tylko umiejętności, których nauczyłeś się w tym tekście. Moim zamiarem nie jest zademonstrowanie, w jaki sposób należy modelować każdą taktykę i umiejętności, ale pokazanie, jak zaprojektować i wdrożyć środowisko sztucznej inteligencji w drużynie sportowej, zdolne do wspierania własnych pomysłów. Mając to na uwadze, zachowałem środowisko gry i zasady Simple Soccer, cóż ... bardzo proste. Postanowiłem też pominąć pewne oczywiste taktyki. Częściowo dlatego, że zmniejszy złożoność sztucznej inteligencji, a tym samym ułatwi ci zrozumienie przepływu logiki automatu stanów, ale głównie dlatego, że da ci możliwość utrwalenia umiejętności, których nauczyłeś się w prawdziwym życiu , w pełni rozwinięty projekt AI gry, jeśli zdecydujesz się wykonać ćwiczenia na końcu tej części. Do czasu ukończenia tego rozdziału będziesz w stanie tworzyć agentów AI zdolnych do grania w większość gier zespołowych. Hokej na lodzie, rugby, krykiet, futbol amerykański, a nawet zdobycie flagi - będziesz w stanie kodować zabawną sztuczną inteligencję.

Proste środowisko i zasady gry w piłkę nożną

Zasady gry są nieskomplikowane. Istnieją dwie drużyny: czerwona i niebieski. Każda drużyna składa się z czterech graczy pola i jednego bramkarza. Celem gry jest zdobycie jak największej liczby bramek. Bramkę zdobywa się, kopiąc piłkę nad linią bramkową drużyny przeciwnej. Boki obszaru gry Simple Soccer (zwanego "boiskiem") są otoczone (jak w hokeju), aby piłka nie mogła wyjść poza pole gry, ale po prostu odbiła się od ścian. Oznacza to, że w przeciwieństwie do zwykłej piłki nożnej, nie ma rzutów rożnych ani rzutów z linii bocznej. Aha, i zdecydowanie nie ma reguły spalonej! Rysunek pokazuje konfigurację na początku typowej gry.



Środowisko gry składa się z następujących elementów:

•  Boisko do piłki nożnej
•  Dwie bramki
•  Jedna piłka
•  Dwa zespoły
•  Ośmiu graczy pola
•  Dwóch bramkarzy

Każdy typ elementu jest enkapsulowany jako obiekt. Możesz zobaczyć, jak wszystkie są ze sobą powiązane, studiując uproszczony schemat klas UML pokazany na rysunku



Obiekty gracza i bramkarza są podobne do agentów gry, z którymi spotkałeś się już w tym tekście. Wkrótce opiszę je szczegółowo, ale najpierw chcę pokazać, w jaki sposób wdrażane są boisko, bramki i piłka nożna. To powinno dać ci poczucie środowiska, w którym zajmują się agenci gry, a następnie mogę przejść do drobiazgowości samej AI.

Boisko do piłki nożnej

Boisko do piłki nożnej jest prostokątnym boiskiem otoczonym ścianami. Na każdym z krótkich krańców boiska stoi centralnie bramka. Małe kółko na środku pola gry jest określane jako centralne miejsce. Piłka jest umieszczana w środkowej pozycji przed rozpoczęciem meczu. Gdy bramka zostanie zdobyta, obie drużyny rezygnują z kontroli nad piłką i zostaje ona ponownie ustawiona na środku boiska, gotowa na kolejne "rozpoczęcie". Pole gry jest otoczone klasą SoccerPitch. Pojedyncze wystąpienie tej klasy jest tworzone w pliku main.cpp. Obiekt SoccerPitch jest właścicielem instancji obiektów SoccerTeam, SoccerBall i Goal. Oto deklaracja klasy:

class SoccerPitch
{
public:
SoccerBall* m_pBall;
SoccerTeam* m_pRedTeam;
SoccerTeam* m_pBlueTeam;
Goal* m_pRedGoal;
Goal* m_pBlueGoal;
Te pierwsze składowe są oczywiste i opiszę odpowiednie klasy szczegółowo wkrótce.

// pojemnik na ściany graniczne
std :: vector m_vecWalls;

Granice boiska w środowisku Simple Soccer są reprezentowane przez Wall2D. Ściany są opisane segmentem linii z dwoma punktami końcowymi i normalną do segmentu linii reprezentującego kierunek skierowany w stronę. Możesz je zapamiętać z opisu zachowania kierownicy unikającego ściany.

// określa wymiary pola gry
Region * m_pPlayingArea;
Obiekt Region służy do opisu wymiarów boiska do piłki nożnej. Region przechowuje lewą górną, prawą dolną i środkową pozycję zadeklarowanego obszaru, a także numer identyfikacyjny (ID).

std :: vector m_Regions;

Piłkarze muszą wiedzieć, gdzie są na boisku i chociaż ich współrzędne x, y dają bardzo konkretną pozycję, warto również podzielić boisko na regiony, w których gracze mogą wykorzystać strategie. Aby to ułatwić, podziałka jest podzielona na osiemnaście obszarów, jak pokazano na rysunku



Na początku gry każdemu graczowi przypisywany jest region macierzysty. To będzie region, do którego wraca po zdobyciu bramki lub zakończeniu gry piłką. Region macierzysty gracza może się różnić w trakcie gry, w zależności od strategii drużyny. Na przykład, gdy atakujesz, korzystne jest, aby drużyna zajmowała pozycje na polu (w górę) bardziej niż podczas obrony.

bool m_bGameOn;

Zespoły mogą zapytać o tę wartość, aby sprawdzić, czy gra jest w trakcie, czy nie. (Gra nie jest włączona, jeśli bramka została właśnie zdobyta, a wszyscy gracze wracają na swoje pozycje początkowe.)

bool m_bGoalKeeperHasBall;

Ta wartość jest ustawiona na true, jeśli bramkarz którejkolwiek drużyny ma piłkę. Gracze mogą zapytać o tę wartość, aby pomóc im wybrać odpowiednie zachowanie. Na przykład, jeśli bramkarz ma piłkę, znajdujący się w pobliżu przeciwnik nie będzie próbował jej kopnąć.

/ * DODATKOWE SZCZEGÓŁY POMINIĘTE * /
public:
SoccerPitch(int cxClient, int cyClient);
~SoccerPitch();
void Update();
bool Render();
/* DODATKOWE SZCZEGÓŁY POMINIĘTE */
};
;

Funkcje SoccerPitch :: Update i SoccerPitch :: Render znajdują się na szczycie hierarchii aktualizacji i renderowania. Na każdym etapie aktualizacji metody te są wywoływane z głównej pętli gry, a z kolei wywoływane są odpowiednie metody renderowania i aktualizacji każdej innej jednostki gry

Bramki

Bramkę na prawdziwym boisku piłkarskim określa lewy słupek bramki i prawy słupek bramki. Gol zostaje zdobyty, jeśli jakakolwiek część piłki przekroczy linię bramkową - linię łączącą słupki bramki. Prostokątny obszar przed każdą bramką jest rysowany kolorem odpowiedniej drużyny, aby ułatwić odróżnienie strony każdej drużyny. Linia bramkowa to linia opisująca tył tego pudełka. Oto deklaracja klasy:

class Goal
{
private:
Vector2D m_vLeftPost;
Vector2D m_vRightPost;
//wektor reprezentujący kierunek do bramki
Vector2D m_vFacing;
//pozycja środka linii bramkowej
Vector2D m_vCenter;
//za każdym razem, gdy Scored() wykryje cel, jest on zwiększany
int m_iNumGoalsScored;
public:
Goal(Vector2D left, Vector2D right):m_vLeftPost(left),
m_vRightPost(right),
m_vCenter((left+right)/2.0),
m_iNumGoalsScored(0)
{
m_vFacing = Vec2DNormalize(right-left).Perp();
}
// Biorąc pod uwagę bieżącą pozycję piłki i poprzednią pozycję piłki,
// ta metoda zwraca wartość true, jeśli piłka przekroczy linię bramkową
// i przyrosty m_iNumGoalsScored
inline bool Scored (const SoccerBall * const ball);
/ * ZAPOMNIANE METODY AKCESORIA * /
};

Na każdym kroku metoda punktowana celu każdej drużyny jest wywoływana z poziomu SoccerPitch :: Aktualizacja. W przypadku wykrycia bramki gracze i piłka są resetowani do pozycji początkowych i są gotowi do rozpoczęcia.

Piłka

Piłka jest trochę bardziej interesująca. Dane i metody enkapsulacji piłki są zakodowane w klasie SoccerBall. Piłka się porusza, więc jej klasa dziedziczy po klasie MovingEntity, której używaliśmy wcześniej. Oprócz funkcji zapewnianej przez MovingEntity, SoccerBall ma również członków danych do rejestrowania ostatniej zaktualizowanej pozycji piłki oraz metod kopania piłki, testowania kolizji i obliczania przyszłej pozycji piłki. Kiedy prawdziwa piłka nożna zostanie kopnięta, delikatnie zwalnia, aby odpocząć z powodu tarcia o ziemię i działającego na nią oporu powietrza. Proste piłki nie istnieją w prawdziwym świecie, ale możemy modelować podobny efekt, stosując stałe opóźnienie (przyspieszenie ujemne) do ruchu piłki. Wielkość opóźnienia jest ustawiona w Params.ini jako wartość Tarcie. Oto pełna deklaracja klasy SoccerBall wraz z opisem kilku ważnych metod.

class SoccerBall : public MovingEntity
{
private:
//prowadzi rejestr pozycji piłki przy ostatniej aktualizacji
Vector2D m_vOldPos;
//wskaźnik do zawodnika (lub bramkarza), który posiada piłkę
PlayerBase* m_pOwner;
// lokalne odniesienie do ścian, które tworzą granicę boiska
// (używane w wykrywaniu kolizji)
const std::vector& m_PitchBoundary; // sprawdza, czy piłka zderzyła się ze ścianą i odbija // z odpowiednią prędkością
void TestCollisionWithWalls(const std::vector& walls);

Piłka sprawdza tylko kolizje z granicą boiska; nie testuje kolizji z zawodnikami, ponieważ piłka musi swobodnie poruszać się wokół i przez swoje "stopy" :

public:
SoccerBall(Vector2D pos,
double BallSize,
double mass,
std::vector& PitchBoundary):
//skonfiguruj klasę podstawową
MovingEntity(pos,
BallSize,
Vector2D(0,0),
-1.0, //maksymalna prędkość - nieużywane
Vector2D(0,1),
mass,
Vector2D(1.0,1.0), // skala - nieużywana
0, // wskaźnik obrotu - nieużywany
0), // maksymalna siła - nieużywana
m_PitchBoundary(PitchBoundary),
m_pOwner(NULL)
{}
// zaimplementuj klasę bazową Update
void Update(double time_elapsed);
// zaimplementuj klasę bazową Render
void Render();
// piłka nożna nie musi obsługiwać wiadomości
bool HandleMessage(const Telegram& msg){return false;}
// ta metoda przykłada siłę kierunkową do piłki (kopie ją!)
void Kick(Vector2D direction, double force);
// podana siła kopnięcia i odległość do obrotu określona przez start
// i punkty końcowe, ta metoda oblicza, jak długo zajmie
// piłce pokonanie odległości.
double TimeToCoverDistance(Vector2D from,
Vector2D to,
double force)const;
// ta metoda oblicza, gdzie będzie piłka w danym momencie
Vector2D FuturePosition(double time)const;
// służy to zawodnikom i bramkarzom do "pułapkowania" piłki - zatrzymania
// gra nie idzie. Zakłada się, że gracz chwytający w pułapki jest w posiadaniu
// piłka i m_pOwner są odpowiednio dostosowane
void Trap(PlayerBase* owner){m_vVelocity.Zero(); m_pOwner = owner;}
Vector2D OldPos()const{return m_vOldPos;}
// umieszcza piłkę w pożądanym miejscu i ustawia jej prędkość na zero
void PlaceAtPosition(Vector2D NewPos);
};

Zanim przejdę do opisu klas zawodników i drużyn, chciałbym omówić kilka publicznych metod SoccerBall, aby upewnić się, że rozumiesz matematykę, którą one zawierają. Metody te są często stosowane przez graczy, aby przewidzieć, gdzie będzie piłka w przyszłości lub przewidzieć, ile czasu zajmie piłka, aby osiągnąć pozycję. Projektując sztuczną inteligencję do gry sportowej / symulacji, będziesz często wykorzystywać swoje umiejętności matematyczne i fizyczne. O tak! Więc jeśli nie znasz teorii, nadszedł czas, aby przejść wróć do części matematyczno-fizycznej i poczytaj o niej; inaczej będziesz bardziej zagubiony niż raper w lesie deszczowym.

Uwaga 3D: Mimo że demo zostało zakodowane w 2D, w grze 3D zastosujesz dokładnie takie same techniki. Jest trochę bardziej złożona, ponieważ piłka odbije się i może poruszać się ponad głowami graczy, więc trzeba będzie dodać dodatkowe umiejętności gracza, aby wykonywać strzały z chipem i "główkować" piłkę, ale są to głównie względy fizyczne. AI jest mniej więcej taki sam; wystarczy dodać kilka dodatkowych stanów do FSM i kilka dodatkowych logik do sprawdzania wysokości piłki podczas obliczania przechwyceń i tym podobnych.

SoccerBall :: Future Position

Biorąc pod uwagę długość czasu jako parametr, FuturePosition oblicza, gdzie piłka będzie w tym czasie w przyszłości - zakładając, że jej trajektoria będzie nieprzerwana. Nie zapominaj, że piłka doświadcza siły tarcia o podłoże, co należy wziąć pod uwagę. Siła tarcia jest wyrażana jako stałe przyspieszenie działające przeciwnie do kierunku ruchu kuli (inaczej zwalnianie). Stała ta jest zdefiniowana w params.ini jako tarcie. Aby określić pozycję Pt kuli w czasie t, musimy obliczyć, jak daleko ona się przemieszcza, używając równania (1.87)

?x = u?t + 1/2 ?a?t2

gdzie a Δx to przebyta odległość, u to prędkość piłki po kopnięciu, a a to opóźnienie spowodowane tarciem.



Projektowanie AI

W drużynie Simple Soccer występują dwa typy piłkarzy: zawodnicy pola i bramkarze. Oba te typy wywodzą się z tej samej klasy podstawowej, PlayerBase. Oba wykorzystują skróconą wersję klasy SteeringBehaviors, którą widzieliście w ostatnim rozdziale, i obie mają własne maszyny stanów skończonych, z własnym zestawem stanów



Nie wszystkie metody każdej klasy są pokazane, ale daje dobre wyobrażenie o projekcie. Większość metod wymienionych dla PlayerBase i SoccerTeam obejmuje interfejs używany przez automat stanu gracza do kierowania logiką AI. (Pominąłem parametry każdej metody, aby pozwolić mi zmieścić się na schemacie na jednej stronie!) Zauważ, że SoccerTeam jest również właścicielem StateMachine, dzięki czemu zespół może zmienić swoje zachowanie w zależności od aktualnego stanu gry. Realizowanie AI na poziomie drużyny oprócz poziomu gracza tworzy tak zwaną AI warstwową. Ten rodzaj sztucznej inteligencji jest wykorzystywany we wszelkiego rodzaju grach komputerowych. W grach strategicznych czasu rzeczywistego (RTS) często znajdziesz wielopoziomową sztuczną inteligencję, w której wroga sztuczna inteligencja jest zwykle implementowana w kilku warstwach, powiedzmy, na poziomie jednostki, wojska i dowódcy. Zwróć też uwagę, jak gracze i ich drużyny mają możliwość wysyłania wiadomości. Wiadomości mogą być przekazywane od gracza do gracza (w tym bramkarzy) lub od drużyny piłkarskiej do gracza. W tej wersji demo gracze nie przekazują wiadomości do swojej drużyny. (Chociaż nie ma powodu, dla którego nie mogliby tego zrobić. Jeśli masz dobry powód, aby Twoi gracze wysyłali wiadomości do swojej drużyny, zrób to.) zobaczycie później w części. Ponieważ stan drużyny gracza w pewnym stopniu decyduje o tym, jak powinien się on zachowywać, Twoja podróż do wnętrzności sztucznej inteligencji Simple Soccer prawdopodobnie najlepiej rozpocząć od opisu klasy SoccerTeam. Po zrozumieniu, co sprawia, że drużyna tyka, przejdę do opisu, w jaki sposób zawodnicy i bramkarze wykorzystują swoją magię piłkarską.

Klasa SoccerTeam

Klasa SoccerTeam posiada instancje graczy, którzy tworzą drużynę piłkarską. Ma wskaźniki na boisko do piłki nożnej, przeciwnej drużyna, główny cel drużyny i cel przeciwnika. Dodatkowo ma wskaźniki dla "kluczowych" graczy na boisku. Poszczególni gracze mogą zapytać drużynę piłkarską i wykorzystać te informacje w logice automatu stanowego. Przede wszystkim opiszę role tych kluczowych graczy, a następnie przejdę do omówienia różnych stanów wykorzystywanych przez zespół Simple Soccer. Oto, w jaki sposób wskaźniki głównego gracza są deklarowane w prototypie klasy:

class SoccerTeam
{
private:
/ * DODATKOWE SZCZEGÓŁY POMINIĘTE * /
// wskaźniki do "kluczowych" graczy
PlayerBase* m_pReceivingPlayer;
PlayerBase* m_pPlayerClosestToBall;
PlayerBase* m_pControllingPlayer;
PlayerBase* m_pSupportingPlayer;
/* DODATKOWE SZCZEGÓŁY POMINIĘTE */
};

Gracz otrzymujący

Kiedy gracz kopie piłkę w kierunku innego gracza, gracz oczekujący na odbiór piłki, co nie jest zaskoczeniem, jest znany jako odbierający. W danym momencie przydzielony będzie tylko jeden odbiorca. Jeśli nie ma przydzielonego odbiorcya, ta wartość jest ustawiona na NULL.

Gracz najbliższy piłce

Ten wskaźnik wskazuje członka drużyny, który jest obecnie najbliżej piłki. Jak możesz sobie wyobrazić, znajomość tego rodzaju informacji jest przydatna, gdy gracz musi zdecydować, czy powinien ścigać piłkę, czy też zostawić ją innemu członkowi zespołu. Na każdym kroku drużyna piłkarska obliczy, który zawodnik jest najbliżej i będzie stale aktualizować ten wskaźnik. Dlatego podczas gry m_pPlayerClosestToBall nigdy nie będzie miał wartości NULL.

Gracz kontrolujący

Gracz kontrolujący to gracz, który dowodzi piłką. Oczywistym przykładem kontrolującego gracza jest ten, który ma zamiar podać koledze z drużyny. Mniej oczywistym przykładem jest gracz, który czeka na piłkę po podaniu. W tym ostatnim przykładzie, chociaż piłka może znajdować się nigdzie w pobliżu gracza odbierającego, mówi się, że gracz ma kontrolę, ponieważ, o ile nie zostanie przechwycony przez przeciwnika, odbiorca będzie następnym graczem, który może kopnąć piłkę. Gracz kontrolujący, poruszając się w górę w kierunku celu przeciwnika, jest często określany jako gracz atakujący lub, mówiąc prościej, jako atakujący. Jeśli drużyna nie kontroluje piłki, wskaźnik ten zostanie ustawiony na NULL.

Gracz wspierający

Gdy gracz przejmie kontrolę nad piłką, drużyna wyznaczy zawodnika wspierającego. Gracz wspierający będzie próbował przesunąć się na użyteczną pozycję dalej w górę pola od atakującego. Pozycje wspierające są oceniane na podstawie pewnych cech, takich jak łatwość podania napastnikowi przez piłkę do pozycji oraz prawdopodobieństwo zdobycia bramki z pozycji. Na przykład pozycja B na poniższym rysunku byłaby uważana za dobrą pozycję pomocniczą (dobry widok na bramkę przeciwnika, łatwą do podania), pozycję C tak bardzo taką pozycję wspierającą (uczciwy widok na bramkę przeciwnika, słaby potencjał podania), a pozycja D bardzo słaba pozycja podparcia (mały potencjał podania, brak strzału w bramkę, nie w górę ofensywy). Jeśli nie ma przydzielonego gracza wspierającego, ten wskaźnik wskaże NULL.



Pozycje wspierające są obliczane przez próbkowanie serii miejsc na boisku i przeprowadzanie na nich kilku testów, co daje łączny wynik. Pozycja z najwyższym wynikiem jest uważana za najlepsze miejsce wspierające lub BSS, jak to czasami nazywam. Osiąga się to za pomocą klasy o nazwie SupportSpotCalculator. Myślę, że teraz może być dobry moment, aby przejść do małej, ale ważnej stycznej, aby pokazać, jak działa ta klasa.

Obliczanie najlepszego miejsca wsparcia
Klasa SupportSpotCalculator oblicza BSS, oceniając liczbę pozycji punktowych próbkowanych z połowy boiska przeciwnika. Domyślne lokalizacje miejsc (dla drużyny czerwonej) pokazano na rysunku



Jak widać, wszystkie miejsca znajdują się na połowie boiska przeciwnika. Nie ma potrzeby próbkowania pozycji dalej w dół pola, ponieważ wspierający gracz zawsze będzie starał się znaleźć miejsce, które daje najlepszą szansę na oddanie strzału, i które nieuchronnie będzie znajdować się blisko bramki przeciwnika. Miejsce wsparcia ma pozycję i wynik, takie jak:

struct SupportSpot
{
Vector2D m_vPos;
double m_dScore;
SupportSpot(Vector2D pos, double val):m_vPos(pos),
m_dScore(value)
{}
};

Punkty są punktowane poprzez badanie każdego z nich kolejno i ocenianie ich pod kątem określonej jakości, na przykład tego, czy cel jest możliwy z pozycji miejsca lub jak daleko od kontrolującego gracza znajduje się miejsce. Wyniki dla każdej jakości są kumulowane, a miejsce o najwyższym wyniku jest oznaczane jako najlepsze miejsce wspierające. Gracz wspierający może następnie zbliżyć się do pozycji BSS, przygotowując się do podania od atakującego.

UWAGA Nie jest konieczne obliczanie BSS na każdym etapie aktualizacji; dlatego liczba wykonywanych obliczeń jest regulowana do czasów Support-SpotUpdateFreq razy na sekundę. Wartość domyślna ustawiona w params.ini wynosi raz na sekundę.

Aby dokładnie określić, jakie powinny być te cechy, musisz myśleć jak piłkarz. Jeśli biegłbyś po boisku do piłki nożnej, próbując postawić się na korzystnej pozycji wspierającej, jakie czynniki byś wziął pod uwagę? Prawdopodobnie cenisz pozycje, w których koledzy z drużyny mogliby podać piłkę. Na swojej mentalnej mapie boiska piłkarskiego wyobrażasz sobie siebie w każdym miejscu i bierzesz pod uwagę te pozycje, w których według ciebie atakujący mógłby bezpiecznie podać piłkę jako dobre pozycje, w których możesz się ustawić. SupportSpotCalculator robi to samo, przyznając każdemu spotowi spełniającemu ten warunek wynik równoważny wartości: Spot_CanPassScore (ustawiony jako 2.0 w params.ini). Poniższy rysunek pokazuje typową pozycję podczas gry, podkreślając wszystkie miejsca, które zostały ocenione pod kątem potencjału do podania.



Ponadto godne są pozycje, z których można strzelić gola

Uwaga. Dlatego SupportSpotCalculator przypisuje wynik Spot_CanScoreFromPositionScore do każdego miejsca, które przejdzie test strzału jest możliwe. Nie jestem ekspertem od piłki nożnej (daleki od tego!), Ale uważam, że umiejętność podania do miejsca powinna być oceniana wyżej niż umiejętność oddania strzału z miejsca - w końcu atakujący musi podać piłkę do zawodnika wspierającego, zanim będzie można wykonać próbę bramkową. Mając to na uwadze, domyślną wartością dla Spot_CanScoreFromPositionScore jest 1,0. Poniżej pokazuje tę samą pozycję jak rycina powyżej z punktami ocenianymi pod kątem potencjału strzału.



Innym aspektem, który może rozważyć gracz wspierający, jest celowanie na pozycję w pewnej odległości od kolegi z drużyny. Nie za daleko, aby przyjęcie nie było trudne i ryzykowne, i nie za blisko, aby przyjęcie nie zostało zmarnotrawione. Użyłem wartości 200 pikseli jako optymalnej odległości, w jakiej gracz wspierający powinien znajdować się z dala od gracza kontrolującego. W tej odległości spot otrzyma optymalny wynik Spot_DistFromControllingPlayerScore (domyślnie 2.0), a wyniki będą się zmniejszać dla odległości bliższych lub dalszych



Po zbadaniu każdej pozycji i zsumowaniu wszystkich wyników, miejsce o najwyższym wyniku jest uważane za najlepsze miejsce wspierające, a atakujący wspierający przesunie się, aby zająć tę pozycję w gotowości do otrzymania podania. Ta procedura określania BSS jest przeprowadzana w metodzie SupportSpotCalculator :: DetermineBestSupportingPosition. Oto kod źródłowy do sprawdzenia:

Vector2D SupportSpotCalculator::DetermineBestSupportingPosition()
{
//aktualizuj spoty tylko co kilka klatek
if (!m_pRegulator->AllowCodeFlow()&& m_pBestSupportingSpot)
{
return m_pBestSupportingSpot->m_vPos;
}
//zresetuj najlepsze miejsce wspierające
m_pBestSupportingSpot = NULL;
double BestScoreSoFar = 0.0;
std::vector::iterator curSpot;
for (curSpot = m_Spots.begin(); curSpot != m_Spots.end(); ++curSpot)
{
// najpierw usuń poprzedni wynik. (wynik jest ustawiony na jeden, aby
// widz mógł zobaczyć pozycje wszystkich miejsc, jeśli ma
// pomoce włączone)
curSpot->m_dScore = 1.0;
// Test 1. czy możliwe jest bezpieczne podanie z pozycji piłki // do tej pozycji?
if(m_pTeam->isPassSafeFromAllOpponents(m_pTeam->ControllingPlayer()->Pos(),
curSpot->m_vPos,
NULL,
Prm.MaxPassingForce))
{
curSpot->m_dScore += Prm.Spot_PassSafeStrength;
}
// Test 2. Ustal, czy można strzelić gola z tej pozycji.
if( m_pTeam->CanShoot(curSpot->m_vPos,
Prm.MaxShootingForce))
{
curSpot->m_dScore += Prm.Spot_CanScoreStrength;
}
// Test 3. obliczyć, jak daleko to miejsce jest od kontroli
//gracz. Im dalej, tym wyższy wynik. Wszelkie odległości dalej
// poza pikselami OptimalDistance nie otrzymuje się wyniku.
if (m_pTeam->SupportingPlayer())
{
const double OptimalDistance = 200.0;
double dist = Vec2DDistance(m_pTeam->ControllingPlayer()->Pos(),
curSpot->m_vPos);
double temp = fabs(OptimalDistance - dist);
if (temp < OptimalDistance)
{
// znormalizuj odległość i dodaj ją do wyniku
curSpot->m_dScore += Prm.Spot_DistFromControllingPlayerStrength *
(OptimalDistance-temp)/OptimalDistance;
}
}
//sprawdź, czy to miejsce ma jak dotąd najwyższy wynik
if (curSpot->m_dScore > BestScoreSoFar)
{
BestScoreSoFar = curSpot->m_dScore;
m_pBestSupportingSpot = &(*curSpot);
}
}
return m_pBestSupportingSpot->m_vPos;

Cóż, myślę, że "mała styczna" do omawiania tematu miejsc wsparcia zmieniła się w dość dużą! Zanim się rozproszyłem, mówiłem ci, jak klasa SoccerTeam zrobiła to, pamiętasz? Jak już wspomniałem, SoccerTeam jest właścicielem automatu stanu. Dzięki temu może zmieniać swoje zachowanie w zależności od tego, w jakim jest stanie. Przyjrzyjmy się teraz dostępnym stanom drużyny i ich wpływowi na zachowanie jej graczy.

SoccerTeam States

W dowolnym momencie drużyna piłkarska może znajdować się w jednym z trzech stanów:

Obrona, atak lub przygotowanie ForKickOff. Utrzymałem logikę tych stanów w bardzo prosty sposób - moim zamiarem jest pokazanie, jak zaimplementować wielopoziomową sztuczną inteligencję, a nie zademonstrowanie, jak tworzyć złożone taktyki piłkarskie - chociaż można je łatwo dodawać i modyfikować, aby tworzyć praktycznie dowolny typ zachowania zespołu, które możesz sobie wyobrazić. Jak wspomniałem wcześniej, gracze wykorzystują ideę "regionów", aby pomóc w pozycjonowaniu ich samych poprawnie na boisku. Stany drużyny używają tych regionów do kontrolowania, gdzie gracze powinni się poruszać, jeśli nie są w posiadaniu piłki lub nie wspierają / atakują. Na przykład w obronie rozsądne jest, aby drużyna piłkarska zbliżała swoich zawodników bliżej bramki gospodarzy, a podczas ataku gracze powinni poruszać się dalej w górę, bliżej bramki przeciwnika. Oto szczegółowe opisy każdego stanu zespołu.

PrepareForKickOff

Drużyna wchodzi w ten stan natychmiast po zdobyciu bramki. Metoda Enter ustawia wszystkie kluczowe wskaźniki dla graczy na NULL, zmienia ich rodzinne regiony z powrotem na pozycje początkowe i wysyła każdemu graczowi wiadomość z prośbą o powrót do swoich macierzystych regionów. W rzeczywistości coś takiego:

void PrepareForKickOff::Enter(SoccerTeam* team)
{
//reset key player pointers
team->SetControllingPlayer(NULL);
team->SetSupportingPlayer(NULL);
team->SetReceiver(NULL);
team->SetPlayerClosestToBall(NULL);
//send Msg_GoHome to each player.
team->ReturnAllFieldPlayersToHome();
}

W każdym cyklu Wykonania drużyna czeka, aż wszyscy gracze z obu drużyn znajdą się w swoich regionach macierzystych, w którym to momencie zmienia stan na Obrona i mecz rozpoczyna się ponownie.

void PrepareForKickOff::Execute(SoccerTeam* team)
{
//jeśli obie drużyny są na pozycji, rozpocznij grę
if (team->AllPlayersAtHome() && team->Opponents()->AllPlayersAtHome())
{
team->ChangeState(team, Defending::Instance());
}

W obronie

Metoda Enter stanu broniącego drużyny piłkarskiej zmienia pozycje domowe wszystkich członków drużyny, którzy będą znajdować się na połowie boiska drużyny. Zbliżanie wszystkich graczy do bramki gospodarza w ten sposób utrudnia drużynie przeciwnej manewrowanie piłką i zdobycie bramki. Poniższy rysunek pokazuje pozycje gospodarzy dla czerwonej drużyny, gdy są w stanie Obrony.



void Defending::Enter(SoccerTeam* team)
{
//określają regiony macierzyste dla tego stanu każdego z graczy
const int BlueRegions[TeamSize] = {1,6,8,3,5};
const int RedRegions[TeamSize] = {16,9,11,12,14};
//skonfiguruj rodzinne regiony gracza
if (team->Color() == SoccerTeam::blue)
{
ChangePlayerHomeRegions(team, BlueRegions);
}
else
{
ChangePlayerHomeRegions(team, RedRegions);
}
// jeśli gracz jest w stanie Wait lub ReturnToHomeRegion, jego
// cel sterowania musi zostać zaktualizowany do nowego regionu macierzystego
team->UpdateTargetsOfWaitingPlayers();
}

Metoda Execution w stanie Broniąca nieustannie sprawdza zespół, czy ma kontrolę nad piłką. Gdy tylko drużyna przejmie kontrolę, zmienia stan na Atak.

void Defending::Execute(SoccerTeam* team)
{
//jeśli w stanie zmiany kontroli
if (team->InControl())
{
team->ChangeState(team, Attacking::Instance()); return;
}
}

Atakowanie

Ponieważ metoda Enter stanu Atakowania wygląda identycznie jak w przypadku stanu Obrona, nie będę marnować miejsca i nie wymienię go tutaj. Jedyna różnica polega na tym, że graczom przypisuje się różne regiony rodzinne. Regiony przypisane graczom drużyny czerwonej podczas Ataku pokazano na rysunku



Jak widać, gracze ustawiają się znacznie bliżej celu przeciwnika. Daje im to większą szansę na utrzymanie piłki na połowie boiska przeciwnika, a tym samym większą szansę na zdobycie bramki. Zauważ, jak jeden gracz jest trzymany z tyłu, ustawiony tuż przed bramkarzem, aby zapewnić odrobinę obrony, jeśli przeciwnik uwolni się z piłką i wybiegnie do bramki drużyny. Metoda wykonania stanu Ataku jest również podobna do metody Obrony z jednym dodatkiem. Gdy drużyna przejmie kontrolę nad piłką, drużyna natychmiast iteruje wszystkich graczy, aby ustalić, który zapewni najlepsze wsparcie dla atakującego. Po przydzieleniu gracza wsparcia, wesoło ruszy w kierunku najlepszego miejsca wsparcia, zgodnie z procesem, który omówiliśmy wcześniej.

void Attacking::Execute(SoccerTeam* team)
{
// jeśli ten zespół nie jest już w stanie zmiany kontroli
if (!team->InControl())
{
team->ChangeState(team, Defending::Instance()); return;
}
// obliczyć najlepszą pozycję dla dowolnego atakującego pomocnika, na który się przeniesie
team->DetermineBestSupportingPosition();
}

Na razie wystarczy o klasie SoccerTeam. Rzućmy okiem na sposób implementacji graczy.

Gracze pola

Gracze na boisku to faceci, którzy biegają po boisku, podają piłkę i strzelają do bramki przeciwnika. Istnieją dwa rodzaje graczy terenowych: atakujący i obrońcy. Oba są tworzone jako obiekty tej samej klasy, FieldPlayer, ale wyliczona zmienna składowa jest ustawiona w celu określenia ich roli. Obrońcy pozostają głównie z tyłu pola, chroniąc bramkę gospodarzy, a atakujący mają więcej swobody w poruszaniu się w górę pola, w kierunku bramki przeciwnika.

Ruch gracza pola

Zawodnik na boisku ma kurs wyrównany do prędkości i wykorzystuje zachowania kierownicze do przemieszczania się na pozycję i gonienia piłki. Gdy gracz nieruchomy obraca się w kierunku piłki,nie robi tego, aby postrzegać piłkę, ponieważ zawsze wie, gdzie jest piłka (od bezpośredniego zapytania do świata gry), ale ponieważ ma większą szansę na podanie natychmiast po przechwyceniu i ponieważ wygląda lepiej dla naszych ludzkich oczu . Pamiętaj, że chodzi o stworzenie iluzji inteligencji, a nie twardej sztucznej inteligencji, jak badają naukowcy. Większość ludzi zakłada, że jeśli komputer śledzi piłkę głową, to musi ona "obserwować" piłkę. Tworząc graczy, którzy zawsze śledzą piłkę, zapewniamy również, że nic dziwnego się nie wydarzy - na przykład gracz odbierający i kontrolujący piłkę, gdy jest ona zwrócona w przeciwnym kierunku. Takie rzeczy przełamałyby iluzję, pozostawiając gracza czującego się oszukanym i niezadowolonym. Jestem pewien, że sam doświadczyłeś tego uczucia podczas grania w gry. Wystarczy niewielkie, podejrzanie wyglądające zdarzenie, aby zniszczyć zaufanie gracza do sztucznej inteligencji. Zawodnicy na boisku poruszają się po boisku, wykorzystując przylot i szukają zachowań, aby kierować się w kierunku celu zachowania kierowania lub ścigają, aby ścigać przewidywaną pozycję piłki w przyszłości. Każde wymagane zachowanie kierowania jest zwykle włączane w metodzie Enter stanu i metodzie Exit, co pozwala mi miło omawiać stany, które może zająć gracz terenowy.

Stany graczy pola

W prawdziwym życiu piłkarze muszą nauczyć się zestawu umiejętności, aby kontrolować piłkę wystarczająco dobrze, aby koordynować grę zespołową i strzelać bramki. Robią to przez niekończące się godziny ćwiczeń i powtarzania tych samych ruchów. Sami piłkarze nie muszą ćwiczyć, ale polegają na tobie, programiście, aby wyposażyć ich w umiejętności, których potrzebują, aby dobrze grać. Skończona maszyna stanu gracza terenowego wykorzystuje osiem stanów:

•  GlobalPlayerState
•  Wait
•  ReceiveBall
•  KickBall
•  Drybling
•  ChaseBall
•  ReturnToHomeRegion
•  SupportAttacker

Zmiany stanu są dokonywane albo w logice samego stanu, albo gdy gracz otrzymuje wiadomość od innego gracza (na przykład w celu otrzymania piłki).

GlobalPlayerState

Głównym celem globalnego stanu gracza pola jest przesłanie routera. Chociaż większość zachowań gracza jest realizowana przez logikę zawartą w każdym z jego stanów, pożądane jest również wdrożenie pewnej formy współpracy gracza za pośrednictwem systemu komunikacji. Dobrym przykładem tego jest sytuacja, gdy gracz wspierający znajduje się na korzystnej pozycji i prosi kolegę z drużyny o podanie. Aby ułatwić graczom komunikację, zaimplementowano sprawdzony system przesyłania wiadomości, o którym dowiedziałeś się w części mat-fiz. W Simple Soccer jest pięć wiadomości. Oni są:

•  Msg_SupportAttacker
•  Msg_GoHome
•  Msg_ReceiveBall
•  Msg_PassToMe
•  Msg_Wait

Wiadomości są wyliczone w pliku SoccerMessages.h. Rzućmy okiem na sposób przetwarzania każdego z nich.

bool GlobalPlayerState::OnMessage(FieldPlayer* player, const Telegram& telegram)
{
switch(telegram.Msg)
{
case Msg_ReceiveBall:
{
//ustaw cel
player->Steering()->SetTarget(*(Vector2D*)(telegram.ExtraInfo));
//zmiana stanu
player->ChangeState(player, ReceiveBall::Instance());
return true;
}
break;

Msg_ReceiveBall jest wysyłany do gracza odbierającego po podaniu przekazania. Pozycja celu podania jest zapisywana jako cel zachowania kierowania odbiorcy. Gracz odbierający potwierdza komunikat, zmieniając stan na ReceiveBall.

case Msg_SupportAttacker:
{
//jeśli już obsługuje, po prostu wróć
if (player->CurrentState() == SupportAttacker::Instance()) return true;
//ustaw cel jako najlepszą pozycję wspierającą
player->Steering()->SetTarget(player->Team()->GetSupportSpot());
//zmień stan
player->ChangeState(player, SupportAttacker::Instance());
return true;
}
break;

Msg_SupportAttacker jest wysyłany przez gracza kontrolującego z prośbą o wsparcie podczas próby przesunięcia piłki dalej w górę pola. Gdy gracz otrzyma tę wiadomość, ustawia swój cel sterowania na najlepsze miejsce wspierające, a następnie zmienia stan na SupportAttacker.

case Msg_GoHome:
{
player->SetDefaultHomeRegion();
player->ChangeState(player, ReturnToHomeRegion::Instance());
return true;
}
break;

Gdy gracz otrzyma tę wiadomość, wraca do swojego rodzinnego regionu. Jest często transmitowany przez bramkarzy przed kopnięciem bramki i przez "boisko", aby przenieść zawodników z powrotem na ich pozycje początkowe między bramkami.

case Msg_Wait:
{
//zmień stan
player->ChangeState(player, Wait::Instance());
return true;
}
break;

Msg_Wait instruuje gracza, aby poczekał na bieżącej pozycji.

case Msg_PassToMe:
{
//uzyskaj pozycję gracza żądającego podania
FieldPlayer* receiver = (FieldPlayer*)(telegram.ExtraInfo);
// jeśli piłka nie znajduje się w zasięgu kopnięcia lub zawodnik jej nie ma
// okno, w którym może wykonać kopnięcie, ten gracz nie może spasować
// piłkę do gracza zgłaszającego żądanie.
if (!player->BallWithinKickingRange())
{
return true;
}
//dokonaj podania
player->Ball()->Kick(receiver->Pos() - player->Ball()->Pos(),
Prm.MaxPassingForce);
//powiadom odbiorcę, że nadchodzi przepustka
Dispatch->DispatchMsg(SEND_MSG_IMMEDIATELY,
player->ID(),
receiver->ID(),
Msg_ReceiveBall,
NO_SCOPE,
&receiver->Pos());
//zmień stan
player->ChangeState(player, Wait::Instance());
player->FindSupport();
return true;
}
break;

Msg_PassToMe jest używany w kilku sytuacjach, głównie gdy zawodnik wspierający przesunął się na pozycję i uważa, że ma duże szanse na zdobycie bramki. Gdy gracz otrzyma tę wiadomość, podaje piłkę do gracza proszącego (jeśli podanie może być wykonane bezpiecznie).

}//end switch
return false;
}

Oprócz OnMessage stan globalny implementuje również metodę Execute. Obniża to maksymalną prędkość zawodnika, jeśli znajduje się on blisko piłki, aby symulować sposób, w jaki piłkarze poruszają się wolniej, gdy są w posiadaniu piłki.

void GlobalPlayerState::Execute(FieldPlayer* player)
{
//jeśli gracz jest w posiadaniu i znajduje się blisko piłki, zmniejsz jego maksymalną prędkość
if((player->BallWithinReceivingRange()) &&
(player->Team()->ControllingPlayer() == player))
{
player->SetMaxSpeed(Prm.PlayerMaxSpeedWithBall);
}
else
{
player->SetMaxSpeed(Prm.PlayerMaxSpeedWithoutBall);
}
}

ChaseBall

Gdy zawodnik jest w stanie ChaseBall, będzie szukał aktualnej pozycji piłki, próbując dostać się w zasięgu kopnięcia. Gdy gracz wejdzie w ten stan, jego zachowanie wyszukiwania jest aktywowane w następujący sposób:

void ChaseBall::Enter(FieldPlayer* player)
{
player->Steering()->SeekOn();
}

Podczas aktualizacji metody Wykonaj gracz zmieni stan na KickBall, jeśli piłka znajdzie się w zasięgu kopnięcia. Jeśli piłka nie znajduje się w zasięgu, zawodnik będzie ją gonił, dopóki zawodnik pozostanie najbliższym członkiem drużyny.

void ChaseBall::Execute(FieldPlayer* player)
{
//jeśli piłka znajduje się w zasięgu kopnięcia, gracz zmienia stan na KickBall.
if (player->BallWithinKickingRange())
{
player->ChangeState(player, KickBall::Instance());
return;
}
// jeśli gracz jest najbliżej piłki, powinien zachować
// gonię to
if (player->isClosestTeamMemberToBall())
{
player->Steering()->SetTarget(player->Ball()->Pos());
return;
}
/// jeśli gracz nie jest już najbliżej piłki, powinien wrócić
// do swojego rodzinnego regionu i poczekaj na kolejną okazję
player->ChangeState(player, ReturnToHomeRegion::Instance());
}

Kiedy gracz wyjdzie z tego stanu, zachowanie poszukiwania jest dezaktywowane
void ChaseBall::Exit(FieldPlayer* player)
{
player->Steering()->SeekOff();
}

Oczekiwanie

W stanie oczekiwania gracz pozostanie pozycjonowany w miejscu określonym przez cel zachowania kierownicy. Jeśli gracz zostanie wypchnięty z pozycji przez innego gracza, wróci na pozycję. Istnieje kilka warunków wyjścia dla tego stanu"

•  Jeśli oczekujący gracz znajdzie się na polu walki z kolegą z drużyny, który kontroluje piłkę, wyśle do niego wiadomość z prośbą o podanie piłki. Wynika to z faktu, że pożądane jest, aby piłkę dostać się jak najdalej w górę i jak najszybciej. Jeśli jest to bezpieczne, członek drużyny wykona podanie, a zawodnik oczekujący zmieni stan na odbiór piłki.
•  Jeśli piłka zbliży się do oczekującego gracza niż jakikolwiek inny członek drużyny i nie ma przydzielonego gracza odbierającego, zmieni stan na ChaseBall.

void Wait::Execute(FieldPlayer* player)
{
//jeśli gracz został wyrzucony z pozycji, wróć na pozycję
if (!player->AtTarget())
{
player->Steering()->ArriveOn();
return;
}
else
{
player->Steering()->ArriveOff();
player->SetVelocity(Vector2D(0,0));
//gracz powinien patrzeć na piłkę!
player->TrackBall();
}
// jeśli drużyna tego gracza kontroluje ORAZ ten gracz nie jest atakującym
// AND jest dalej w polu niż atakujący, który powinien poprosić o podanie
if ( player->Team()->InControl() &&
(!player->isControllingPlayer()) &&
player->isAheadOfAttacker() )
{
player->Team()->RequestPass(player);
return;
}
if (player->Pitch()->GameOn())
{
// jeśli piłka jest bliżej tego zawodnika niż jakikolwiek inny członek zespołu ORAZ
// nie ma przypisanego odbiornika ORAZ żaden bramkarz nie ma
// piłka, idź za nią
if (player->isClosestTeamMemberToBall() &&
player->Team()->Receiver() == NULL &&
!player->Pitch()->GoalKeeperHasBall())
{
player->ChangeState(player, ChaseBall::Instance());
return;
}
}
}

ReceiveBall

Gracz przechodzi w stan ReceiveBall podczas przetwarzania komunikatu Msg_ReceiveBall. Ta wiadomość jest wysyłana do gracza odbierającego przez gracza, który właśnie wykonał podanie. Pole ExtraInfo w telegramie zawiera pozycję docelową piłki, dzięki czemu można odpowiednio ustawić cel sterujący gracza odbierającego, pozwalając odbiorcy przesunąć się na pozycję, gotowy do przechwycenia piłki. Zawsze może być tylko jeden gracz z każdej drużyny w stanie ReceiveBall - nie byłoby dobrą taktyką, aby dwóch lub więcej graczy próbowało przechwycić tę samą przepustkę, więc pierwszą rzeczą, jaką robi metoda Enter tego stanu, jest zaktualizowanie odpowiedniego Wskaźniki SoccerTeam, aby umożliwić innym członkom zespołu ich zapytanie w razie potrzeby. Aby stworzyć bardziej interesującą i naturalnie wyglądającą grę, istnieją dwie metody otrzymania piłki. Jedna metoda wykorzystuje zachowanie przylotu do kierowania w kierunku docelowej pozycji piłki; drugi wykorzystuje zachowanie pościgowe do ścigania piłki. Gracz wybiera między nimi w zależności od wartości ChanceOfUsingArriveTypeReceiveBehavior, niezależnie od tego, czy przeciwnik znajduje się w promieniu zagrożenia, i czy odbiornik znajduje się na trzeciej pozycji boiska najbliżej celu przeciwnika (nazywam ten obszar "gorącym" region").

void ReceiveBall::Enter(FieldPlayer* player)
{
//powiadom drużynę, że ten gracz otrzymuje piłkę
player->Team()->SetReceiver(player);
//ten gracz jest teraz także graczem kontrolującym
player->Team()->SetControllingPlayer(player);
// istnieją dwa typy zachowania odbiorczego. Jedno użycie przyjeżdża do kierowania
// odbiornik do pozycji wysłanej przez przechodnia w jego telegramie
// inne używa zachowania pościgowego do ścigania piłki.
// Ta instrukcja wybiera między nimi w zależności od prawdopodobieństwa
// ChanceOfUsingArriveTypeReceiveBehavior, niezależnie od tego, czy jest przeciwny
// gracz jest blisko gracza odbierającego i tego, czy odbiera
// gracz znajduje się w "gorącym regionie" przeciwnika (trzeci na boisku najbliższym
// do bramki przeciwnika)
const double PassThreatRadius = 70.0;
if ((player->InHotRegion() ||
RandFloat() < Prm.ChanceOfUsingArriveTypeReceiveBehavior) &&
!player->Team()->isOpponentWithinRadius(player->Pos(), PassThreatRadius))
{
player->Steering()->ArriveOn();
}
else
{
player->Steering()->PursuitOn();
}
}

Metoda wykonania jest prosta. Zawodnik odbierający przesunie się na pozycję i pozostanie tam, chyba że piłka dotrze na określoną odległość lub jeśli jego drużyna straci kontrolę nad piłką, w którym to momencie gracz przejdzie do stanu ChaseBall.

void ReceiveBall::Execute(FieldPlayer* player)
{
// jeśli piłka zbliży się wystarczająco do gracza lub jego drużyna straci kontrolę
// powinien zmienić stan, aby gonić piłkę
if (player->BallWithinReceivingRange() || !player->Team()->InControl())
{
player->ChangeState(player, ChaseBall::Instance());
return;
}
// cel gracza musi być stale aktualizowany o pozycję piłki
// jeśli do podążania za piłką zastosowano zachowanie kierowania pościgowego.
if (player->Steering()->PursuitIsOn())
{
player->Steering()->SetTarget(player->Ball()->Pos());
}
// jeśli gracz "dotarł" do celu sterowania, powinien poczekać i
// odwróć się twarzą do piłki
if (player->AtTarget())
{
player->Steering()->ArriveOff();
player->Steering()->PursuitOff();
player->TrackBall();
player->SetVelocity(Vector2D(0,0));
}
}

KickBall

Jeśli jest jedna rzecz, którą piłkarze lubią robić więcej niż upić się i przytulać, to kopanie piłki. O tak. Oni to kochają. Zwykli piłkarze nie różnią się. Cóż, myślę, że się nie upijają i nie przytulają, ale lubią kopać. Prosty piłkarz musi być w stanie kontrolować i kopać piłkę na wiele sposobów. Musi być w stanie wykonać rzut na bramkę przeciwnika, posiadać umiejętności niezbędne do podania piłki innemu zawodnikowi i umieć kozłować. Gdy gracz uzyska kontrolę nad piłką, powinien wybrać najbardziej odpowiednią opcję do użycia w dowolnym momencie. Stan KickBall implementuje logikę rzutów i podania. Jeśli z jakiegoś powodu gracz nie może wykonać strzału lub podanie nie jest konieczne, stan gracza zostanie zmieniony na Drybling. Gracz nie może pozostać w stanie KickBall dłużej niż jeden cykl aktualizacji; niezależnie od tego, czy piłka zostanie kopnięta, czy nie, gracz zawsze zmieni stan gdzieś w drodze przez logikę stanu. Gracz wchodzi w ten stan, jeśli piłka znajdzie się w odległości PlayerKickingDistance od swojej pozycji. Pozwól, że przeprowadzę cię przez kod źródłowy:

void KickBall::Enter(FieldPlayer* player)
{
//poinformuj zespół, że ten gracz kontroluje
player->Team()->SetControllingPlayer(player);
//gracz może wykonać tyle prób kopnięcia na sekundę.
if (!player->isReadyForNextKick())
{
player->ChangeState(player, ChaseBall::Instance());
}
}

Metoda Enter najpierw informuje drużynę, że ten gracz jest graczem kontrolującym, a następnie sprawdza, czy gracz może kopnąć piłkę w tym kroku aktualizacji. Gracze mogą wykonywać kopnięcia tylko kilka razy na sekundę, z częstotliwością zapisaną w zmiennej PlayerKickFrequency. Jeśli gracz nie może wykonać próby kopnięcia, jego stan zostaje zmieniony na ChaseBall i będzie on kontynuował bieg po piłce. Liczba przypadków, w których gracz może kopnąć piłkę na sekundę, jest ograniczona, aby zapobiec nieprawidłowym zachowaniom. Na przykład, bez ograniczeń, mogą wystąpić sytuacje, w których piłka zostaje kopnięta, gracz przechodzi w stan oczekiwania, a następnie, ponieważ piłka wciąż znajduje się w zasięgu kopania, ułamek sekundy później gracze kopią ją ponownie. Ze względu na sposób obchodzenia się z fizyką piłki może to spowodować gwałtowny, nienaturalny ruch piłki.

void KickBall::Execute(FieldPlayer* player)
{
// oblicz iloczyn iloczynu wektora wskazującego na piłkę
// i kierunek gracza
Vector2D ToBall = player->Ball()->Pos() - player->Pos();
double dot = player->Heading().Dot(Vec2DNormalize(ToBall));
// nie może kopnąć piłki, jeśli bramkarz jest w posiadaniu lub jeśli jest w posiadaniu
// za odtwarzaczem lub jeśli jest już przypisany odbiornik. Więc tylko
// kontynuuj pogoń za piłką
if (player->Team()->Receiver() != NULL ||
player->Pitch()->GoalKeeperHasBall() ||
(dot < 0) )
{
player->ChangeState(player, ChaseBall::Instance());
return;
}

Po wprowadzeniu metody wykonania oblicza się iloczyn punktowy głowy gracza i wektora wskazującego piłkę, aby ustalić, czy piłka znajduje się za, czy przed graczem. Jeśli piłka jest z tyłu lub zawodnik już czeka na jej odbiór, lub jeden z bramkarzy ma piłkę, stan gracza jest zmieniany, tak aby dalej go gonił. Jeśli gracz jest w stanie kopnąć piłkę, logika stanu określa, czy można wykonać rzut do bramki. W końcu gole są celem gry, więc naturalnie powinna być to pierwsza rzecz brana pod uwagę, gdy gracz uzyskuje kontrolę nad piłką.

/ * Próba strzału w bramkę * /
// iloczyn skalarny służy do regulacji siły strzału. Więc
// piłka jest bezpośrednio przed zawodnikiem, tym silniejsze jest kopnięcie
double power = Prm.MaxShootingForce * dot;
Zauważ, że siła strzału jest proporcjonalna do tego, jak bezpośrednio przed graczem znajduje się piłka. Jeśli piłka znajduje się z boku, moc, z jaką można wykonać strzał, jest zmniejszona.
// jeśli strzał jest możliwy, ten wektor zachowa pozycję wzdłuż
// linia bramkowa przeciwnika, do której gracz powinien dążyć.
br>
// jeśli zostanie ustalone, że gracz może strzelić gola z tej pozycji
// LUB jeśli powinien po prostu kopnąć piłkę, gracz spróbuje
// zrobić strzał
if (player->Team()->CanShoot(player->Ball()->Pos(),
power,
BallTarget) ||
(RandFloat() < Prm.ChancePlayerAttemptsPotShot))
{

Metoda CanShoot określa, czy istnieje potencjalny strzał na bramkę. W przypadku potencjalnego strzału CanShoot zwróci wartość true i zapisze pozycję, do której gracz powinien dążyć w wektorowym BallTarget. Jeśli zwróci fałsz, sprawdzamy, czy należy wykonać "kosmetyczny" potshot (BallTarget zachowa lokalizację ostatniej pozycji uznanej za niepoprawną przez CanShoot, więc wiemy, że strzał jest nieudany). Powodem, dla którego od czasu do czasu robi się potshot, jest ożywienie gry, dzięki czemu ludzki obserwator wygląda o wiele bardziej ekscytująco; może szybko stać się nudny, jeśli gracze komputerowi zawsze zdobywają punkty po bramce. Od czasu do czasu losowy potshot wprowadza trochę niepewności i sprawia, że gra jest znacznie przyjemniejsza.

// dodaj trochę hałasu do kopnięcia. Nie chcemy graczy, którzy są
// zbyt dokładne! Ilość hałasu można regulować poprzez zmianę
//Prm.PlayerKickingAccuracy
BallTarget = AddNoiseToKick (player-> Ball () -> Pos (), BallTarget);
// w tym kierunku zostanie kopnięta piłka
Vector2D KickDirection = BallTarget - player->Ba

Piłkę kopie się, wywołując metodę SoccerBall :: Kick z żądanym nagłówkiem. Ponieważ idealni gracze wykonujący doskonałe kopnięcia cały czas nie zapewniają bardzo realistycznie wyglądającej piłki nożnej, do kierunku kopnięcia dodawany jest hałas. Zapewnia to, że gracze czasami wykonują słabe kopnięcia.

//change state
player->ChangeState(player, Wait::Instance());
player->FindSupport();
return;
}

Po kopnięciu piłki gracz przechodzi w stan oczekiwania i prosi o pomoc innego członka drużyny, wywołując metodę PlayerBase :: FindSupport. FindSupport "prosi" zespół o określenie partnera, który najlepiej nadaje się do zapewnienia wsparcia, oraz o wysłanie żądania za pośrednictwem systemu przesyłania wiadomości, aby członek zespołu wszedł w stan SupportAttacker. Stan następnie zwraca kontrolę nad metodą aktualizacji odtwarzacza. Jeśli nie jest możliwy strzał w bramkę, gracz rozważa podanie. Gracz rozważy tę opcję tylko wtedy, gdy grozi jej przeciwnik. Gracza uważa się za zagrożonego przez innego, gdy oba są w odległości mniejszej niż piksele PlayerComfortZone, a przeciwnik znajduje się przed płaszczyzną gracza. Wartość domyślna jest ustawiona w pliku params.ini na 60 pikseli. Większa wartość spowoduje, że gracze wykonają więcej podań, a mniejsza wartość spowoduje bardziej udane rozwiązania.

/ * Próba podania graczowi * /
// jeśli odbiorca zostanie znaleziony, wskaże to
PlayerBase* receiver = NULL;
power = Prm.MaxPassingForce * dot;
// sprawdź, czy są jacyś potencjalni kandydaci dostępni do otrzymania przepustki
if (player->isThreatened() &&
player->Team()->CanPass(player,
receiver,
BallTarget,
power,
Prm.MinPassDist))
{
//dodaj trochę hałasu do kopnięcia
BallTarget = AddNoiseToKick(player->Ball()->Pos(), BallTarget);
Vector2D KickDirection = BallTarget - player->Ball()->Pos();
player->Ball()->Kick(KickDirection, power);
//powiadom odbiorcę, że nadchodzi podanie
Dispatch->DispatchMsg(SEND_MSG_IMMEDIATELY,
player->ID(),
receiver->ID(),
Msg_ReceiveBall,
NO_SCOPE,
&BallTarget);

Metoda FindPass sprawdza wszystkich przyjaznych graczy, aby znaleźć członka drużyny znajdującego się najdalej na boisku, w pozycji, w której można wykonać podanie bez przechwycenia. Jeśli zostanie znalezione prawidłowe podanie, zostanie wykonane kopnięcie (z dodatkowym hałasem, jak poprzednio), a odbiorca zostanie powiadomiony, wysyłając wiadomość o zmianie stanu na ReceiveBall.

// gracz powinien czekać na swojej aktualnej pozycji, chyba że otrzyma takie polecenie
//Inaczej
player->ChangeState(player, Wait::Instance());
player->FindSupport();
return;
}

Jeśli logika gry płynie do tego punktu, nie znaleziono ani odpowiedniego podania, ani próby bramkowej. Gracz nadal ma piłkę, więc przechodzi ona w stan Dryblowania. (Warto zauważyć, że nie jest to jedyny czas, który upływa - członkowie drużyny mogą prosić o podanie od graczy, wysyłając im odpowiednią wiadomość).

// nie może strzelać ani podawać, więc kozłuj piłkę w górę
else
{
player->FindSupport();
player->ChangeState(player, Dribble::Instance());
}
}

Dryblowanie

Drybling jest czymś, z czym dzieci świetnie sobie radzą, z obu stron… ale słowo to zostało również przyjęte w grze w piłkę nożną, aby opisać sztukę poruszania piłką wzdłuż boiska w serii małych kopnięć i kresek. Korzystając z tej umiejętności, gracz może obracać się w miejscu lub poruszać się zwinnie wokół przeciwnika, zachowując kontrolę nad piłką. Ponieważ jednym z ćwiczeń na końcu będzie próba ulepszenia tej umiejętności, wdrożyłem tylko prostą metodę dryblingu, dającą graczowi wystarczającą możliwość poruszania się grą w rozsądnym tempie. Metoda Enter po prostu informuje resztę zespołu, że zakłada się, że zawodnik dryblujący ma kontrolę nad piłką.

void Dribble::Enter(FieldPlayer* player)
{
//let the team know this player is controlling
player->Team()->SetControllingPlayer(player);
}

Metoda wykonania zawiera większość logiki AI. Najpierw sprawdza się, czy piłka znajduje się pomiędzy zawodnikiem a jego bramką gospodarza (boisko gracza). Ta sytuacja jest niepożądana, ponieważ gracz chce przesunąć piłkę tak daleko, jak to możliwe. Dlatego gracz musi się odwrócić, zachowując kontrolę nad piłką. Aby to osiągnąć, gracze wykonują serię bardzo małych kopnięć w kierunku (45 stopni) od kierunku zwróconego w ich stronę. Po każdym małym kopnięciu gracz zmienia stan na ChaseBall. Jeśli zostanie to zrobione kilka razy w krótkich odstępach czasu, spowoduje to obrócenie gracza i piłki, dopóki nie zmierzą we właściwym kierunku (w kierunku bramki przeciwnika). Jeśli piłka zostanie ustawiona nad polem gracza, gracz popchnie ją na krótką odległość do przodu, a następnie zmieni stan na ChaseBall, aby podążać za nią.

void Dribble::Execute(FieldPlayer* player)
{
double dot = player->Team()->HomeGoal()->Facing().Dot(player->Heading());
// jeśli piłka jest pomiędzy zawodnikiem a bramką gospodarzy, musi się obrócić
// wykonując piłkę, wykonując wiele małych kopnięć i obrotów, aż gracz
// jest skierowany we właściwym kierunku
if (dot < 0)
{
// nagłówek gracza zostanie obrócony o niewielką ilość (Pi / 4)
// a następnie piłka zostanie kopnięta w tym kierunku
Vector2D direction = player->Heading();
// oblicz znak (+/-) kąta między kursem gracza a
// skierowany w kierunku bramki, aby gracz obracał się w
// poprawny kierunek
double angle = QuarterPi * -1 *
player->Team()->HomeGoal()->Facing().Sign(player->Heading());
Vec2DRotateAroundOrigin(direction, angle);
// ta wartość działa dobrze, gdy gracz próbuje kontrolować
// piłka i obrót w tym samym czasie
const double KickingForce = 0.8;
player->Ball()->Kick(direction, KickingForce);
}
// kopnij piłkę w dół pola
else
{
player->Ball()->Kick(player->Team()->HomeGoal()->Facing(),
Prm.MaxDribbleForce);
}
// gracz kopnął piłkę, więc musi teraz zmienić stan, aby podążać za nią
player->ChangeState(player, ChaseBall::Instance());
return;
}

SupportAttacker

Gdy gracz uzyskuje kontrolę nad piłką, natychmiast prosi o wsparcie, wywołując metodę PlayerBase :: FindSupport. FindSupport bada każdego członka zespołu po kolei, aby ustalić, który gracz jest najbliżej najlepszego miejsca wsparcia (obliczanego co kilka kroków przez SupportSpot-Kalkulator) i wiadomości, które gracz zmienia stan na SupportAttacker. Po wejściu w ten stan zachowanie gracza przy włączaniu jest włączane, a jego cel sterujący ustawiony jest na lokalizację BSS.

void SupportAttacker::Enter(FieldPlayer* player)
{
player->Steering()->ArriveOn();
player->Steering()->SetTarget(player->Team()->GetSupportSpot());
}

Istnieje szereg warunków, które składają się na logikę metody Execute. Przejdźmy je.

void SupportAttacker::Execute(FieldPlayer* player)
{
// jeśli jego drużyna straci kontrolę, wróć do domu
if (!player->Team()->InControl())
{
player->ChangeState(player, ReturnToHomeRegion::Instance()); return;
}

Jeśli drużyna gracza straci kontrolę, gracz powinien zmienić stan, aby wrócić do pozycji wyjściowej.

// jeśli zmienia się najlepszy punkt wspierający, zmień cel kierowania
if (player->Team()->GetSupportSpot() != player->Steering()->Target())
{
player->Steering()->SetTarget(player->Team()->GetSupportSpot());
player->Steering()->ArriveOn();
}

Jak widzieliście, pozycja najlepszego miejsca wspierającego zmienia się zgodnie z wieloma czynnikami, więc każdy gracz wspierający musi zawsze upewnić się, że jego cel sterowania jest aktualizowany o najnowszą pozycję.

// jeśli ten gracz strzela w bramkę ORAZ atakujący może przekazać
// piłkę do atakującego powinien podać piłkę temu graczowi
if( player->Team()->CanShoot(player->Pos(),
Prm.MaxShootingForce) )
{
player->Team()->RequestPass(player);
}

Zawodnik wspierający spędza większość czasu na połowie boiska przeciwnika. Dlatego zawsze należy zwracać uwagę na możliwość strzału w bramkę przeciwnika. Te kilka linii używa metody SoccerTeam :: Can-Shoot, aby ustalić, czy istnieje potencjalny rzut do bramki. Jeśli wynik jest pozytywny, gracz prosi o podanie przez zawodnika kontrolującego piłkę. Z kolei jeśli RequestPass ustali, że podanie od gracza kontrolującego do tego gracza jest możliwe bez przechwycenia, zostanie wysłana wiadomość Msg_ReceiveBall, a gracz odpowiednio zmieni stan w gotowości do otrzymania piłki.

// jeśli ten gracz znajduje się w miejscu wsparcia, a jego drużyna nadal go ma
// posiadanie, powinien pozostać nieruchomy i odwrócić się twarzą do piłki
if (player->AtTarget())
{
player->Steering()->ArriveOff();
//gracz powinien patrzeć na piłkę!
player->TrackBall();
player->SetVelocity(Vector2D(0,0));
// jeśli nie jest zagrożony przez innego gracza, poproś o podanie
if (!player->isThreatened())
{
player->Team()->RequestPass(player);
}
}
}

Wreszcie, jeśli zawodnik wspierający osiągnie pozycję BSS, czeka i upewnia się, że zawsze stoi twarzą do piłki. Jeśli w jego bezpośrednim sąsiedztwie nie ma żadnych przeciwników i nie czuje się zagrożony, prosi gracza o podanie.

UWAGA Należy pamiętać, że żądanie przepustki nie oznacza, że przepustka zostanie wykonana. Podanie zostanie wykonane tylko wtedy, gdy zostanie uznane za bezpieczne przed przechwyceniem.

Bramkarze

Zadaniem bramkarza jest powstrzymanie piłki przed przesunięciem się nad linią bramkową. Aby to zrobić, bramkarz wykorzystuje inny zestaw umiejętności niż zawodnik z pola i dlatego jest wdrażany jako osobna klasa, GoalKeeper. Bramkarz przesunie się do tyłu i do przodu wzdłuż bramki, aż piłka znajdzie się w określonym zakresie, w którym to momencie przesunie się na zewnątrz w kierunku piłki, próbując ją przechwycić. Jeśli bramkarz wejdzie w posiadanie piłki, ponownie wrzuca piłkę do gry, kopiąc ją do odpowiedniego członka drużyny. Bramkarz Simple Soccer jest przypisany do regionu, który pokrywa się z celem jego drużyny. Dlatego czerwony bramkarz jest przypisany do regionu 16, a niebieski bramkarz do regionu 1.

Ruch bramkarza

Oprócz posiadania zupełnie innego zestawu stanów niż zawodnik terenowy, klasa GoalKeeper musi zastosować nieco inny układ dla swojego ruchu. Jeśli zobaczysz bramkarza grającego w piłkę nożną, zauważysz, że prawie zawsze patrzy bezpośrednio na piłkę i że wiele jego ruchów odbywa się z boku na bok, a nie wzdłuż jego kierunku jak zawodnik na boisku. Ponieważ istota używająca zachowań kierowniczych ma kurs z wyrównaną prędkością, bramkarz wykorzystuje inny wektor, m_vLookAt, aby wskazać kierunek, w którym jest skierowany, i ten wektor jest przekazywany do funkcji Render w celu przekształcenia wierzchołków bramkarza. Rezultatem końcowym jest istota, która wydaje się być zawsze zwrócona w kierunku piłki i może poruszać się na boki z boku na bok, a także wzdłuż swojej osi kursu.

Stany bramkarza

Bramkarz wykorzystuje pięć stanów. To są:

•  GlobalKeeperState
•  TendGoal
•  Powrót do domu
•  PutBallBackInPlay
•  InterceptBall

Rzućmy okiem na każdy z nich szczegółowo, aby zobaczyć, co powoduje, że bramkarz drga.

GlobalKeeperState

Podobnie jak stan globalny FieldPlayer, stan globalny GoalKeeper jest używany jako router dla wszystkich wiadomości, które może otrzymywać. Bramkarz nasłuchuje tylko dwóch wiadomości: Msg_GoHome i Msg_ReceiveBall. Myślę, że kod może tutaj mówić sam za siebie:

bool GlobalKeeperState::OnMessage(GoalKeeper* keeper, const Telegram& telegram)
{
switch(telegram.Msg)
{
case Msg_GoHome:
{
keeper->SetDefaultHomeRegion();
keeper->ChangeState(keeper, ReturnHome::Instance());
}
break;
case Msg_ReceiveBall:
{
keeper->ChangeState(keeper, InterceptBall::Instance());
}
break;
}//end switch
return false;
}

TendGoal

W stanie TendGoal bramkarz przesunie się w poprzek przodu bramki, próbując utrzymać swoje ciało pomiędzy piłką a pozycją ruchu znajdującą się z tyłu, gdzieś wzdłuż linii bramkowej. Oto metoda Enter stanu:

void TendGoal::Enter(GoalKeeper* keeper)
{
//włącz interpose
keeper->Steering()->InterposeOn(Prm.GoalKeeperTendingDistance);
// interpose ustawia agenta między pozycją piłki a celem
// pozycja usytuowana wzdłuż ujścia bramki. To wywołanie określa cel
keeper->Steering()->SetTarget(keeper->GetRearInterposeTarget());
}

Po pierwsze, aktywowane jest zachowanie kierowania pośredniego. Interpose zwróci siłę kierującą, która próbuje ustawić bramkarza między piłką a pozycją usytuowaną wzdłuż ujścia bramki. Pozycja ta jest określana przez metodę GoalKeeper :: GetRearInterposeTarget, która przypisuje pozycję do celu proporcjonalnie tak daleko, jak długość ujścia bramki, gdy piłka jest ustawiona na szerokość boiska. (Mam nadzieję, że to zdanie miało sens, ponieważ męczyłem się nim przez dziesięć minut i jest to najlepsze, co mogłem zrobić!) Mam nadzieję, że poniższy rysunek pomoże ci zrozumieć. Z punktu widzenia bramkarza im dalej piłka jest w lewo, tym dalej w lewo wzdłuż linii bramkowej znajduje się cel celujący. Gdy piłka przesuwa się w prawą bramkę bramkarza, celowany w ten sposób tylny cel przesuwa się wraz z nią na prawo od bramki.



Czarna podwójna strzałka wskazuje dystans bramkarza który próbuje utrzymać się między sobą a tyłem sieci. Ta wartość jest ustawiona w params.ini jako GoalKeeperTendingDistance. Przejdźmy do metody Execute.

void TendGoal::Execute(GoalKeeper* keeper)
{
// tylny cel pośredni zmieni się wraz ze zmianą pozycji piłki
// więc musi być aktualizowany na każdym etapie aktualizacji
keeper->Steering()->SetTarget(keeper->GetRearInterposeTarget());
// jeśli piłka znajdzie się w zasięgu, bramkarz ją łapie, a następnie zmienia stan
// przywrócenie piłki do gry
if (keeper->BallWithinPlayerRange())
{
keeper->Ball()->Trap();
keeper->Pitch()->SetGoalKeeperHasBall(true);
keeper->ChangeState(keeper, PutBallBackInPlay::Instance());
return;
}
// jeśli piłka znajduje się w określonej odległości, bramkarz rusza się z
// pozycja, aby spróbować go przechwycić
if (keeper->BallWithinRangeForIntercept())
{
keeper->ChangeState(keeper, InterceptBall::Instance());
}

Najpierw sprawdza się, czy piłka jest wystarczająco blisko, aby bramkarz mógł ją złapać. Jeśli tak, piłka jest uwięziona, a bramkarz zmienia stan na PutBallBackInPlay. Następnie, jeśli piłka znajdzie się w zasięgu przechwytywania, pokazanego na powyższym rysunku jako obszar jasnoszarego i ustawionego w params.ini jako GoalKeeperInterceptRange, bramkarz zmienia stan na InterceptBall.

// jeśli bramkarz zaszedł zbyt daleko od linii bramkowej i tam
// nie stanowi zagrożenia ze strony przeciwników, powinien się do niej cofnąć
if (keeper->TooFarFromGoalMouth() && keeper->Team()->InControl())
{
keeper->ChangeState(keeper, ReturnHome::Instance());
return;
}
}

Czasami po zmianie stanu z InterceptBall na TendGoal bramkarz może znaleźć się zbyt daleko od bramki. Ostatnie kilka linii kodu sprawdza tę ewentualność i, jeśli jest to bezpieczne, zmienia stan właściciela na ReturnHome. Metoda TendGoal :: Exit jest bardzo prosta; po prostu dezaktywuje zachowanie kierowania pośredniego.

void TendGoal::Exit(GoalKeeper* keeper)
{
keeper->Steering()->InterposeOff();
}

Powrót do domu

Stan ReturnHome przesuwa bramkarza z powrotem w kierunku swojego regionu ojczystego. Po osiągnięciu regionu gospodarza lub gdy przeciwnicy przejmują kontrolę nad piłką, bramkarz wraca do stanu TendGoal

void ReturnHome::Enter(GoalKeeper* keeper)
{ keeper->Steering()->ArriveOn();
}
void ReturnHome::Execute(GoalKeeper* keeper)
{
keeper->Steering()->SetTarget(keeper->HomeRegion()->Center());
// jeśli wystarczająco blisko domu lub przeciwnicy przejdą kontrolę nad piłką,
// zmień stan na tendencję do celu
if (keeper->InHomeRegion() || !keeper->Team()->InControl())
{
keeper->ChangeState(keeper, TendGoal::Instance());
}
}
void ReturnHome::Exit(GoalKeeper* keeper)
{
keeper->Steering()->ArriveOff();
}

PutBallBackInPlay

Gdy bramkarz zdobywa piłkę, przechodzi w stan PutBallBack-InPlay. Kilka rzeczy dzieje się w metodzie Enter tego stanu. Najpierw bramkarz informuje swoją drużynę, że ma piłkę, a następnie wszyscy gracze polowi są proszeni o powrót do swoich rodzinnych miejsc poprzez połączenie z metodą SoccerTeam :: ReturnAllFieldPlayersToHome. Zapewnia to wystarczającą ilość wolnego miejsca między bramkarzem a zawodnikami, aby wykonać rzut od bramki.

void PutBallBackInPlay::Enter(GoalKeeper* keeper)
{
//poinformuj zespół, że bramkarz ma kontrolę
keeper->Team()->SetControllingPlayer(keeper);
//wyślij wszystkich graczy do domu
keeper->Team()->Opponents()->ReturnAllFieldPlayersToHome();
keeper->Team()->ReturnAllFieldPlayersToHome();
}

Bramkarz czeka teraz, aż wszyscy pozostali gracze odejdą daleko,wystarczająco daleko i może wykonać czyste podanie do jednego z członków swojego zespołu. Gdy tylko okazja do podania jest dostępna, bramkarz podaje piłkę, wysyła wiadomość do gracza odbierającego, aby poinformować ją, że piłka jest w drodze, a następnie zmienia stan, aby powrócić do utrzymywania bramki.

void PutBallBackInPlay::Execute(GoalKeeper* keeper)
{
PlayerBase* receiver = NULL;
Vector2D BallTarget;
// sprawdź, czy na boisku są gracze dalej, możemy
// być w stanie przejść do. Jeśli tak, wykonaj przepustkę.
if (keeper->Team()->FindPass(keeper,
receiver,
BallTarget,
Prm.MaxPassingForce,
Prm.GoalkeeperMinPassDist))
{
//dokonaj podania
keeper->Ball()->Kick(Vec2DNormalize(BallTarget - keeper->Ball()->Pos()),
Prm.MaxPassingForce);
//bramkarz nie ma już piłki
keeper->Pitch()->SetGoalKeeperHasBall(false);
//poinformuj gracza odbierającego, że piłka nadchodzi do niego
Dispatcher->DispatchMsg(SEND_MSG_IMMEDIATELY,
keeper->ID(),
receiver->ID(),
Msg_ReceiveBall,
&BallTarget);
//wróć do pielęgnowania celu
keeper->GetFSM()->ChangeState(TendGoal::Instance());
return;
}
keeper->SetVelocity(Vector2D());
}

InterceptBall

Bramkarz spróbuje przechwycić piłkę, jeśli przeciwnicy mają kontrolę i jeśli znajdzie się w "zasięgu zagrożenia" - szary obszar pokazany na poniższym rysunku. Wykorzystuje zachowanie polegające na kierowaniu pościgiem w celu skierowania go w kierunku piłki.



void InterceptBall::Enter(GoalKeeper* keeper)
{
keeper->Steering()->PursuitOn();
}

Gdy bramkarz porusza się na zewnątrz, w kierunku piłki, ciągle sprawdza odległość do bramki, aby upewnić się, że nie przesunie się ona zbyt daleko. Jeśli bramkarz znajdzie się poza zakresem bramkowym, zmienia stan na ReturnHome. Jest jeden wyjątek: jeśli bramkarz jest poza zasięgiem bramkowym, ale jest najbliższym graczem na boisku do piłki, biegnie za nim. Jeśli piłka znajdzie się w zasięgu bramkarza, zatrzymuje piłkę przy użyciu metody SoccerBall :: Trap, informuje wszystkich, że jest w posiadaniu, i zmienia stan, aby ponownie wprowadzić piłkę do gry.

void InterceptBall::Execute(GoalKeeper* keeper)
{
// jeśli bramkarz oddali się zbyt daleko od bramki, powinien wrócić do swojego
// region macierzysty, chyba że jest najbliższym piłką, w takim przypadku
// powinien nadal próbować go przechwycić.
if (keeper->TooFarFromGoalMouth() && !keeper->ClosestPlayerOnPitchToBall())
{
keeper->ChangeState(keeper, ReturnHome::Instance());
return;
}
// jeśli piłka znajdzie się w zasięgu rąk bramkarza, łapie ją w pułapkę
// piłka i przywraca ją do gry
if (keeper->BallWithinPlayerRange())
{
keeper->Ball()->Trap();
keeper->Pitch()->SetGoalKeeperHasBall(true);
keeper->ChangeState(keeper, PutBallBackInPlay::Instance());
return;
}
}

Metoda wyjścia InterceptBall wyłącza zachowanie pościgowe.

Kluczowe metody stosowane przez AI

AI często stosuje wiele metod klasy SoccerTeam, dlatego pełny opis jest ważny dla pełnego zrozumienia działania AI. Mając to na uwadze przeprowadzę cię krok po kroku. Załóż z powrotem swój kapelusz matematyczny…

SoccerTeam :: isPassSafeFromAllOpponents

Piłkarz, bez względu na swoją rolę w grze, nieustannie ocenia swoją pozycję w stosunku do otaczających go osób i wydaje opinie na podstawie tych ocen. Jednym z obliczeń, które AI wykonuje często, jest ustalenie, czy podanie z pozycji A do pozycji B może zostać przechwycone przez dowolnego przeciwnika w dowolnym punkcie trajektorii piłki. Potrzebuje tych informacji, aby ocenić, czy może wykonać podanie, czy powinien poprosić o podanie od aktualnego napastnika, czy też istnieje szansa na zdobycie bramki.



//// Zastanów się nad rysunkiem powyżej. Gracz A chciałby wiedzieć, czy może podać piłkę do gracza B bez przechwycenia go przez któregokolwiek z przeciwników W, X, Y lub Z. Aby to ustalić, musi rozważyć każdego przeciwnika po kolei i obliczyć, czy prawdopodobne jest przechwycenie . SoccerTeam :: isPassSafeFromOpponent to miejsce, w którym wykonywana jest cała praca. Metoda przyjmuje jako parametry początkową i końcową pozycję podania, wskaźnik do przeciwnika, który należy wziąć pod uwagę, wskaźnik do odbiornika, do którego piłka jest podawana, i siły, z jaką piłka zostanie kopnięta. Metoda jest wywoływana dla każdego przeciwnika w drużynie przeciwnej za pomocą metody SoccerTeam :: isPassSafeFromAllOpponents.

bool SoccerTeam::isPassSafeFromOpponent(Vector2D from,
Vector2D target,
const PlayerBase* const receiver,
const PlayerBase* const opp,
double PassingForce)const
{
//przenieś przeciwnika w lokalną przestrzeń.
Vector2D ToTarget = target - from;
Vector2D ToTargetNormalized = Vec2DNormalize(ToTarget);
Vector2D LocalPosOpp = PointToLocalSpace(opp->Pos(),
ToTargetNormalized,
ToTargetNormalized.Perp(),
from);
Pierwszym krokiem jest założenie, że A patrzy bezpośrednio na pozycję "docelową" (w tym przykładzie na pozycję gracza B) i przesunięcie przeciwnika do lokalnego układu współrzędnych A. Poniższy rysunek pokazuje, w jaki sposób wszyscy przeciwnicy na rysunku powyżej są rozmieszczeni po przesunięciu na lokalną przestrzeń gracza A



// jeśli przeciwnik znajduje się za kickerem, podanie uznaje się za w porządku (tak jest
// w oparciu o założenie, że piłka zostanie kopnięta za pomocą
// prędkość większa niż prędkość maksymalna przeciwnika)
if (LocalPosOpp.x <0)
{
return true;
}

Zakłada się, że piłka będzie zawsze kopana z prędkością początkową większą niż prędkość maksymalna gracza. Jeśli to prawda, przeciwnicy znajdujący się za lokalną osią y kickera mogą zostać usunięci z dalszych rozważań. Dlatego, biorąc pod uwagę przykład na poniższym rysunku, W można odrzucić. Następnie brani są pod uwagę wszyscy przeciwnicy znajdujący się dalej od gracza podającego niż cel. Jeśli sytuacja jest taka, jak pokazano na powyższym rysunku, a docelowa lokalizacja podania znajduje się u stóp odbiorcy, wówczas dowolnego przeciwnika znajdującego się dalej niż to można natychmiast odrzucić. Jednak ta metoda jest również wywoływana w celu przetestowania ważności potencjalnych przepustek, które znajdują się po obu stronach gracza odbierającego, takich jak te pokazane na rysunku poniżej



W takim przypadku należy wykonać dodatkowy test, aby sprawdzić, czy przeciwnik znajduje się dalej od pozycji docelowej niż odbiornik. Jeśli tak, przeciwnik może zostać odrzucony.

// jeśli przeciwnik znajduje się dalej niż cel, musimy rozważyć, czy
// przeciwnik może osiągnąć pozycję przed odbiorcą.
if (Vec2DDistanceSq(from, target) < Vec2DDistanceSq(opp->Pos(), from))
{
// ten warunek jest tutaj, ponieważ czasami można wywołać tę funkcję
// bez odniesienia do odbiornika. (Na przykład możesz chcieć znaleźć
// jeśli piłka może osiągnąć pozycję na boisku przed przeciwnikiem
// można się do tego dostać)
if (receiver)
{
if (Vec2DDistanceSq(target, opp->Pos()) >
Vec2DDistanceSq(target, receiver->Pos()))
{
return true;
}
}
else
{
return true;
}
}
Najlepszą szansą, że przeciwnik znajdujący się pomiędzy dwoma poprzednimi warunkami ma przechwycenie piłki, jest bieg do punktu, w którym trajektoria piłki jest prostopadła do pozycji przeciwnika, pokazanej jako punkty Yp i Xp odpowiednio dla graczy Y i X na rysunku



Aby przechwycić piłkę, przeciwnik musi być w stanie dotrzeć do tego punktu, zanim piłka się tam dostanie. Aby pokazać, jak obliczyć, czy jest to możliwe, przyjrzyjmy się przypadkowi przeciwnika Y. Po pierwsze, czas potrzebny na pokonanie piłki przez odległość od A do Yp jest określany przez wywołanie SoccerBall :: TimeToCoverDistance. Ta metoda została szczegółowo opisana wcześniej, więc powinieneś zrozumieć, jak to działa. Biorąc pod uwagę ten czas, można obliczyć, jak daleko może pokonać przeciwnik Y, zanim piłka osiągnie punkt Yp (czas * prędkość). Nazywam tę odległość Y zasięgiem, ponieważ jest to odległość, jaką Y może pokonać w dowolnym kierunku w określonym czasie. Do tego zakresu należy dodać promień piłki nożnej i promień okręgu ograniczającego gracza. Ta wartość zasięgu reprezentuje teraz "zasięg" zawodnika, biorąc pod uwagę czas, w którym piłka osiąga Yp. Zasięg Y i X pokazano za pomocą kropkowanych kół na rysunku



Jeśli okrąg opisany przez zasięg przeciwnika przecina oś x, oznacza to, że przeciwnik jest w stanie przechwycić piłkę w wyznaczonym czasie. Dlatego w tym przykładzie można stwierdzić, że przeciwnik Y nie jest zagrożeniem, ale przeciwnikiem X jest. Oto ostatni fragment kodu do sprawdzenia.

// oblicz, ile czasu zajmuje piłce pokonanie odległości do
// pozycja prostopadła do pozycji przeciwnika
double TimeForBall =
Pitch()->Ball()->TimeToCoverDistance(Vector2D(0,0),
Vector2D(LocalPosOpp.x, 0),
PassingForce);
// oblicz teraz, jak daleko przeciwnik może uciec w tym czasie
double reach = opp->MaxSpeed() * TimeForBall +
Pitch()->Ball()->BRadius()+
opp->BRadius();
// jeśli odległość do pozycji y przeciwnika jest mniejsza niż jego bieg
// zasięg plus promień piłki i promień przeciwnika, a następnie
// piłka może zostać przechwycona
if ( fabs(LocalPosOpp.y) < reach )
{
return false;
}
return true;
}
UWAGA Technicznie rzecz biorąc, zakresy pokazane na powyższym rysunku są nieprawidłowe. Przyjąłem założenie, że obracanie się przeciwnika w kierunku punktu przecięcia zajmuje zero czasu. Aby być dokładnym, należy również wziąć pod uwagę czas potrzebny na obrót, w którym to przypadku zakres jest opisany przez elipsę zamiast koła. Jak tu:



Oczywiście obliczenie linii elipsy jest znacznie droższe przecięcia, dlatego zamiast tego używane są koła.

SoccerTeam :: CanShoot

Jedną z bardzo ważnych umiejętności piłkarza jest oczywiście umiejętność strzelania bramek. Gracz będący w posiadaniu piłki może zapytać metodę SoccerTeam :: CanShoot, aby sprawdzić, czy jest w stanie strzelić gola, biorąc pod uwagę aktualną pozycję piłki i wartość reprezentującą siłę, z jaką gracz kopie piłkę. Jeśli metoda ustali, że gracz jest w stanie strzelać, zwróci wartość true i zapisze pozycję, w której gracz powinien strzelać, w odniesieniu do wektora ShotTarget. Metoda polega na losowym wybieraniu liczby pozycji wzdłuż bramki i testowaniu każdej z nich po kolei, aby sprawdzić, czy piłkę można kopnąć do tego punktu bez przechwycenia przez któregokolwiek z przeciwników. Zobacz rysunek poniżej

Oto lista kodów. (Zwróć uwagę na sposób sprawdzania, czy siła kopnięcia jest wystarczająca do przesunięcia piłki nad linią bramkową.

bool SoccerTeam::CanShoot(Vector2D BallPos,
double power
Vector2D& ShotTarget)const
{
//liczba losowo utworzonych celów strzału, które ta metoda przetestuje
int NumAttempts = Prm.NumAttemptsToFindValidStrike;
while (NumAttempts--)
{
//wybierz losową pozycję wzdłuż bramki przeciwnika. (zrobienie
// upewnij się, że promień piłki jest brany pod uwagę)
ShotTarget = OpponentsGoal()->Center();
// wartość y pozycji strzału powinna leżeć gdzieś pomiędzy nimi
// słupki bramkowe (biorąc pod uwagę średnicę piłki)
int MinYVal = OpponentsGoal()->LeftPost().x + Pitch()->Ball()->BRadius();
int MaxYVal = OpponentsGoal()->RightPost().x - Pitch()->Ball()->BRadius();
ShotTarget.x = RandInt(MinYVal, MaxYVal);
// upewnij się, że uderzenie piłki o podanej mocy wystarcza do jazdy
// piłka nad linią bramkową.
double time = Pitch()->Ball()->TimeToCoverDistance(BallPos,
ShotTarget,
power);
// jeśli tak, ten strzał jest następnie testowany, aby sprawdzić, czy któryś z przeciwników
// może to przechwycić.
{
if (isPassSafeFromAllOpponents(BallPos, ShotTarget, NULL, power))
{
return true;
}
}
}
return false;
}
SoccerTeam :: FindPass

Metoda FindPass jest wywoływana przez gracza w celu ustalenia, czy podanie do członka drużyny jest możliwe, a jeśli tak, to do której pozycji i członka drużyny najlepiej podać piłkę. Metoda przyjmuje jako parametry wskaźnik do gracza żądającego podania; odniesienie do wskaźnika, który będzie wskazywał na odbierającego gracza (jeśli znaleziono przepustkę); odniesienie do wektora PassTarget, do którego zostanie przypisana pozycja, w której zostanie wykonane przejście; siła, z jaką piłka zostanie kopnięta; oraz wartość reprezentującą minimalną odległość, jaką powinien znajdować się odbiorca od gracza podającego, MinPassingDistance. Następnie metoda iteruje wszystkich członków zespołu przechodzących i wywołuje GetBestPassToReceiver dla każdego z tych, którzy są przynajmniej MinPassing - Odległość od przechodnia. GetBestPassToReceiver sprawdza liczbę potencjalnych lokalizacji mijania dla rozważanego członka drużyny, a jeśli pass można bezpiecznie wykonać, przechowuje najlepszą okazję w wektorze BallTarget. Po rozpatrzeniu wszystkich członków drużyny, jeśli znaleziono prawidłową przepustkę, ta, która jest najbliższa linii bazowej przeciwnika, jest przypisywana do PassTarget, a wskaźnik do gracza, który powinien otrzymać przepustkę, jest przypisywany do odbiorcy. Następnie metoda zwraca true. Oto listing do sprawdzenia.

bool SoccerTeam::FindPass(const PlayerBase*const passer,
PlayerBase*& receiver,
Vector2D& PassTarget,
double power,
double MinPassingDistance)const
{
std::vector::const_iterator curPlyr = Members().begin();
double ClosestToGoalSoFar = MaxDouble;
Vector2D BallTarget;
//iteruj przez wszystkich członków zespołu tego gracza i oblicz, które
// jeden jest w stanie podać piłkę
for (curPlyr; curPlyr != Members().end(); ++curPlyr)
{
// upewnij się, że potencjalny odbiorca, który jest badany, nie jest tym graczem
// i że jest dalej niż minimalna odległość przejścia
if ( (*curPlyr != passer) &&
(Vec2DDistanceSq(passer->Pos(), (*curPlyr)->Pos()) >
MinPassingDistance*MinPassingDistance))
{
if (GetBestPassToReceiver(passer, *curPlyr, BallTarget, power))
{
// jeśli cel podania jest najbliższy znalezionej linii bramkowej przeciwnika
// do tej pory prowadź rejestr
double Dist2Goal = fabs(BallTarget.x - OpponentsGoal()->Center().x);
if (Dist2Goal < ClosestToGoalSoFar)
{
ClosestToGoalSoFar = Dist2Goal;
//prowadź rejestr tego odtwarzacza
receiver = *curPlyr;
//i cel
PassTarget = BallTarget;
}
}
}
}//następny członek zespołu
if (receiver) return true;
else return false;
}

SoccerTeam :: GetBestPassToReceiver>

Biorąc pod uwagę przechodnia i odbiorcę, ta metoda sprawdza kilka różnych pozycji wokół odbiornika, aby sprawdzić, czy można bezpiecznie przejść do któregoś z nich. Jeśli można dokonać podania, metoda przechowuje najlepsze podanie - to do pozycji najbliższej linii bazowej przeciwnika - w parametrze PassTarget i zwraca true. Pozwól, że omówię algorytm, korzystając z sytuacji pokazanej na poniższym rysunku



bool SoccerTeam::GetBestPassToReceiver(const PlayerBase* const passer,
const PlayerBase* const receiver,
Vector2D& PassTarget,
double power)const
{

Przede wszystkim metoda oblicza, ile czasu zajmie kulka, aby osiągnąć pozycję odbierającego, i natychmiast zwraca wartość false, jeśli nie można osiągnąć tego punktu z uwagi na siłę kopnięcia, moc.

// najpierw obliczyć, ile czasu zajmie dotarcie piłki
// ten odbiornik
double time = Pitch()->Ball()->TimeToCoverDistance(Pitch()->Ball()->Pos(),
receiver->Pos(),
power);
// zwraca fałsz, jeśli piłka nie może dosięgnąć odbierającego po jej zakończeniu
// wyrzucony z podaną mocą
if (time <= 0) return false;
Można teraz obliczyć, jak daleko odbiornik może się poruszać w tym czasie, korzystając z równania x = v t. Punkty przecięcia stycznych od piłki do tego okręgu zasięgu reprezentują granice obwiedni podania zawodnika. Zobacz rysunek



// maksymalna odległość, jaką odbiornik może pokonać w tym czasie
double InterceptRange = time * receiver->MaxSpeed();

Innymi słowy, zakładając, że nie marnuje czasu na obracanie się lub przyspieszanie do maksymalnej prędkości, odbiornik może osiągnąć pozycje ip1 lub ip2 w samą porę, aby przechwycić piłkę. Jednak w rzeczywistości odległość ta jest często zbyt duża, zwłaszcza jeśli odległość między odbiornikiem a przechodzącym dochodzi do granic, w których można wykonać podanie (często ip1 i ip2 ostatecznie znajdą się poza obszarem gry). O wiele lepiej jest rozważyć podanie do pozycji dobrze leżących w tym regionie. Zmniejszy to szansę na przechwycenie piłki przez przeciwników, a także sprawi, że podanie będzie mniej podatne na nieprzewidziane trudności (takie jak odbieranie przez manewr wokół przeciwników, aby osiągnąć cel podania). Daje również odbiornikowi "przestrzeń", przez co może osiągnąć pozycję, a następnie odpowiednio ustawić się w odpowiednim czasie, aby otrzymać przepustkę. Mając to na uwadze, zakres przechwytywania jest zmniejszany do około jednej trzeciej jego pierwotnego rozmiaru.



// Zmniejsz zakres przechwytywania
const double ScalingFactor = 0.3;
InterceptRange *= ScalingFactor;

Jak widać, wygląda to o wiele bardziej rozsądnie i bardziej przypomina rodzaj zasięgu, który wziąłby pod uwagę ludzki piłkarz. Następnym krokiem jest obliczenie pozycji ip1 i ip2. Zostaną one uznane za potencjalne cele przechodzące. Ponadto metoda uwzględni również przejście bezpośrednio do bieżącej lokalizacji odbiornika. Te trzy pozycje są przechowywane w tablicach Pasy.

// obliczyć cele podania, które znajdują się na przechwyceniach
// stycznych od piłki do koła zasięgu odbierającego.
Vector2D ip1, ip2;
GetTangentPoints(receiver->Pos(),
InterceptRange,
Pitch()->Ball()->Pos(),
ip1,
ip2);
const int NumPassesToTry = 3;
Vector2D Passes[NumPassesToTry] = {ip1, receiver->Pos(), ip2};

Wreszcie, metoda iteruje przez każde potencjalne podanie, aby upewnić się, że pozycja znajduje się w obszarze gry i aby była bezpieczna przed próbą przechwycenia przez przeciwników. Pętla zapisuje najlepsze prawidłowe sprawdzenie, które sprawdza, i odpowiednio zwraca wynik.

// to przekazanie jest najlepiej dostępna do tej pory, jeśli:
//
// 1. Dalej w górę pola niż najbliższy ważny przepust dla tego odbiornika
// znaleziono do tej pory
// 2. W obszarze gry
// 3. Nie może zostać przechwycony przez żadnego przeciwnika
double ClosestSoFar = MaxDouble;
bool bResult = false;
for (int pass=0; pass {
double dist = fabs(Passes[pass].x - OpponentsGoal()->Center().x);
if (( dist < ClosestSoFar) &&
Pitch()->PlayingArea()->Inside(Passes[pass]) &&
isPassSafeFromAllOpponents(Pitch()->Ball()->Pos(),
Passes[pass],
receiver,
power))
{
ClosestSoFar = dist;
PassTarget = Passes[pass];
bResult = true;
}
}
return bResult;
}

Dokonywanie szacunków i założeń dla Ciebie

Prawdopodobnie zauważyłeś, że wykorzystałem wiele szacunków i założeń podczas obliczeń opisanych tu. Na początku może się to wydawać złe, ponieważ jako programiści jesteśmy przyzwyczajeni do upewnienia się, że wszystko działa "tak", jak idealne automaty zegarowe. Czasami jednak korzystne jest zaprojektowanie sztucznej inteligencji gry w taki sposób, aby powodowała sporadyczne błędy. Jest to dobra rzecz, jeśli chodzi o tworzenie gier komputerowych AI. Dlaczego? Ponieważ jest bardziej realistyczny. Ludzie cały czas popełniają błędy i wyciągają błędne sądy, dlatego też sporadyczny błąd popełniony przez sztuczną inteligencję sprawia, że doświadczenie gracza jest znacznie bardziej zabawne. Istnieją dwa sposoby wywoływania błędów. Pierwszym z nich jest uczynienie AI "idealną" i stępienie jej. Drugim jest umożliwienie wkradania się "błędów" poprzez dokonywanie założeń lub szacunków podczas projektowania algorytmów wykorzystywanych przez AI. Widziałeś obie te metody stosowane w Simple Soccer. Przykładem tego pierwszego jest użycie losowego hałasu do wprowadzenia niewielkiej ilości błędu w kierunku za każdym razem, gdy piłka jest kopana. Przykładem tego ostatniego jest użycie kół zamiast elips do opisu zakres przechwytywania przeciwnika. Podejmując decyzję o tworzeniu błędów i niepewności w sztucznej inteligencji, należy dokładnie zbadać każdy odpowiedni algorytm. Moja rada jest następująca: jeśli algorytm jest łatwy do kodowania i nie wymaga dużo czasu procesora, zrób to w "poprawny" sposób, uczyń go doskonałym i głupim. W przeciwnym razie sprawdź, czy możliwe jest dokonanie jakichkolwiek założeń lub szacunków, które pomogą zmniejszyć złożoność algorytmu. Jeśli twój algorytm można uprościć w ten sposób, koduj go, a następnie dokładnie go przetestuj, aby upewnić się, że AI działa zadowalająco.

Podsumowanie

Simple Soccer pokazuje, w jaki sposób można stworzyć drużynową sztuczną inteligencję dla gry sportowej, używając tylko kilku podstawowych technik sztucznej inteligencji. Oczywiście na obecnym etapie zachowanie nie jest ani szczególnie wyrafinowane, ani kompletne. Wraz ze wzrostem wiedzy na temat technik sztucznej inteligencji i doświadczenia zobaczysz wiele obszarów, w których można ulepszyć lub uzupełnić projektowanie gry Simple Soccer. Na początek warto spróbować swoich sił w niektórych z poniższych ćwiczeń.

Praktyka czyni mistrza

Poniższe ćwiczenia zostały opracowane w celu wzmocnienia każdej z różnych umiejętności, których nauczyłeś się do tej pory. Mam nadzieję, że dobrze się z nimi zabawisz.

1. Na obecnym etapie zachowanie dryblingu jest słabe: jeśli zawodnik wspierający nie może wystarczająco szybko przenieść się na odpowiednią pozycję, atakujący z przyjemnością rzuci piłkę w linii prostej bezpośrednio w ręce bramkarza drużyny przeciwnej (lub z tyłu ściana, w zależności od tego, co nastąpi wcześniej). Popraw to zachowanie, dodając logikę, aby atakujący nie posunął się zbyt daleko przed graczem wspierającym. Znalazłeś to łatwe? Jakie są inne sposoby poprawy zachowania atakującego? Czy potrafisz tworzyć zawodników, którzy mogą dryblować piłkę wokół przeciwników?

2. Poza zmianą regionów przebywania w przykładowym kodzie nie ma żadnej gry obronnej. Twórz graczy, którzy potrafią wmieszać się między atakujących i wspierających graczy.

3. Dostosuj kalkulator punktu wsparcia, aby wypróbować różne schematy punktacji. Tutaj możesz eksperymentować z wieloma opcjami. Na przykład, możesz ocenić pozycję pod względem jakości bycia w równej odległości od wszystkich przeciwników lub jakości wyprzedzania pozycji gracza kontrolującego. Możesz nawet zmieniać schemat oceniania w zależności od tego, gdzie znajdują się kontrolujący i wspierający gracze.

4. Utwórz dodatkowe stany na poziomie zespołu, aby zastosować bardziej zróżnicowane taktyki. Oprócz przypisywania graczom różnych regionów macierzystych, stwórz stany, które przypisują role także niektórym graczom. Jedną z taktyk może być obalenie atakującego gracza drużyny przeciwnej poprzez przydzielenie graczy do otoczenia. Innym może być nakazanie niektórym graczom pozostania blisko - aby "zaznaczyć" w terminologii piłkarskiej - blokowanie przeciwników AI postrzega to jako zagrożenie (na przykład takie, które znajduje się w pobliżu bramki gospodarzy).

5. Zmień program, aby przechodzący gracz kopał piłkę za pomocą potrzebnej odpowiedniej siły, aby dotarł do stóp odbiornika z wybraną przez Ciebie prędkością.

6. Przedstaw ideę wytrzymałości. Wszyscy gracze zaczynają z taką samą wytrzymałością, a biegając po polu, zużywają ją. Im mniejszą mają wytrzymałość, tym wolniej biegają i tym mniej mocniej mogą strzelać. Mogą odzyskać wytrzymałość tylko wtedy, gdy są nieruchomi. 7. Wdrożenie sędziego. To nie jest tak proste, jak się wydaje. Sędzia musi zawsze starać się ustawić w miejscu, w którym widzi piłkę i zawodników, bez ingerencji w akcję.