Autonomiczny Agent GryProgramowanie A.I. Gier

Autonomiczny Agent Gry

W późnych latach 80. pamiętam film dokumentalny BBC Ho najnowocześniejszej grafice komputerowej i animacji. Program zawierał wiele ekscytujących rzeczy, ale najbardziej żywo pamiętam niesamowitą demonstrację stada ptaków. Opierał się na bardzo prostych zasadach, ale wyglądał tak spontanicznie i naturalnie, a oglądanie go hipnotyzowało. Programista, który zaprojektował zachowanie, nazywa się Craig Reynolds. Nazywał stado ptaków "boidami", a proste zasady, z których wyłoniły się stada, nazwał "zachowaniami sterowania". Od tego czasu Reynolds opublikował wiele artykułów na temat różnych rodzajów zachowań sterowania, z których wszystkie są fascynujące. Większość, jeśli nie wszystkie, jego zachowaństerowaniah ma bezpośrednie znaczenie dla gier, dlatego spędzę dużo czasu, opisując je i pokazując, jak je kodować i jak z nich korzystać.

Co to jest agent autonomiczny?

Widziałem wiele definicji tego, czym jest autonomiczny agent, ale prawdopodobnie najlepsza jest taka:

Autonomiczny agent to system znajdujący się w jego wnętrzu i będący częścią środowiska, który wyczuwa to środowisko i z czasem na niego działa, dążąc do realizacji własnych celów i aby wpływać na to, co wyczuwa w przyszłości.

W tej części będziemy używać terminu "agent autonomiczny" w odniesieniu do agentów, którzy posiadają pewien stopień autonomicznego ruchu. Jeśli autonomiczny agent natknie się na nieoczekiwaną sytuację, na przykład znajdując na swojej drodze ścianę, będzie mógł odpowiednio zareagować i odpowiednio dostosować swój ruch. Na przykład możesz zaprojektować jednego autonomicznego agenta, który będzie zachowywał się jak królik, a drugi jak lis. Jeśli podczas chrupania szczęśliwie na świeżej, zroszonej trawie królik zdoła dostrzec lisa, będzie on sam próbował go ominąć. W tym samym czasie lis będzie autonomicznie ścigał królika. Oba te zdarzenia występują bez dalszej interwencji programisty; po uruchomieniu autonomiczni agenci po prostu dbają o siebie. Nie oznacza to, że autonomiczny agent powinien być w stanie poradzić sobie z absolutnie każdą sytuacją (choć może to być jeden z twoich celów), ale często bardzo przydatne jest nadanie mu pewnej autonomii. Na przykład częstym problemem podczas pisania kodu wyszukiwania ścieżek jest sposób radzenia sobie z przeszkodami dynamicznymi. Dynamiczne przeszkody to obiekty w twoim świecie gry, które poruszają się lub zmieniają pozycję, takie jak inni agenci, przesuwane drzwi i tak dalej. Biorąc pod uwagę odpowiednie środowisko, włączenie prawidłowego zachowania sterującego do postaci w grze uniemożliwi pisanie specjalnego kodu identyfikującego ścieżkę do obsługi dynamicznych przeszkód - autonomiczny agent będzie w stanie poradzić sobie z nimi, jeśli i kiedy będzie musiał. Ruch autonomicznego agenta można podzielić na trzy warstwy:

•  Wybór akcji: jest to część zachowania agenta odpowiedzialna za wybór jego celów i decydowanie o tym, jaki plan zastosować. To jest część która mówi "idź tutaj" i "wykonaj A, B, a następnie C." •  Sterowanie: ta warstwa odpowiada za obliczanie pożądanych trajektorii wymaganych do spełnienia celów i planów określonych przez warstwę wyboru akcji. Zachowania sterujące są implementacją tej warstwy. Wytwarzają siłę sterującą, która opisuje, gdzie agent powinien się poruszać i jak szybko powinien podróżować, aby się tam dostać.

•  Lokomocja: dolna warstwa, lokomocja, reprezentuje bardziej mechaniczne aspekty ruchu agenta. To sposób podróżowania od A do B. Na przykład, jeśli zastosowałeś mechanikę wielbłąda, akwarium i złotej rybki, a następnie wydałeś im polecenie podróży na północ, wszyscy użyliby różnych procesów mechanicznych do wywołania ruchu, nawet jeśli ich zamiar ( na północ) jest identyczny. Oddzielając tę warstwę od warstwy sterującej, można z niewielkimi modyfikacjami wykorzystać te same zachowania kierowania dla zupełnie różnych rodzajów poruszania się.

Reynolds wykorzystuje doskonałą analogię do opisania ról każdej z tych warstw w swoim artykule "Zachowania sterujące dla postaci autonomicznych".
"Weźmy na przykład niektórych kowbojów hodujących stado bydła Zakres. Krowa odchodzi od stada. Szef szlaku każe kowbojowi zabrać zabłąkanego. Kowboj mówi "wio" swojemu koniowi i prowadzi go do krowy, unikając przeszkód po drodze. W tym przykładzie szef szlaku reprezentuje wybór akcji: zauważenie, że zmienił się stan świata (krowa opuściła stado) i wyznaczenie celu (odzyskanie zbłąkanej). Poziom kierowania jest reprezentowany przez kowboja, który rozkłada cel na serię prostych celów cząstkowych (zbliż się do krowy, unikaj przeszkód, odzyskaj krowę). Cel cząstkowy odpowiada zachowaniu kierowniczemu drużyny kowbojów i koni. Używając różnych sygnałów sterujących (komendy głosowe, ostrogi, wodze) kowboj kieruje konia w kierunku celu. Ogólnie rzecz biorąc, sygnały te wyrażają pojęcia takie jak: idź szybciej, idź wolniej, skręć w prawo, skręć w lewo i tak dalej. Koń realizuje poziom poruszania się. Biorąc za sygnały wejściowe kowboja, koń porusza się we wskazanym kierunku. Ten ruch jest wynikiem złożonej interakcji percepcji wzrokowej konia, jego poczucia równowagi i jego mięśni przykładających momenty do stawów szkieletu. Z technicznego punktu widzenia poruszanie się nogami jest bardzo trudnym problemem, ale ani kowboj, ani koń nie zastanawiają się nad tym. "

Jednak nie wszystko jest słodkie w świecie niezależnych agentów. Wdrożenie zachowań sterowniczych może obciążyć programistę mnóstwem nowych problemów do rozwiązania. Niektóre zachowania mogą wymagać intensywnego ręcznego dostosowywania, podczas gdy inne muszą być starannie zakodowane, aby uniknąć użycia dużej części czasu procesora. Łącząc zachowania, zwykle należy zachować ostrożność, aby uniknąć możliwości, że dwie lub więcej z nich może się anulować. Istnieją jednak sposoby i sposoby na obejście większości tych problemów (no cóż, z wyjątkiem drobnych poprawek - ale i tak jest fajnie), a najczęściej korzyści z zachowań kierowniczych znacznie przewyższają wszelkie wady.

Model pojazdu

Zanim omówię poszczególne zachowania związane z kierowaniem, poświęcę trochę czasu na wyjaśnienie kodu i projektu klasy modelu pojazdu (lokomocji). MovingEntity to klasa podstawowa, z której wywodzą się wszyscy agenci gier w ruchu. Zawiera dane opisujące podstawowy pojazd o masie punktowej. Pozwól, że przeprowadzę cię przez deklarację klasy:

class MovingEntity : public BaseGameEntity
{
protected:

Klasa MovingEntity wywodzi się z klasy BaseGameEntity, która definiuje encję o identyfikatorze, typie, pozycji, promieniu granicznym i skali. Wszystkie elementy gry odtąd w tej książce będą pochodzić z BaseGameEntity. BaseGameEntity ma również dodatkową zmienną boolowską, m_bTag, która będzie wykorzystywana na różne sposoby, z których niektóre zostaną opisane wkrótce. Nie wymienię tutaj deklaracji klasy, ale polecam rzucić okiem na nagłówek BaseGameEntity.h podczas czytania tej części.

SVector2D m_vVelocity;
// znormalizowany wektor wskazujący kierunek, w którym zmierza jednostka.
SVector2D m_vHeading;
// wektor prostopadły do wektora kierunku
SVector2D m_vSide;
Wektory nagłówka i boczne definiują lokalny układ współrzędnych dla poruszającego się obiektu. W przykładach podanych w tej sekcji kurs pojazdu zawsze będzie wyrównany z jego prędkością (na przykład pociąg ma kurs z wyrównaną prędkością). Wartości te będą często używane przez algorytmy sterowania i są aktualizowane co każdą klatkę.

double m_dMass;
//maksymalna prędkość, z jaką ta jednostka może podróżować.
double m_dMaxSpeed;
//maksymalna siła, jaką ta istota może wytworzyć, aby sama się zasilić
//(pomyśl o rakietach i pchnięciu)

double m_dMaxForce;
// maksymalna prędkość (radianów na sekundę), z jaką pojazd może się obracać
double m_dMaxTurnRate;
public:
/* DODATKOWE SZCZEGÓŁY POMINIĘTE*/
};

Chociaż jest to wystarczająca ilość danych, aby przedstawić ruchomy obiekt, wciąż potrzebujemy sposobu na zapewnienie ruchomej istocie dostępu do różnych rodzajów zachowań sterujących. Utworzymy klasę Vehicle, która odziedziczy po MovingEntity i jest właścicielem instancji klasy zachowania kierowniczego, SteeringBehaviors. SteeringBehaviors zawiera wszystkie różne zachowania związane z kierowaniem, które omówię w tej części. Więcej o tym za chwilę; najpierw spójrzmy na deklarację klasy pojazdu.

class Vehicle : public MovingEntity
{
private:
//wskaźnik do danych światowych umożliwiający pojazdowi dostęp do dowolnej przeszkody
//dane ścieżki, ściany lub agenta
GameWorld* m_pWorld;

Klasa GameWorld zawiera wszystkie dane i obiekty związane ze środowiskiem, w którym znajdują się agenci, takie jak ściany, przeszkody i tak dalej. Nie wymienię tutaj deklaracji, aby zaoszczędzić miejsce, ale może warto sprawdzić GameWorld.h w swoim IDE w pewnym momencie, aby się o tym przekonać.

// klasa zachowania sterowniczego
SteeringBehaviors* m_pSteering;
Pojazd ma dostęp do wszystkich dostępnych zachowań kierowniczych poprzez własną instancję klasy zachowań kierowniczych.

public:
//aktualizuje pozycję i orientację pojazdu
void Update(double time_elapsed);
/* DODATKOWE SZCZEGÓŁY POMINIĘTE */
};

Relacje klas można wyraźnie zobaczyć na uproszczonym diagramie UML pokazanym na rysunku



Aktualizacja fizyki pojazdu

Zanim przejdziemy do samych zachowań związanych z kierowaniem, chciałbym poprowadzić Cię przez metodę Vehicle :: Update. Ważne jest, aby zrozumieć każdą linię kodu w tej funkcji, ponieważ jest to główny koń roboczy klasy Vehicle.

bool Vehicle::Update(double time_elapsed)
{
// obliczyć łączną siłę z każdego zachowania kierowania w
//liście pojazdów
SVector2D SteeringForce = m_pSteering->Calculate();

Najpierw obliczana jest siła kierowania dla tego etapu symulacji. Metoda obliczania sumuje wszystkie aktywne zachowania kierownicy pojazdu i zwraca całkowitą siłę kierowania.

//Acceleration = Force/Mass
SVector2D acceleration = SteeringForce / m_dMass;

Korzystając z praw fizyki Newtona, siła kierująca jest przekształcana w przyspieszenie.

// zaktualizuj prędkość
m_vVelocity += acceleration * time_elapsed;
Za pomocą przyspieszenia można zaktualizować prędkość pojazdu

/upewnij się, że pojazd nie przekracza maksymalnej prędkości
m_vVelocity.Truncate(m_dMaxSpeed);
// zaktualizuj pozycję
m_vPos += m_vVelocity * time_elapsed;
Pozycję pojazdu można teraz aktualizować za pomocą nowej prędkości.

// zaktualizuj kurs, jeśli prędkość pojazdu jest większa niż bardzo mała
// wartość
if (m_vVelocity.LengthSq() > 0.00000001)
{
m_vHeading = Vec2DNormalize(m_vVelocity);
m_vSide = m_vHeading.Perp();
}

Jak wspomniano wcześniej, MovingEntity ma lokalny układ współrzędnych, który musi być aktualizowany na każdym etapie symulacji. Kurs pojazdu powinien zawsze być wyrównany z jego prędkością, więc jest on aktualizowany, aby był równy znormalizowanemu wektorowi prędkości. Ale - i to jest ważne - kurs jest obliczany tylko wtedy, gdy prędkość pojazdu jest powyżej bardzo małej wartości progowej. Wynika to z faktu, że jeśli wielkość prędkości wynosi zero, program zawiesi się z błędem dzielenia przez zero, a jeśli wielkość nie jest zerowa, ale bardzo mała, pojazd może (w zależności od platformy i systemu operacyjnego) ruszyć błędnie kilka sekund po zatrzymaniu. Składnik boczny lokalnego układu współrzędnych można łatwo obliczyć, wywołując SVector2D :: Perp.

// traktuj ekran jak toroid
WrapAround(m_vPos, m_pWorld->cxClient(), m_pWorld->cyClient());
}

Na koniec uważa się, że obszar wyświetlania zawija się od góry do dołu i od lewej do prawej (jeśli wyobrażasz to sobie w 3D, byłby toroidalny - w kształcie pączka). Dlatego sprawdza się, czy zaktualizowane położenie pojazdu przekroczyło granice ekranu. Jeśli tak, pozycja jest odpowiednio zawijana. To nudne rzeczy na uboczu - przejdźmy dalej i baw się dobrze!

Zachowania sterujące

Teraz opiszę każde zachowanie sterownicze indywidualnie. Po omówieniu ich wszystkich wyjaśnię klasę SteeringBehaviors, która je kapsułkuje, i pokażę różne dostępne metody ich łączenia. Pod koniec rozdziału przedstawię kilka wskazówek i wskazówek, które pozwolą w pełni wykorzystać możliwości sterowania.

Szukanie

Zachowanie polegające na wyszukiwaniu zachowania sterowniczego zwraca siłę, która kieruje agenta w kierunku pozycji docelowej. Programowanie jest bardzo proste. Kod wygląda następująco (zauważ, że m_pVehicle wskazuje na pojazd, który jest właścicielem klasy SteeringBehaviors):

Vector2D SteeringBehaviors::Seek(Vector2D TargetPos)
{
Vector2D DesiredVelocity = Vec2DNormalize(TargetPos - m_pVehicle->Pos())
* m_pVehicle->MaxSpeed();
return (DesiredVelocity - m_pVehicle->Velocity());
}

Najpierw obliczana jest pożądana prędkość. Jest to prędkość, której agent będzie potrzebował, aby osiągnąć pozycję docelową w idealnym świecie. Reprezentuje wektor od agenta do celu, skalowany do długości maksymalnej możliwej prędkości agenta. Siła sterująca zwrócona tą metodą jest wymaganą siłą, która po dodaniu do wektora prędkości prądu czynnika daje pożądaną prędkość. Aby to osiągnąć, wystarczy odjąć bieżącą prędkość środka od żądanej prędkości. Patrz rysunek



Kliknij lewym przyciskiem myszy, aby zmienić pozycję celu. Zauważ, jak agent przekroczy cel, a następnie odwróci się, by ponownie się zbliżyć. Wielkość przekroczenia zależy od stosunku MaxSpeed do MaxForce. Możesz zmienić wielkość tych wartości, naciskając klawisze Ins / Del i Home / End. Szukaj jest przydatny przy różnego rodzaju rzeczach. Jak zobaczysz, wiele innych zachowań sterujących będzie z niego korzystać.

Ucieczka

Ucieczka jest przeciwieństwem poszukiwania. Zamiast wytworzyć siłę sterującą, aby skierować agenta w kierunku pozycji docelowej, ucieczka tworzy siłę, która kieruje agenta dalej. Oto kod:

Vector2D SteeringBehaviors::Flee(Vector2D TargetPos)
{
Vector2D DesiredVelocity = Vec2DNormalize(m_pVehicle->Pos() - TargetPos)
* m_pVehicle->MaxSpeed();
return (DesiredVelocity - m_pVehicle->Velocity());
}

Zauważ, że jedyną różnicą jest to, że obliczana jest DesiredVelocity za pomocą wektora wskazującego w przeciwnym kierunku (m_pVehicle-> Pos () -TargetPos zamiast TargetPos - m_pVehicle-> Pos ()). Ucieczka może być łatwo dostosowana do generowania siły ucieczki tylko wtedy, gdy pojazd znajdzie się w określonym zasięgu celu. Wystarczy kilka dodatkowych wierszy kodu

Vector2D SteeringBehaviors::Flee(Vector2D TargetPos)
{
// uciekaj tylko, jeśli cel znajduje się w "odległości paniki". Pracuj
// w odległości do kwadratu.
const double PanicDistanceSq = 100.0 * 100.0;
if (Vec2DDistanceSq(m_pVehicle->Pos(), target) > PanicDistanceSq)
{
return Vector2D(0,0);
}
Vector2D DesiredVelocity = Vec2DNormalize(m_pVehicle->Pos() - TargetPos)
* m_pVehicle->MaxSpeed();
return (DesiredVelocity - m_pVehicle->Velocity());
}

Zauważ, jak obliczana jest odległość do celu w odległości do kwadratu. Jak widzieliśmy w części 1, ma to na celu zapisanie obliczania pierwiastka kwadratowego.

Przybycie

Szukanie jest przydatne, gdy agent porusza się we właściwym kierunku, ale często chcesz, aby jego agenci zatrzymali się delikatnie w pozycji docelowej, a jak widzieliście, szukanie nie jest zbyt świetne w zatrzymywaniu się z gracją. Przyjazd to zachowanie, które steruje agentem w taki sposób, że zwalnia do pozycji docelowej. Oprócz celu funkcja ta przyjmuje parametr wyliczonego typu Deceleration, określony przez:

enum Deceleration{slow = 3, normal = 2, fast = 1};

Przybycie wykorzystuje tę wartość do obliczenia, ile czasu agent potrzebuje na dotarcie do celu. Na podstawie tej wartości możemy obliczyć, z jaką prędkością agent musi podróżować, aby osiągnąć żądaną pozycję w pożądanym czasie. Następnie obliczenia przebiegają tak samo, jak w przypadku wyszukiwania.

Vector2D SteeringBehaviors::Arrive(Vector2D TargetPos,
Deceleration deceleration)
{
Vector2D ToTarget = TargetPos - m_pVehicle->Pos();
// obliczyć odległość do pozycji docelowej
double dist = ToTarget.Length();
if (dist > 0)
{
// ponieważ opóźnienie jest wyliczane jako liczba całkowita, ta wartość jest wymaga
// aby zapewnić precyzyjne dostosowanie opóźnienia
const double DecelerationTweaker = 0.3;
// obliczyć prędkość wymaganą do osiągnięcia celu przy żądanym
// spowolnieniu
double speed = dist / ((double)deceleration * DecelerationTweaker);
// upewnij się, że prędkość nie przekracza maks
speed = min(speed, m_pVehicle->MaxSpeed());
// stąd postępuj podobnie jak Szukaj, chyba że nie musimy się normalizować
// wektor ToTarget, ponieważ już zadaliśmy sobie trud
// obliczania jego długości: dist.
Vector2D DesiredVelocity = ToTarget * speed / dist;
return (DesiredVelocity - m_pVehicle->Velocity());
}
return Vector2D(0,0);
}

Teraz, gdy wiesz, co to robi, spójrz na plik wykonywalny wersji demo. Zauważ, że kiedy pojazd znajduje się daleko od celu, zachowanie przyjeździe działa tak samo, jak szukanie, i jak opóźnienie działa tylko wtedy, gdy pojazd zbliży się do celu.

Pościg

Zachowanie pościgowe jest przydatne, gdy agent musi przechwycić ruchomy cel. Oczywiście może nadal szukać aktualnej pozycji celu, ale tak naprawdę nie pomogłoby to stworzyć iluzji inteligencji. Wyobraź sobie, że znów jesteś dzieckiem i bawisz się w berka na boisku szkolnym. Kiedy chcesz kogoś oznaczyć, nie biegniesz prosto na jego obecną pozycję (która skutecznie do niego dąży); przewidujesz, gdzie będą w przyszłości, i biegniesz w kierunku tego przesunięcia, wprowadzając zmiany w miarę zmniejszania luki. Zobacz rysunek. Takie zachowanie chcemy pokazać naszym agentom.



Powodzenie funkcji pościgu zależy od tego, jak dobrze prześladowca jest w stanie przewidzieć trajektorię uniku Może to być bardzo skomplikowane, dlatego trzeba osiągnąć kompromis, aby uzyskać odpowiednią wydajność bez zużywania zbyt wielu cykli zegara. Istnieje jedna sytuacja, w której prześladowca może się wcześnie wydostać: jeśli uciekający jest przed nami i prawie bezpośrednio jest skierowany do agenta, agent powinien skierować się bezpośrednio do aktualnej pozycji uciekiniera. Można to szybko obliczyć za pomocą produktów punktowych. W przykładowym kodzie odwrócony nagłówek uciekiniera musi znajdować się w odległości około 20 stopni (w przybliżeniu) od agenta, aby uznać go za "skierowanego w stronę". Jedną z trudności w tworzeniu dobrego predyktora jest decyzja, jak daleko w przyszłość agent powinien przewidzieć. Oczywiste jest, że okres patrzenia w przyszłość powinien być proporcjonalny do separacji między prześladowcą a jego pogromcą i odwrotnie proporcjonalny do prędkości prześladowcy i pędnika. Po podjęciu decyzji o tym czasie osoba ścigająca może obliczyć szacunkową przyszłą pozycję. Rzućmy okiem na kod tego zachowania:

Vector2D SteeringBehaviors::Pursuit(const Vehicle* evader)
{
// jeśli evader jest przed nami i jest skierowany w stronę agenta, możemy
// po prostu sprawdzić aktualną pozycję uciekiniera
Vector2D ToEvader = evader->Pos() - m_pVehicle->Pos();
double RelativeHeading = m_pVehicle->Heading().Dot(evader->Heading());
if ((ToEvader.Dot(m_pVehicle->Heading()) > 0) &&
(RelativeHeading < -0.95)) //acos(0.95)=18 degs
{
return Seek(evader->Pos());
}
// Nie rozważamy tego z wyprzedzeniem, więc przewidujemy, gdzie będzie uceikinier
// czas oczekiwania jest proporcjonalny do odległości między uciekinierem
// a prześladowcą; i jest odwrotnie proporcjonalny do sumy
// rędkości agentów
double LookAheadTime = ToEvader.Length() /
(m_pVehicle->MaxSpeed() + evader->Speed());
// teraz przymierz się do przewidywanej przyszłej pozycji ewakuatora
return Seek(evader->Pos() + evader->Velocity() * LookAheadTime);
}

Niektóre modele lokomocji mogą również wymagać uwzględnienia pewnego czasu w celu obrócenia agenta w kierunku przesunięcia. Możesz to zrobić po prostu, zwiększając LookAheadTime o wartość proporcjonalną do iloczynu punktowego dwóch nagłówków i maksymalnej prędkości skrętu pojazdu. Coś jak:

LookAheadTime += TurnAroundTime(m_pVehicle, evader->Pos());

Where TurnAroundTime is the function:

double TurnaroundTime(const Vehicle* pAgent, Vector2D TargetPos)
{
//określić znormalizowany wektor do celu
Vector2D toTarget = Vec2DNormalize(TargetPos - pAgent->Pos());
double dot = pAgent->Heading().Dot(toTarget);
// zmień tę wartość, aby uzyskać pożądane zachowanie. Im wyższy maksymalny obrót
// wskaźnik pojazdu, tym wyższa powinna być ta wartość. Jeśli pojazd
// zmierza w kierunku przeciwnym do swojej pozycji docelowej, a następnie wartości
// 0,5 oznacza, że funkcja zwróci czas 1 sekundy dla
// pojazd do zawrócenia
const double coefficient = 0.5;
// iloczyn skalarny daje wartość 1, jeśli cel znajduje się bezpośrednio przed nami, i -1
// jeśli jest bezpośrednio z tyłu. Odejmowanie 1 i mnożenie przez minus z
// współczynnik daje wartość dodatnią proporcjonalną do obrotu
//przemieszczenie pojazdu i ładunek.
return (dot - 1.0) * -coefficient;
}

Demo pościgu pokazuje mały pojazd ścigany przez większy. Celownik wskazuje szacunkową przyszłą pozycję uciekiniera. (Uciekinier wykorzystuje niewielką ilość ruchów kierownicą, aby wpłynąć na jego ruch. Zaraz zakryję wędrówkę.). Ofiarę prześladowcy ustawia się, przekazując odpowiednią metodę wskaźnikowi do danego celu. Aby skonfigurować sytuację podobną do wersji demonstracyjnej tego zachowania, należy utworzyć dwóch agentów, jednego do ścigania, a drugiego do wędrówki, tak jak poniżej:

Vehicle* prey = new Vehicle(/* params omitted */);
prey->Steering()->WanderOn();
Vehicle* predator = new Vehicle(/* params omitted */);
predator->Steering()->PursuitOn(prey);

Zrozumiałeś? OK, przejdźmy do pogoni za czymś przeciwnym: uchylania się.

Uchylanie się

Unikanie jest prawie takie samo jak pościg, z tą różnicą, że tym razem uciekinier ucieka od szacowanej przyszłej pozycji.

Vector2D SteeringBehaviors::Evade(const Vehicle* pursuer)
{
/* Tym razem nie jest konieczne sprawdzenie czeku na kierunek */
Vector2D ToPursuer = pursuer->Pos() - m_pVehicle->Pos();
//czas oczekiwania jest proporcjonalny do odległości między podążaniem
// i uciekinierem; i jest odwrotnie proporcjonalny do sumy
// prędkości agentów
double LookAheadTime = ToPursuer.Length() /
(m_pVehicle->MaxSpeed() + pursuer->Speed());
// teraz uciekaj od przewidywanej przyszłej pozycji prześladowcy
return Flee(pursuer->Pos() + pursuer->Velocity() * LookAheadTime);
}

Pamiętaj, że tym razem nie jest konieczne sprawdzenie wyboru kierunku jazdy.

Błądzenie

Często podczas wędrówki agenta często znajdujesz użyteczny składnik. Został zaprojektowany w celu wytworzenia siły sterującej, która sprawi wrażenie przypadkowego przejścia przez środowisko agenta. Naiwnym podejściem jest obliczanie losowej siły kierowania za każdym razem, ale powoduje to roztrzęsienie bez możliwości uzyskania długich uporczywych zwrotów. (Właściwie dość sprytną funkcję losową, szum Perlina, można wykorzystać do płynnego skrętu, ale nie jest to bardzo przyjazne procesorowi. Nadal warto się zastanowić, jeśli znudzi Ci się deszczowy hałas Perlina ma wiele zastosowań.) Rozwiązaniem Reynoldsa jest rzutowanie koła przed pojazdem i kierowanie się w kierunku celu, który musi poruszać się wzdłuż obwodu. Za każdym razem do tego celu dodaje się niewielkie przypadkowe przemieszczenie, które z czasem przesuwa się do tyłu i do przodu wzdłuż obwodu koła, tworząc piękny ruch bez drgań. Metodę tę można zastosować do wytworzenia całego zakresu losowego ruchu, od bardzo płynnych falujących zakrętów po dzikie wiry typu Strittly Ballroom i piruety, w zależności od wielkości koła, odległości od pojazdu i wielkości przypadkowego przemieszczenia każdej klatki. Jak mówią, obraz jest wart tysiąca słów, więc prawdopodobnie warto zapoznać się z rysunkiem, aby uzyskać lepsze zrozumienie.



Pozwól mi przeprowadzić Cię krok po kroku przez kod. Najpierw są trzy zmienne składowe członka, które wykorzystują:

double m_dWanderRadius;

Jest to promień ograniczającego koła.

double m_dWanderDistance;

Jest to odległość, jaką krąg wędrujący rzutuje przed agentem

double m_dWanderJitter;

Wreszcie, m_dWanderJitter to maksymalna wielkość przypadkowego przemieszczenia, którą można dodać do celu co sekundę. Teraz sama metoda:

SVector2D SteeringBehaviors::Wander()
{
// najpierw dodaj mały losowy wektor do pozycji celu (RandomClampe
// zwraca wartość od -1 do 1)
m_vWanderTarget += SVector2D(RandomClamped() * m_dWanderJitter,
RandomClamped() * m_dWanderJitter);

m_dWanderRadius, wyśrodkowany na pojeździe (początkowa pozycja m_vWanderTarget jest ustalona w konstruktorze SteeringBehaviors). Z każdym krokiem małe losowe przemieszczenie jest dodawane do pozycji celu wędrownego.

/ ponownie skieruj ten nowy wektor z powrotem na okrąg jednostkowy
m_vWanderTarget.Normalize();
// zwiększ długość wektora do tego samego promienia
// koła wędrownego
m_vWanderTarget *= m_dWanderRadius;

Następnym krokiem jest przerzucenie tego nowego celu z powrotem na krąg wędrówki. Osiąga się to poprzez normalizację wektora i pomnożenie go przez promień koła wędrującego.

// przesuń cel na pozycję WanderDist przed agentem
SVector2D targetLocal = m_vWanderTarget + SVector2D(m_dWanderDistance, 0);
// rzutować cel w przestrzeń światową
SVector2D targetWorld = PointToWorldSpace(targetLocal,
m_pVehicle->Heading(),
m_pVehicle->Side(),
m_pVehicle->Pos());
// i kierujcie się w tym kierunku
return targetWorld - m_pVehicle->Pos();
}

Wreszcie nowy cel zostaje przesunięty przed pojazdem o kwotę równą m_dWanderDistance i rzutowany w przestrzeń kosmiczną. Siła kierowania jest następnie obliczana jako wektor do tej pozycji.



Jeśli masz pod ręką komputer, zalecamy sprawdzenie wersji demonstracyjnej tego zachowania. Zielone kółko to ograniczające "kółko wędrujące", a czerwona kropka cel. Demo pozwala dostosować rozmiar kręgu wędrówki, ilość drgań i odległość wędrówki, dzięki czemu można obserwować ich wpływ na zachowanie. Zwróć uwagę na związek między odległością błądzenia a zmianą kąta siły kierowania zwróconej z metody. Gdy kółko wędrowne znajduje się daleko od pojazdu, metoda powoduje niewielkie zmiany kąta, ograniczając w ten sposób pojazd do małych zakrętów. Gdy koło zostanie przesunięte bliżej pojazdu, ilość, którą może obrócić, staje się coraz bardziej ograniczona. Jeśli chcesz, aby agenci wędrowali w trzech wymiarach (jak statek kosmiczny patrolujący jego terytorium), wszystko, co musisz zrobić, to ograniczyć cel wędrówki do kuli zamiast koła.

Unikanie przeszkód

Unikanie przeszkód to zachowanie, które kieruje pojazdem, aby unikać przeszkód leżących na jego drodze. Przeszkoda to dowolny obiekt, który może być przybliżony przez okrąg (lub kulę, jeśli pracujesz w 3D). Uzyskuje się to poprzez kierowanie pojazdem, aby zachować prostokątną skrzynkę detekcji obszaru, rozciągającą się do przodu od pojazdu bez kolizji. Szerokość skrzynki wykrywającej jest równa promieniu ograniczającemu pojazdu, a jej długość jest proporcjonalna do aktualnej prędkości pojazdu - im szybciej jedzie, tym dłuższa jest skrzynka wykrywająca. Myślę, że zanim opiszę ten proces, dobrym pomysłem byłoby pokazanie diagramu. Rycina pokazuje pojazd, niektóre przeszkody i pole wykrywania zastosowane w obliczeniach.



Znalezienie najbliższego punktu przecięcia

Proces sprawdzania skrzyżowań z przeszkodami jest dość skomplikowany, więc zróbmy to krok po kroku.

A) Pojazd powinien brać pod uwagę tylko te przeszkody, które znajdują się w zasięgu pola detekcji. Początkowo algorytm unikania przeszkód dokonuje iteracji przez wszystkie przeszkody w świecie gry i oznacza te, które znajdują się w tym zakresie, do dalszego rozważenia.
B) Algorytm przekształca następnie wszystkie oznaczone przeszkody w lokalną przestrzeń pojazdu. Ułatwia to życie, ponieważ po transformacji można odrzucić dowolne obiekty o ujemnej lokalnej współrzędnej x.
C) Algorytm musi teraz sprawdzić, czy przeszkody nie pokrywają się z polem wykrywania. Przydatne są tutaj lokalne współrzędne, ponieważ wystarczy rozszerzyć promień ograniczający przeszkodę o połowę szerokości pola wykrywania (promień ograniczający pojazdu), a następnie sprawdzić, czy jego lokalna wartość y jest mniejsza od tej wartości. Jeśli tak nie jest, nie będzie przecinać pola wykrywania, a następnie może zostać odrzucone.

Rysunek powinien pomóc ci wyjaśnić te trzy pierwsze kroki. Litery na przeszkodach na schemacie odpowiadają opisom.



D) W tym momencie pozostały tylko przeszkody, które przecinają pole detekcji. Teraz trzeba znaleźć punkt przecięcia najbliżego pojazdu. Po raz kolejny na ratunek przybywa lokalna przestrzeń. Krok C poszerzył promień ograniczający obiektu. Korzystając z tego, można użyć prostego testu przecięcia linii / okręgu, aby znaleźć miejsce, w którym rozszerzony okrąg przecina oś x. Będą dwa punkty przecięcia, jak pokazano na rysunku poniżej. (Nie musimy się martwić przypadkiem, w którym jedno koło styczne do koła - pojazd wydaje się po prostu oderwać się od przeszkody.) Pamiętaj, że przed pojazdem można mieć przeszkodę, ale jest będzie miał punkt przecięcia z tyłu pojazdu. Jest to pokazane na rysunku przez przeszkodę A. Algorytm odrzuca te przypadki i bierze pod uwagę tylko punkty przecięcia leżące na dodatniej osi x.



Algorytm testuje wszystkie pozostałe przeszkody, aby znaleźć tę z najbliższym (dodatnim) punktem przecięcia. Zanim pokażę ci, jak obliczana jest siła kierowania, pozwól mi wymienić część kodu algorytmu unikania przeszkód, który realizuje kroki od A do D.

Vector2D

SteeringBehaviors::ObstacleAvoidance(const std::vector
&obstacles)
{
//długość skrzynki wykrywającej jest proporcjonalna do prędkości agenta
m_dDBoxLength = Prm.MinDetectionBoxLength +
(m_pVehicle->Speed()/m_pVehicle->MaxSpeed()) *
Prm.MinDetectionBoxLength;

Wszystkie parametry używane w projekcie są odczytywane z pliku inicjującego o nazwie Params.ini i przechowywane w klasie singleton ParamLoader. Wszystkie dane w tej klasie są publiczne i są łatwo dostępne poprzez #definition Prm (#define Prm (* ParamLoader :: Instance ())). Jeśli konieczne jest dodatkowe wyjaśnienie, zobacz plik ParamLoader.h.

// oznaczyć wszystkie przeszkody w zasięgu pola do przetworzenia
m_pVehicle->World()->TagObstaclesWithinViewRange(m_pVehicle, m_dDBoxLength);
// pozwoli to śledzić najbliższą przeszkodę (CIB)
BaseGameEntity* ClosestIntersectingObstacle = NULL;
// zostanie to wykorzystane do śledzenia odległości do CIB
double DistToClosestIP = MaxDouble;
// spowoduje to zapisanie przekształconych lokalnych współrzędnych CIB
Vector2D LocalPosOfClosestObstacle; std::vector::const_iterator curOb = obstacles.begin();
while(curOb != obstacles.end())
{
// jeśli przeszkoda została oznaczona w zasięgu, kontynuuj
if ((*curOb)->IsTagged())
{
//obliczyć pozycję tej przeszkody w przestrzeni lokalneje
Vector2D LocalPos = PointToLocalSpace((*curOb)->Pos(),
m_pVehicle->Heading(),
m_pVehicle->Side(),
m_pVehicle->Pos());
// jeśli pozycja lokalna ma ujemną wartość x, to musi leżeć
// za agentem. (w takim przypadku można to zignorować)
if (LocalPos.x >= 0)
{
// jeśli odległość od osi x do pozycji obiektu jest mniejsza
// niż jego promień + połowa szerokości pola detekcji
// istnieje potencjalne przecięcie.
double ExpandedRadius = (*curOb)->BRadius() + m_pVehicle->BRadius();
if (fabs(LocalPos.y) < ExpandedRadius)
{
// teraz, aby wykonać test przecięcia linii / okręgu. Środek koła
// jest reprezentowany przez (cX, cY). Punkty przecięcia to
//podane przez wzór x = cX +/-sqrt(r^2-cY^2) for y=0.
// Musimy tylko spojrzeć na najmniejszą dodatnią wartość x, ponieważ
// będzie to najbliższy punkt przecięcia.
double cX = LocalPos.x;
double cY = LocalPos.y;
//wystarczy tylko raz obliczyć część sqrt powyższego równania
double SqrtPart = sqrt(ExpandedRadius*ExpandedRadius - cY*cY);
double ip = A - SqrtPart;
if (ip <= 0)
{
ip = A + SqrtPart;
}
// sprawdź, czy jest to jak dotąd najbliższe. Jeśli tak,
// zapisz przeszkodę i jej lokalne współrzędne
if (ip < DistToClosestIP)
{
DistToClosestIP = ip;
ClosestIntersectingObstacle = *curOb;
LocalPosOfClosestObstacle = LocalPos;
}
}
}
}
++curOb;
}

Obliczanie siły kierowania

Określenie siły kierowania jest łatwe. Oblicza się go w dwóch częściach: siły bocznej i siły hamowania.



Istnieje wiele sposobów obliczania siły bocznej, ale preferuję odjęcie wartości y lokalnego położenia przeszkody od jej promienia. Powoduje to boczną siłę kierującą z dala od przeszkody, która zmniejsza się wraz z odległością przeszkody od osi X. Siła ta jest skalowana proporcjonalnie do odległości pojazdu od przeszkody (ponieważ im pojazd znajduje się bliżej przeszkody, tym szybciej powinien zareagować). Kolejnym składnikiem siły kierowania jest siła hamowania. Jest to siła działająca do tyłu, wzdłuż osi poziomej, jak pokazano na rysunku, a także jest skalowana proporcjonalnie do odległości pojazdu od przeszkody. Siła kierowania zostaje ostatecznie przekształcona w przestrzeń świata, w wyniku czego wartość jest zwracana z metody. Kod jest następujący

// jeśli znaleźliśmy przeszkodę przecinającą,
// obliczyć od niej siłę kierującą
Vector2D SteeringForce;
if (ClosestIntersectingObstacle)
{
// im bliżej znajduje się agent, tym silniejsza powinna być siła kierowania
// siła kierująca
double multiplier = 1.0 + (m_dDBoxLength - LocalPosOfClosestObstacle.x) /
m_dDBoxLength;
// obliczyć siłę boczną
SteeringForce.y = (ClosestIntersectingObstacle->BRadius()-
LocalPosOfClosestObstacle.y) * multiplier;
// zastosować siłę hamowania proporcjonalną do odległości przeszkody od
//pojazdu.
const double BrakingWeight = 0.2;
SteeringForce.x = (ClosestIntersectingObstacle->BRadius() -
LocalPosOfClosestObstacle.x) *
BrakingWeight;
}
// na koniec przekonwertuj wektor sterujący z przestrzeni lokalnej na światową
return VectorToWorldSpace(SteeringForce,
m_pVehicle->Heading(),
m_pVehicle->Side());
}

WSKAZÓWKA Przy wdrażaniu omijania przeszkód w trzech wymiarach, użyj kul, aby przybliżyć przeszkody i walec zamiast skrzynki wykrywającej. Matematyka sprawdzania względem kuli nie różni się tak bardzo od sprawdzania względem koła. Gdy przeszkody zostaną przekształcone w lokalną przestrzeń, kroki A i B są takie same, jak już widzieliście, a krok C polega tylko na sprawdzeniu względem innej osi.

Unikanie ścian

Ściana to segment liniowy (w 3D, wielokąt) prostopadły w kierunku, w którym jest skierowany. Unikanie ścian steruje, aby uniknąć potencjalnych kolizji ze ścianą. Dokonuje tego poprzez rzutowanie trzech "czujników" przed pojazd i testowanie, czy którekolwiek z nich przecinają się ze ścianami w świecie gry. (Mały "odcinek" w połowie ściany wskazuje kierunek normalnej ściany.) Jest to podobne do sposobu, w jaki koty i gryzonie używają swoich wąsów do poruszania się po otoczeniu w ciemności



Po znalezieniu najbliższej przecinającej się ściany (jeśli istnieje), obliczana jest siła kierowania. Oblicza się to na podstawie obliczenia, jak daleko końcówka czujnika dotarła do ściany, a następnie poprzez wytworzenie siły tej wielkości w kierunku normalnej ściany.

Vector2D SteeringBehaviors::WallAvoidance(const std::vector& walls)
{
//the feelers are contained in a std::vector, m_Feelers
CreateFeelers();
double DistToThisIP = 0.0;
double DistToClosestIP = MaxDouble;
//this will hold an index into the vector of walls
int ClosestWall = -1;
Vector2D SteeringForce,
point, //used for storing temporary info
ClosestPoint; //holds the closest intersection point
//examine each feeler in turn
for (int flr=0; flr {
//run through each wall checking for any intersection points
for (int w=0; w {
if (LineIntersection2D(m_pVehicle->Pos(),
m_Feelers[flr],
walls[w].From(),
walls[w].To(),
DistToThisIP,
point))
{
//is this the closest found so far? If so keep a record
if (DistToThisIP < DistToClosestIP)
{
DistToClosestIP = DistToThisIP;
ClosestWall = w;
ClosestPoint = point;
}
}
}//next wall
//if an intersection point has been detected, calculate a force
//that will direct the agent away
if (ClosestWall >=0)
{
//calculate by what distance the projected position of the agent
//will overshoot the wall
Vector2D OverShoot = m_Feelers[flr] - ClosestPoint;
//create a force in the direction of the wall normal, with a
//magnitude of the overshoot
SteeringForce = walls[ClosestWall].Normal() * OverShoot.Length();
}
}//next feeler
return SteeringForce;
}

Znalazłem podejście z trzema czujnikami, które daje dobre wyniki, ale możliwe jest osiągnięcie rozsądnej wydajności za pomocą tylko jednego czujnika, który stale skanuje w lewo i prawo przed pojazdem. Wszystko zależy od liczby cykli procesora, z którymi musisz grać i od tego, jak dokładne jest zachowanie.

UWAGA Jeśli jesteś niecierpliwy i patrzyłeś już na kod źródłowy, być może zauważyłeś, że końcowa funkcja aktualizacji w źródle jest nieco bardziej skomplikowana niż podstawowa funkcja aktualizacji wymieniona wcześniej. Wynika to z faktu, że wiele technik, które opiszę pod koniec tej części, wymaga dodania lub nawet zmiany tej funkcji. Wszystkie zachowania sterujące wymienione na kilku kolejnych stronach wykorzystują jednak ten podstawowy szkielet.

Wstawianie

Interpose zwraca siłę kierującą, która porusza pojazd do punktu środkowego linii urojonej, łącząc dwóch innych agentów (lub punkty w przestrzeni lub agenta i punkt). Ochroniarz przyjmujący kulę dla swojego pracodawcy lub piłkarza otzymującego kartkę są przykładami tego rodzaju zachowania. Podobnie jak pościg, pojazd musi oszacować, gdzie w przyszłości będą znajdować się dwaj agenci w czasie T. Następnie może skierować się w kierunku tej pozycji. Ale skąd wiemy, jaka jest najlepsza wartość T? Odpowiedź brzmi: nie, więc zamiast tego dokonujemy obliczeń. Pierwszym krokiem w obliczaniu tej siły jest określenie punktu środkowego linii łączącej pozycje agentów w bieżącym kroku czasu. Odległość od tego punktu jest obliczana, a wartość dzielona przez maksymalną prędkość pojazdu, aby dać czas potrzebny na przebycie dystansu. To jest nasza wartość T. Patrz rysunek 3.11, góra. Za pomocą T pozycje agentów są ekstrapolowane w przyszłość. Punkt środkowy tych przewidywanych pozycji jest określany, a na koniec pojazd używa zachowania przylotu, aby kierować się do tego punktu.



Oto listing:

Vector2D SteeringBehaviors::Interpose(const Vehicle* AgentA,
const Vehicle* AgentB)
{
// najpierw musimy dowiedzieć się, gdzie będą w przyszłości dwaj agenci w czasie T.
// Przybliża się to przez określenie czasu potrzebnego do osiągnięcia
// punktu środkowego w bieżącym czasie przy maksymalnej prędkości.
Vector2D MidPoint = (AgentA->Pos() + AgentB->Pos()) / 2.0;
double TimeToReachMidPoint = Vec2DDistance(m_pVehicle->Pos(), MidPoint) /
m_pVehicle->MaxSpeed();
// teraz mamy T, zakładamy, że agent A i agent B będą kontynuować
// prostą trajektorię i ekstrapolować, aby uzyskać swoje przyszłe pozycje
Vector2D APos = AgentA->Pos() + AgentA->Velocity() * TimeToReachMidPoint;
Vector2D BPos = AgentB->Pos() + AgentB->Velocity() * TimeToReachMidPoint;
// obliczyć punkt środkowy tych przewidywanych pozycji
MidPoint = (APos + BPos) / 2.0;
// następnie kieruj się, aby do niego dotrzeć
return Arrive(MidPoint, fast);
}

Należy pamiętać, że przyjazd jest wywoływany z szybkim zwalnianiem, umożliwiając pojazdowi jak najszybsze osiągnięcie pozycji docelowej. Demo tego zachowania pokazuje czerwony pojazd próbujący wejść w interakcję między dwoma niebieskimi wędrownymi pojazdami.

Ukrycie

Hide stara się ustawić pojazd tak, aby przeszkoda znajdowała się zawsze między nim a agentem - myśliwym - przed którym się ukrywa. Możesz użyć tego zachowania nie tylko w sytuacjach, w których potrzebujesz, aby NPC ukrył się przed osłoną znajdującą się jak gracz podczas strzelania, ale także w sytuacjach, w których chciałbyś, aby NPC podkradł się do gracza. Na przykład możesz stworzyć NPC zdolnego do prześladowania gracza przez ponury las, skaczącego z drzewa na drzewo. Przerażający! Metoda, którą wolę wprowadzić w to zachowanie, jest następująca:

Krok pierwszy. Dla każdej z przeszkód wyznacza się kryjówkę.



Są one obliczane za pomocą metody GetHidingPosition, która wygląda następująco:

SVector2D SteeringBehaviors::GetHidingPosition(const SVector2D& posOb,
const double radiusOb,
const SVector2D& posTarget)
{
// obliczyć, jak daleko agent ma być od promienia ograniczającego
// wybraną przeszkodę
const double DistanceFromBoundary = 30.0;
double DistAway = radiusOb + DistanceFromBoundary;
// obliczyć kurs w kierunku obiektu od celu
SVector2D ToOb = Vec2DNormalize(posOb - posTarget);
// skaluj go do rozmiaru i dodaj do pozycji przeszkody
/kryjówkę
return (ToOb * DistAway) + posOb;
}

Biorąc pod uwagę położenie celu oraz położenie i promień przeszkody, ta metoda oblicza pozycję DistanceFromBoundary od promienia granicznego obiektu i bezpośrednio naprzeciw celu. Robi to, skalując znormalizowany wektor "do przeszkody" o wymaganą odległość od środka przeszkody, a następnie dodając wynik do pozycji przeszkody. Czarne kropki na powyższym rysunku pokazują ukryte miejsca zwrócone tą metodą dla tego przykładu.

Krok drugi. Odległość do każdego z tych miejsc jest określana. Następnie pojazd wykorzystuje zachowanie przylotu do kierowania w kierunku najbliższego. Jeśli nie można znaleźć odpowiednich przeszkód, pojazd omija cel. Oto jak to się robi w kodzie:

SVector2D SteeringBehaviors::Hide(const Vehicle* target,
vector& obstacles)
{
double DistToClosest = MaxDouble
SVector2D BestHidingSpot;
std::vector::iterator curOb = obstacles.begin();
while(curOb != obstacles.end())
{
// obliczyć położenie kryjówki dla tej przeszkody
SVector2D HidingSpot = GetHidingPosition((*curOb)->Pos(),
(*curOb)->BRadius(),
target->Pos());
// pracuj w kwadracie odległości, aby znaleźć najbliższe
// miejsce ukrycia agenta
double dist = Vec2DDistanceSq(HidingSpot, m_pVehicle->Pos());
if (dist < DistToClosest)
{
DistToClosest = dist;
BestHidingSpot = HidingSpot;
}
++curOb;
} //zakończ
// jeśli nie zostaną znalezione odpowiednie przeszkody, omiń cel
if (DistToClosest == MaxDouble)
{
return Evade(target);
}
// w przeciwnym razie użyj Przybycie w kryjówce
return Arrive(BestHidingSpot, fast);
}

Istnieje kilka modyfikacji tego algorytmu:

1. Możesz pozwolić pojazdowi na ukrycie się tylko wtedy, gdy cel znajduje się w jego polu widzenia. Ma to jednak tendencję do niezadowalającego działania, ponieważ pojazd zaczyna zachowywać się jak dziecko ukrywające się przed potworami pod pościelą. Jestem pewien, że pamiętasz uczucie - efekt "jeśli go nie widzisz, to cię nie widzi". Może to działać, gdy jesteś dzieckiem, ale takie zachowanie powoduje, że pojazd wydaje się głupi. Można temu przeciwdziałać, dodając efekt czasowy, dzięki czemu pojazd ukryje się, jeśli cel będzie widoczny lub jeśli zobaczył cel w ciągu ostatnich n sekund. Daje to rodzaj pamięci i daje rozsądnie wyglądające zachowanie.
2. To samo co powyżej, ale tym razem pojazd próbuje się ukryć tylko wtedy, gdy pojazd widzi cel, a cel widzi pojazd.
3. Może być pożądane wytworzenie siły, która kieruje pojazdem, tak aby zawsze sprzyjał ukryciu pozycji z boku lub z tyłu prześladowcy. Można to łatwo osiągnąć za pomocą przyjaciela, iloczynu skalarnego, w celu odchylenia odległości zwróconych z GetHidingPosition.
4. Na początku dowolnej z metod można sprawdzić, czy cel znajduje się w "odległości zagrożenia" przed dalszymi obliczeniami. Jeśli cel nie jest zagrożeniem, metoda może natychmiast powrócić z wektorem zerowym.

Ścieżka

Ścieżka podążająca tworzy siłę kierującą, która porusza pojazdem wzdłuż szeregu punktów trasy tworzących ścieżkę. Czasami ścieżki mają punkt początkowy i końcowy, a innym razem zapętlają się wokół siebie, tworząc niekończącą się, zamkniętą ścieżkę.



Znajdziesz niezliczone zastosowania korzystania ze ścieżek w grze. Możesz ich używać do tworzenia agentów, którzy patrolują ważne obszary mapy, aby umożliwić jednostkom przemierzanie trudnego terenu lub pomóc samochodom wyścigowym w nawigacji po torze wyścigowym. Są przydatne w większości sytuacji, w których agent musi odwiedzić szereg punktów kontrolnych. Ścieżki, którymi podążają pojazdy , są opisane przez std :: list Vector2D. Ponadto pojazd musi również wiedzieć, jaki jest aktualny punkt trasy i czy jest to ścieżka zamknięta, czy też nie, aby umożliwić mu podjęcie odpowiednich działań po osiągnięciu końcowego punktu trasy. Jeśli jest to zamknięta ścieżka, powinna wrócić do pierwszego punktu na liście i zacząć od nowa. Jeśli jest to otwarta ścieżka, pojazd powinien po prostu zwolnić do zatrzymania (przybycia) nad ostatnim punktem trasy. Path to klasa, która opiekuje się wszystkimi tymi szczegółami. Nie wymienię go tutaj, ale możesz chcieć to sprawdzić w swoim IDE. Można go znaleźć w pliku Path.h. Najprostszym sposobem podążania ścieżką jest ustawienie bieżącego punktu na pierwszy na liście, kierowanie się w jego kierunku za pomocą funkcji wyszukiwania, aż pojazd znajdzie się w jego odległości docelowej, a następnie złapanie następnego punktu i poszukiwanie go itd. dopóki bieżący punkt nie będzie ostatnim punktem na liście. Kiedy tak się stanie, pojazd powinien dotrzeć do bieżącego punktu trasy lub, jeśli ścieżka jest zamkniętą pętlą, aktualny punkt trasy powinien być ponownie ustawiony na pierwszym na liście, a pojazd po prostu szuka. Oto kod następującej ścieżki:

SVector2D SteeringBehaviors::FollowPath()
{
// przejdź do następnego celu, jeśli jest wystarczająco blisko bieżącego celu
// (pracując w odległości do kwadratu))
if(Vec2DDistanceSq(m_pPath->CurrentWaypoint(), m_pVehicle->Pos()) <
m_WaypointSeekDistSq)
{
m_pPath->SetNextWaypoint();
}
if (!m_pPath->Finished())
{
return Seek(m_pPath->CurrentWaypoint());
}
else
{
return Arrive(m_pPath->CurrentWaypoint(), normal);
}
}

Musisz być bardzo ostrożny przy wdrażaniu ścieżki. Zachowanie jest bardzo wrażliwe na stosunek maksymalnej siły kierowania / prędkości maksymalnej, a także zmienną m_WaypointSeekDistSq. Plik wykonywalny demonstracji tego zachowania umożliwia zmianę tych wartości, aby zobaczyć, jaki mają one efekt. Jak się przekonasz, łatwo jest stworzyć niechlujne zachowanie. To, jak wąska jest ścieżka, zależy wyłącznie od środowiska gry. Jeśli masz grę z wieloma ponurymi, ciasnymi korytarzami, (prawdopodobnie) będziesz potrzebować surowszej ścieżki niż gra na Saharze.

Offset Pursuit

Funkcja przesunięcia oblicza siłę kierowania wymaganą do utrzymania pojazdu w pozycji określonej odległości od pojazdu docelowego. Jest to szczególnie przydatne do tworzenia formacji. Podczas oglądania pokazów lotniczych, takich jak Brytyjskie Czerwone Strzałki, wiele spektakularnych manewrów wymaga, aby drony pozostały w tych samych względnych pozycjach, co samoloty wiodące. Takie zachowanie chcemy naśladować.

Przesunięcie jest zawsze definiowane w przestrzeni "lidera", więc pierwszą rzeczą do zrobienia przy obliczaniu tej siły kierowania jest określenie pozycji przesunięcia w przestrzeni świata. Następnie funkcja przebiega podobnie do pościgu: Przewiduje się przyszłą pozycję przesunięcia i pojazd dotrze do tej pozycji.

SVector2D SteeringBehaviors::OffsetPursuit(const Vehicle* leader,
const SVector2D offset)
{
// obliczyć pozycję przesunięcia w przestrzeni świata
SVector2D WorldOffsetPos = PointToWorldSpace(offset,
leader->Heading(),
leader->Side(),
leader->Pos());
SVector2D ToOffset = WorldOffsetPos - m_pVehicle->Pos();
// czas oczekiwania jest proporcjonalny do odległości między liderem a prześladowcą;
// i jest odwrotnie proporcjonalny do sumy
// prędkości obu agentów
double LookAheadTime = ToOffset.Length() /
(m_pVehicle->MaxSpeed() + leader->Speed());
// teraz osiągnij przewidywaną przyszłą pozycję przesunięcia
return Arrive(WorldOffsetPos + leader->Velocity() * LookAheadTime, fast);
}

Przyjazd jest używany zamiast poszukiwania, ponieważ zapewnia znacznie płynniejszy ruch i nie jest tak zależny od ustawień maksymalnej prędkości i siły pojazdów. Poszukiwania mogą czasami dawać dziwne wyniki - uporządkowane formacje mogą zmienić się w rój pszczół atakujących lidera formacji!

Ściganie offsetowe jest przydatne we wszystkich sytuacjach. Tu jest kilka:
•  Oznaczanie przeciwnika w symulacji sportowej
•  Dokowanie statkiem kosmicznym
•  Cieniowanie samolotu
•  Wdrażanie formacji bojowych

Plik demonstracyjny wykonywany w ramach pościgu pokazuje trzy mniejsze pojazdy próbujące pozostać w odsunięciu od większego pojazdu wiodącego. Wiodący pojazd korzysta z przybycia do podążania za celownikiem (kliknij lewym przyciskiem myszy, aby ustawić celownik).

Zachowania grupowe

Zachowania grupowe to zachowania sterowania, które uwzględniają niektóre lub wszystkie inne pojazdy w świecie gry. Uciekanie, opisane na początku, jest dobrym przykładem zachowania grupowego. W rzeczywistości flokowanie jest kombinacją trzech zachowań grupowych - spójności, separacji i dostosowania - wszystko to działa razem. Niedługo przyjrzymy się szczegółowo tym konkretnym zachowaniom, ale najpierw pokażę ci, jak definiuje się grupę. Aby określić siłę sterującą dla zachowania grupowego, pojazd weźmie pod uwagę wszystkie inne pojazdy w obrębie okrągłego obszaru o z góry określonym rozmiarze - znanym jako promień sąsiedztwa wyśrodkowany na pojeździe. Rysunek 3.15 powinien pomóc w wyjaśnieniu. Biały pojazd jest środkiem kierującym, a szary okrąg pokazuje zasięg jego sąsiedztwa. W związku z tym wszystkie pojazdy pokazane na czarno są uważane za sąsiadów, a pojazdy pokazane na szaro nie



Przed obliczeniem siły kierowania należy ustalić sąsiadów pojazdu i przechowywać je w pojemniku lub oznaczyć jako gotowe do przetworzenia. W kodzie demonstracyjnym tego rozdziału sąsiednie pojazdy są oznaczone przy użyciu metody BaseGameEntity :: Tag. Odbywa się to za pomocą szablonu funkcji TagNeighbors. Oto kod:

template
void TagNeighbors(const T* entity, conT& ContainerOfEntities, double radius)
{
// teruj przez wszystkie byty sprawdzające zasięg
for (typename conT::iterator curEntity = ContainerOfEntities.begin();
curEntity != ContainerOfEntities.end();
++curEntity)
{
// najpierw wyczyść dowolny bieżący tag
(*curEntity)->UnTag();
Vector2D to = (*curEntity)->Pos() - entity->Pos();
// promień ograniczający drugiego wzięto pod uwagę, dodając go
// do zakresu
double range = radius + (*curEntity)->BRadius();
// jeśli jednostka jest w zasięgu, oznacz do dalszego rozpatrzenia. (praca w
// przestrzeni z kwadratem odległości, aby uniknąć sqrts)
if ( ((*curEntity) != entity) && (to.LengthSq() < range*range))
{
(*curEntity)->Tag();
}
}// następny byt
}

Większość zachowań grupowych wykorzystuje podobny promień sąsiedztwa, więc możemy zaoszczędzić trochę czasu, wywołując tę metodę tylko raz przed wywołaniem któregokolwiek z zachowań grupowych.

if (On(separation) || On(alignment) || On(cohesion))
{
TagNeighbors(m_pVehicle, m_pVehicle->World()->Agents(), ViewDistance);
}

WSKAZÓWKA Możesz nieco zwiększyć realizm zachowań grupowych, dodając do swojego agenta ograniczenie pola widzenia. Na przykład możesz ograniczyć pojazdy znajdujące się w sąsiednim regionie, zaznaczając tylko te, które znajdują się, powiedzmy, 270 stopni w stosunku do kursu sterownika. Możesz to łatwo wdrożyć, testując iloczynu skalarnego nagłówka agenta sterującego i wektora do potencjalnego sąsiada. Możliwe jest nawet dynamiczne dostosowanie pola widzenia agenta i włączenie go do funkcji sztucznej inteligencji. Na przykład w grze wojennej na pole widzenia żołnierza może niekorzystnie wpływać jego zmęczenie, co wpływa na jego zdolność postrzegania otoczenia. Nie sądzę, że ten pomysł został wykorzystany w grze komercyjnej, ale z pewnością jest to przemyślenie. Teraz, gdy wiesz, jak definiowana jest grupa, przyjrzyjmy się niektórym zachowaniom, które na nią działają.

Separacja

Oddzielenie tworzy siłę, która kieruje pojazd z dala od znajdujących się w nim pojazdów regionu sąsiedztwa. Po zastosowaniu do wielu pojazdów, będą rozchodzić się, próbując zmaksymalizować odległość od każdego innego pojazdu.



Jest to łatwe zachowanie do wdrożenia. Przed wywołaniem separacji wszyscy agenci znajdujący się w pobliżu pojazdu są oznaczani. Separacja następnie iteruje oznaczone pojazdy, badając każdy z nich. Wektor do każdego rozważanego pojazdu jest znormalizowany, podzielony przez odległość do sąsiada i dodany do siły kierowania.

Vector2D SteeringBehaviors::Separation(const std::vector& neighbors)
{
Vector2D SteeringForce;
for (int a=0; a {
// upewnij się, że ten agent nie jest uwzględniony w obliczeniach i że
// agent sprawdzany jest wystarczająco blisko.
if((neighbors[a] != m_pVehicle) && neighbors[a]->IsTagged())
{
Vector2D ToAgent = m_pVehicle->Pos() - neighbors[a]->Pos();
// skalować siłę odwrotnie proporcjonalną do odległości agenta
// od sąsiada.
SteeringForce += Vec2DNormalize(ToAgent)/ToAgent.Length();
}
}
return SteeringForce;
}

Wyrównanie

Wyrównanie próbuje utrzymać wyrównanie kursu pojazdu z sąsiadami. Patrz rysunek powyżej, środek. Siła jest obliczana najpierw przez iterację przez wszystkich sąsiadów i uśrednienie ich wektorów kierunkowych. Ta wartość to pożądany kurs, więc odejmujemy kurs pojazdu, aby uzyskać siłę kierowania.

Vector2D SteeringBehaviors::Alignment(const std::vector& neighbors)

{
// używane do rejestrowania średniego kursu sąsiadów
Vector2D AverageHeading;
// używane do zliczania liczby pojazdów w okolicy
int NeighborCount = 0
// iteruj po wszystkich oznaczonych pojazdach i zsumuj ich wektor kursu
for (int a=0; a {
// upewnij się, że * ten * agent nie jest uwzględniony w obliczeniach i że
// agent sprawdzany jest wystarczająco blisko
if((neighbors[a] != m_pVehicle) && neighbors[a]->IsTagged)
{
AverageHeading += neighbors[a]->Heading();
++NeighborCount;
}
}
// jeśli okolica zawiera jeden lub więcej pojazdów, uśrednij ich
// wektory nagłówkowe.
if (NeighborCount > 0)
{
AverageHeading /= (double)NeighborCount;
AverageHeading -= m_pVehicle->Heading();
}
return AverageHeading;
}

Samochody poruszające się po drogach wykazują zachowanie typu wyrównania. Wykazują także separację, starając się zachować minimalną odległość od siebie.

Spójność

Kohezja wytwarza siłę kierującą, która porusza pojazd w kierunku środka masy jego sąsiadów. Patrz rysunek powyżej, u dołu. Owca biegająca za stadem wykazuje spójne zachowanie. Użyj tej siły, aby utrzymać grupę pojazdów razem. Ta metoda przebiega podobnie do ostatniej, tyle że tym razem obliczamy średnią wektorów pozycji sąsiadów. To daje nam środek masy sąsiadów - miejsce, do którego pojazd chce się dostać - więc dąży do tej pozycji.

Vector2D SteeringBehaviors::Cohesion(const std::vector& neighbors)
{
// najpierw znajdź środek masy wszystkich agentów
Vector2D CenterOfMass, SteeringForce;
int NeighborCount = 0;
// iteruj przez sąsiadów i podsumuj wszystkie wektory położenia
for (int a=0; a {
// upewnij się, że * ten * agent nie jest uwzględniony w obliczeniach i że
// badany agent jest sąsiadem if((neighbors[a] != m_pVehicle) && neighbors[a]->IsTagged())
{
CenterOfMass += neighbors[a]->Pos();
++NeighborCount;
}
}
if (NeighborCount > 0)
{
// środek masy jest średnią sumy pozycji
CenterOfMass /= (double)NeighborCount;
// teraz szukaj w kierunku tej pozycji
SteeringForce = Seek(CenterOfMass);
}
return SteeringForce;
}

Możesz być trochę rozczarowany, że nie uwzględniłem wersji demonstracyjnych dotyczących separacji, spójności i dostosowania. Cóż, jest ku temu powód: podobnie jak Itchy i Scratchy, same w sobie nie są szczególnie interesujące; są one o wiele bardziej doceniane, gdy są połączone, co przyjemnie przenosi nas do ucieczki.

Flokowanie

Flokowanie to zachowanie, o którym wspomniałem na początku tej części - to, co widziałem w dokumencie BBC. To piękna demonstracja tego, co stało się znane jako zachowanie wschodzące. Wyłaniające się zachowanie to zachowanie, które wygląda na złożone i / lub celowe dla obserwatora, ale w rzeczywistości pochodzi spontanicznie z dość prostych zasad. Podmioty niższego poziomu przestrzegające reguł nie mają pojęcia o szerszym obrazie; są świadomi tylko siebie i być może kilku swoich sąsiadów. Dobrym przykładem wschodu jest eksperyment przeprowadzony przez Chrisa Melhuisha i Owena Holland na University of the West of England. Melhuish i Holland są zainteresowani stygmatyzacją, dziedziną nauki częściowo zajmującą się pojawiającymi się zachowaniami owadów społecznych, takich jak mrówki, termity i pszczoły. Zainteresowali się sposobem, w jaki mrówki zbierają swoje martwe, jaja i inne materiały w stosy, a zwłaszcza mrówką Leptothorax, ponieważ żyje ona wśród pęknięć w skałach i działa, we wszystkich celach i celach, w 2D… podobnie jak robot kołowy . Obserwując Leptothorax w laboratorium, krzątanie się w ich symulowanym pęknięciu - dwie tafle szkła - zauważyli, że mrówki mają tendencję do spychania małych granulek materiału skalnego w gromady i zastanawiali się, czy mogą zaprojektować roboty zdolne do tego samego. Po odrobinie potu i trudu udało im się stworzyć roboty działające na bardzo prostych zasadach, zdolne do gromadzenia losowo rozrzuconych Frisbees w klastry. Roboty nie znały się nawzajem i nie wiedziały, co to jest gromada, a nawet Frisbee. Nie widzieli nawet Frisbees. Frisbee mogli popychać tylko za pomocą przedniego ramienia w kształcie litery U. Jak więc zachowuje się klastrowanie? Cóż, kiedy roboty są włączone, błąkają się, aż wpadną na frisbee. Pojedynczy Frisbee nie zmienia zachowania robota. Jednak gdy robot Frisbeepushing wpada na drugiego Frisbee, natychmiast pozostawia dwa Frisbee tam, gdzie się znajdują, cofa się nieco, obraca o losową liczbę, a następnie ponownie odchodzi. Wykorzystując te proste zasady i trochę czasu, kilka robotów zepchnie wszystkie Frisbee do kilku dużych gromad. Tak jak mrówki. W każdym razie pozwólcie, że porzucę całą tę rozmowę o Frisbees i wrócę do trzody. Flokowanie, jak pierwotnie opisał Reynolds, jest kombinacją trzech wcześniej opisanych zachowań grupowych: separacja, wyrównanie i spójność. Działa to dobrze, ale z powodu ograniczonej odległości widzenia pojazdu agent może zostać odizolowany od stada. Jeśli tak się stanie, po prostu usiądzie i nic nie zrobi. Aby temu zapobiec, wolę również dodać zachowanie wędrówki. W ten sposób wszyscy agenci cały czas się poruszają. Ulepszenie wielkości każdego z zachowań przyczyni się do uzyskania różnych efektów, takich jak ławice ryb, luźne wirujące stada ptaków lub tętniące życiem stada owiec. Udało mi się nawet stworzyć gęste stada setek drobnych cząstek przypominających meduzę. Ponieważ takie zachowanie jest lepiej widoczne niż opisane, polecam otworzyć plik demo i chwilę się pobawić. Uwaga: uciekajcie, ponieważ uciekają! (Być może dlatego niektóre zwierzęta tak bardzo to lubią…) Możesz dostosować wpływ każdego zachowania za pomocą klawiszy "A / Z", "S / X" i "D / C". Ponadto możesz przeglądać sąsiadów jednego z agentów naciskając klawisz "G".

CIEKAWY FAKT. Zachowania sterujące są często wykorzystywane do tworzenia efektów specjalnych w filmach. Pierwszym filmem, w którym zastosowano zachowanie uciekające, był Powrót Batmana, w którym można zobaczyć stada nietoperzy i stada pingwinów. Najnowsze filmy przedstawiające zachowania kierownicze to trylogia Władca Pierścieni w reżyserii Petera Jacksona. Ruch armii orków w tych filmach jest tworzony za pomocą zachowań sterujących za pomocą oprogramowania o nazwie Massive. Teraz, gdy zobaczyłeś zalety, przyjrzyjmy się dokładnie, jak można łączyć zachowania kierownicze.

Łączenie zachowań sterujących

Często będziesz używać kombinacji zachowań sterujących, aby uzyskać pożądane zachowanie. Bardzo rzadko będziesz używać tylko jednego zachowania w izolacji. Na przykład, możesz chcieć wdrożyć bota FPS, który będzie działał od A do B (podążanie ścieżką), unikając innych botów (separacja) i ścian (unikanie ścian), które mogą próbować utrudnić jego postęp. Lub możesz chcieć, aby owce, które zaimplementowałeś jako zasób pokarmowy w swojej grze RTS, gromadziły się razem (flokowały), jednocześnie wędrując po otoczeniu (wędrując), unikając drzew (omijanie przeszkód) i rozpraszając (unikając) za każdym razem, gdy człowiek lub pies zbliża się. Wszystkie zachowania sterujące opisane tu są metodami jednej klasy: SteeringBehaviors. Pojazd posiada instancję tej klasy i aktywuje / dezaktywuje różne zachowania poprzez włączanie i wyłączanie ich za pomocą metod akcesoriów. Na przykład, aby ustawić jedną z owiec w sytuacji opisanej w poprzednim akapicie, możesz zrobić coś takiego (zakładając, że agent podobny do psa został już utworzony):

Vehicle* Sheep = new Vehicle();
Sheep->Steering()->SeparationOn();
Sheep->Steering()->AlignmentOn();
Sheep->Steering()->CohesionOn();
Sheep->Steering()->ObstacleAvoidanceOn(); Sheep->Steering()->WanderOn();
Sheep->Steering()->EvadeOn(Dog);
I odtąd owce będą się o siebie troszczyć! (Być może jednak będziesz musiał to ścinać latem.)

UWAGA. Klasa SteeringBehaviors jest ogromna i zawiera znacznie więcej kodu, niż kiedykolwiek byłby używany w jednym projekcie. Bardzo rzadko używasz więcej niż kilku zachowań w każdej projektowanej grze. Dlatego za każdym razem, gdy używam zachowań sterujących w późniejszych rozdziałach, będę używać skróconej wersji klasy SteeringBehaviors, dostosowanej do konkretnego zadania. Sugeruję, abyś zrobił to samo. (Innym podejściem jest zdefiniowanie osobnej klasy dla każdego zachowania i dodanie ich do std :: container według potrzeb). Wewnątrz metody Vehicle :: Update zobaczysz następujący wiersz:

SVector2D SteeringForce = m_pSteering-> Calculate ();

To wywołanie określa siłę wynikową ze wszystkich aktywnych zachowań. Nie jest to jednak suma wszystkich sił kierujących. Nie zapominaj, że pojazd jest ograniczony przez maksymalną siłę kierowania, więc ta suma musi zostać w jakiś sposób obcięta, aby jego wielkość nigdy nie przekroczyła limitu. Można to zrobić na wiele sposobów. Nie można powiedzieć, czy jedna metoda jest lepsza od innej, ponieważ zależy to od tego, z jakimi zachowaniami musisz pracować i jakie zasoby procesora musisz oszczędzić. Wszystkie mają swoje zalety i wady. Zdecydowanie polecam eksperymentować dla siebie.

Ważona okrojona suma

Najprostszym podejściem jest pomnożenie każdego sposobu kierowania przez wagę, zsumowanie ich wszystkich razem, a następnie obcięcie wyniku do maksymalnej dopuszczalnej siły kierowania. Lubię to:

SVector2D Calculate ()
{
SVector2D SteeringForce;
SteeringForce + = Wander () * dWanderAmount;
SteeringForce + = ObstacleAvoidance () * dObstacleAvoidanceAmount;
SteeringForce + = Separation () * dSeparationAmount;
zwraca SteeringForce.Truncate (MAX_STEERING_FORCE);
}
Może to działać dobrze, ale kompromis polega na tym, że wiąże się z kilkoma problemami. Pierwszy problem polega na tym, że ponieważ każde aktywne zachowanie jest obliczane za każdym razem, jest to bardzo kosztowna metoda przetwarzania. Dodatkowo, ciężary zachowania mogą być bardzo trudne do dostosowania. (Czy mówiłem, że jest trudny? Przepraszam, mam na myśli bardzo trudny!) Największy problem zdarza się jednak w przypadku sprzecznych sił - częstym scenariuszem jest podpieranie pojazdu o ścianę przez kilka innych pojazdów. W tym przykładzie siły oddzielające od sąsiednich pojazdów mogą być większe niż siła odpychająca od ściany, a pojazd może ostatecznie zostać przepchnięty przez granicę ściany. To prawie na pewno nie będzie korzystne. Jasne, że możesz sprawić, by ciężary unikania ścian były ogromne, ale wtedy twój pojazd może zachowywać się dziwnie następnym razem, gdy znajdzie się sam i przy ścianie. Jak już wspomniałem, poprawianie parametrów ważonej sumy może być dość żonglerką!

Ważona obcięta suma bieżąca z ustalaniem priorytetów

Co za kęs! Jest to metoda stosowana do określenia sił sterujących dla wszystkich przykładów użytych tu, wybrana głównie dlatego, że daje dobry kompromis między prędkością a dokładnością. Metoda ta polega na obliczeniu ważonej sumy całkowitej ważonej, która jest obcinana po dodaniu każdej siły, aby upewnić się, że wielkość siły kierującej nie przekracza maksymalnej dostępnej. Priorytetem są zachowania kierownicze, ponieważ niektóre zachowania można uznać za znacznie ważniejsze od innych. Powiedzmy, że pojazd wykorzystuje separację zachowań, wyrównanie, spójność, unikanie ścian i omijanie przeszkód. Unikanie ścian i zachowania związane z unikaniem przeszkód powinny mieć pierwszeństwo przed innymi, ponieważ pojazd powinien starać się nie przecinać ściany ani przeszkody - ważniejsze jest, aby pojazd unikał ściany niż zrównanie się z innym pojazdem. Jeśli to, czego potrzeba, aby ominąć ścianę, to wyrównanie higgledy-piggledy, to prawdopodobnie będzie dobrze i z pewnością lepsze niż kolizja ze ścianą. Ważniejsze jest również, aby pojazdy zachowały pewną odległość od siebie niż wyrównanie. Ale prawdopodobnie mniej ważne jest, aby pojazdy utrzymywały separację niż omijały ściany. Widzisz, gdzie idę z tym? Każde zachowanie jest traktowane priorytetowo i przetwarzane w kolejności. Zachowania o najwyższym priorytecie są przetwarzane jako pierwsze, te o najniższym, ostatnie. Oprócz ustalania priorytetów, ta metoda dokonuje iteracji przez każde aktywne zachowanie, sumując siły (wraz z ważeniem) w miarę upływu czasu. Natychmiast po obliczeniu każdego nowego zachowania siła wypadkowa wraz z sumą bieżącą jest wysyłana do metody o nazwie AccumulateForce. Ta funkcja określa najpierw pozostałą część maksymalnej dostępnej siły kierowania, a następnie następuje jedna z następujących czynności:

•  Jeśli pozostała nadwyżka, nowa siła jest dodawana do bieżącej sumy.
•  Jeśli nie ma już nadwyżki, metoda zwraca false. Gdy tak się stanie, funkcja Calculate natychmiast zwraca bieżącą wartość parametru m_vSteeringForce i nie bierze pod uwagę żadnych dalszych aktywnych zachowań.
•  Jeśli nadal dostępna jest pewna siła kierująca, ale pozostała wielkość jest mniejsza niż wielkość nowej siły, nowa siła zostaje obcięta do pozostałej wielkości przed jej dodaniem.

Oto fragment kodu z metody SteeringBehaviors :: Calculate, aby pomóc Ci lepiej zrozumieć, o czym mówię.

SVector2D SteeringBehaviors::Calculate()
{
// zresetuj siłę.
m_vSteeringForce.Zero();
SVector2D force;
if (On(wall_avoidance))
{
force = WallAvoidance(m_pVehicle->World()->Walls()) *
m_dMultWallAvoidance;
if (!AccumulateForce(m_vSteeringForce, force)) return m_vSteeringForce;
}
if (On(obstacle_avoidance))
{
force = ObstacleAvoidance(m_pVehicle->World()->Obstacles()) *
m_dMultObstacleAvoidance;
if (!AccumulateForce(m_vSteeringForce, force)) return m_vSteeringForce;
}
if (On(separation))
{
force = Separation(m_pVehicle->World()->Agents()) *
m_dMultSeparation;
if (!AccumulateForce(m_vSteeringForce, force)) return m_vSteeringForce;
}
/* EXTRANEOUS STEERING FORCES OMITTED */
return m_vSteeringForce;
}

Nie pokazuje to wszystkich sił kierujących, tylko kilka, dzięki czemu można uzyskać ogólny pomysł. Aby zobaczyć listę wszystkich zachowań i ich kolejność ustalanie priorytetów, sprawdź metodę SteeringBehaviors :: Calculate w swoim IDE. Metodę AccumulateForce można również lepiej wyjaśnić w kodzie. Nie spiesz się, przeglądając tę metodę i upewnij się, że rozumiesz, co ona robi.

bool SteeringBehaviors::AccumulateForce(Vector2D &RunningTot,
Vector2D ForceToAdd)
{
// obliczyć, ile siły kierującej zużył dotychczas pojazd
double MagnitudeSoFar = RunningTot.Length();
// obliczyć, ile siły kierującej ma zużyć ten pojazd
double MagnitudeRemaining = m_pVehicle->MaxForce() - MagnitudeSoFar;
// zwraca false, jeśli nie ma już siły, aby użyć
if (MagnitudeRemaining <= 0.0) return false;
// oblicz wielkość siły, którą chcemy dodać double MagnitudeToAdd = ForceToAdd.Length();
// jeśli wielkość sumy ForceToAdd i bieżącej sumy
// nie przekracza maksymalnej siły dostępnej dla tego pojazdu, tylko
// dodać razem. W przeciwnym razie dodaj tyle wektora ForceToAdd, ile
// możliwe bez przekraczania maks.
if (MagnitudeToAdd < MagnitudeRemaining)
{
RunningTot += ForceToAdd;
}
else
{
// dodaj go do siły kierowania
RunningTot += (Vec2DNormalize(ForceToAdd) * MagnitudeRemaining);
}
return true;
}

Dithering z priorytetem

W swoim artykule Reynolds sugeruje metodę kombinacji sił, którą nazywa priorytetem ditheringu. Gdy jest używana, ta metoda sprawdza, czy zachowanie pierwszego priorytetu będzie oceniane na tym etapie symulacji, w zależności od ustalonego prawdopodobieństwa. Jeśli tak, a wynik jest niezerowy, metoda zwraca obliczoną siłę i nie są brane pod uwagę inne aktywne zachowania. Jeśli wynik wynosi zero lub jeśli zachowanie to zostało pominięte ze względu na prawdopodobieństwo jego oceny, rozważane jest zachowanie o kolejnym priorytecie i tak dalej, dla wszystkich aktywnych zachowań. To jest mały fragment kodu, który pomoże ci zrozumieć tę koncepcję:

SVector2D SteeringBehaviors::CalculateDithered()
{
// zresetować siłę kierowania
m_vSteeringForce.Zero();
// prawdopodobieństwa zachowania
const double prWallAvoidance = 0.9;
const double prObstacleAvoidance = 0.9;
const double prSeparation = 0.8;
const double prAlignment = 0.5;
const double prCohesion = 0.5;
const double prWander = 0.8;
if (On(wall_avoidance) && RandFloat() > prWallAvoidance)
{
m_vSteeringForce = WallAvoidance(m_pVehicle->World()->Walls()) *
m_dWeightWallAvoidance / prWallAvoidance;
if (!m_vSteeringForce.IsZero())
{
m_vSteeringForce.Truncate(m_pVehicle->MaxForce());
return m_vSteeringForce;
}
}
if (On(obstacle_avoidance) && RandFloat() > prObstacleAvoidance)
{
m_vSteeringForce += ObstacleAvoidance(m_pVehicle->World()->Obstacles()) *
m_dWeightObstacleAvoidance / prObstacleAvoidance;
if (!m_vSteeringForce.IsZero())
{
m_vSteeringForce.Truncate(m_pVehicle->MaxForce());
return m_vSteeringForce;
}
}
if (On(separation) && RandFloat() > prSeparation)
{
m_vSteeringForce += Separation(m_pVehicle->World()->Agents()) *
m_dWeightSeparation / prSeparation;
if (!m_vSteeringForce.IsZero())
{
m_vSteeringForce.Truncate(m_pVehicle->MaxForce());
return m_vSteeringForce;
}
}
/* ETC ETC */

Ta metoda wymaga znacznie mniej czasu procesora niż inne, ale kosztem dokładności. Ponadto będziesz musiał nieco zwiększyć prawdopodobieństwo, zanim uzyskasz takie zachowanie, jakie chcesz. Niemniej jednak, jeśli masz mało zasobów i nie jest konieczne, aby ruchy twojego agenta były precyzyjne, z pewnością warto eksperymentować z tą metodą. Możesz zobaczyć efekt każdej z trzech opisanych przeze mnie metod sumowania, uruchamiając wersję demonstracyjną Big Shoal / Big Shoal.exe. Ta demonstracja pokazuje, że ławica 300 małych pojazdów (myślę, że ryby) jest nieufna wobec jednego większego pojazdu wędrownego (myślę, że rekin). Możesz przełączać się między różnymi metodami sumowania, aby zaobserwować, jak wpływają one na częstotliwość klatek i dokładność zachowań. Możesz także dodać ściany lub przeszkody do otoczenia, aby zobaczyć, jak działają agenci używający różnych metod sumowania.

Zapewnienie zerowego nakładania się

Często podczas łączenia zachowań pojazdy czasami nakładają się na siebie. Sama siła sterująca separacją nie jest wystarczająca, aby temu zapobiec. Przez większość czasu jest to w porządku - niewielkie nakładanie się zostanie niezauważone przez gracza - ale czasami konieczne jest upewnienie się, że cokolwiek się stanie, pojazdy nie będą mogły przejeżdżać przez promienie ograniczające się nawzajem. Można temu zapobiec za pomocą ograniczenia niepenetrującego. Jest to funkcja testująca nakładanie się. Jeśli takie istnieją, pojazdy są odsunięte od siebie w kierunku od punktu styku (bez względu na ich masę, prędkość lub inne fizyczne ograniczenia).



Ograniczenie jest zaimplementowane jako szablon funkcji i może być używane dla dowolnych obiektów pochodzących z BaseGameEntity. Możesz znaleźć kod w nagłówku EntityFunctionTemplates.h i wygląda to tak:

template
void EnforceNonPenetrationConstraint(const T& entity,
const conT& ContainerOfEntities)
{
// iteruj przez wszystkie jednostki, sprawdzając, czy zachodzą na siebie promienie ograniczające
for (typename conT::const_iterator curEntity =ContainerOfEntities.begin();
curEntity != ContainerOfEntities.end();
++curEntity)
{
// upewnij się, że nie sprawdzimy w stosunku do danej osoby
if (*curEntity == entity) continue;
// obliczyć odległość między pozycjami jednostek
Vector2D ToEntity = entity->Pos() - (*curEntity)->Pos();
double DistFromEachOther = ToEntity.Length();
// jeśli ta odległość jest mniejsza niż suma ich promieni, to ta
// jednostka musi zostać odsunięta w kierunku równoległym do
// Wektor ToEntity
double AmountOfOverLap = (*curEntity)->BRadius() + entity->BRadius() -
DistFromEachOther;
if (AmountOfOverLap >= 0)
{
// odsuń byt na odległość równą odległości nakładania się.
entity->SetPos(entity->Pos() + (ToEntity/DistFromEachOther) *
AmountOfOverLap);
}
}// następny byt
}
Możesz obserwować działanie ograniczenia penetracji w akcji, uruchamiając sztucznie nazwaną wersję demonstracyjną Non Penetration Constraint.exe. Spróbuj zmienić ilość separacji, aby zobaczyć, jaki ma to wpływ na pojazdy.

UWAGA .W przypadku dużej liczby gęsto upakowanych pojazdów, takich jak w dużych zatłoczonych stadach, ograniczenie braku penetracji czasami się nie powiedzie i będzie się nakładać. Na szczęście nie stanowi to zwykle problemu, ponieważ nakładanie się jest trudne do dostrzeżenia ludzkim okiem.

Radzenie sobie z dużą ilością pojazdów: partycjonowanie przestrzenne

Kiedy masz wiele współdziałających pojazdów, coraz bardziej nieefektywne jest oznaczanie sąsiednich bytów poprzez porównywanie ich ze sobą. W teorii algorytmów coś, co nazywa się notacją Big O, służy do wyrażenia związku czasu potrzebnego do liczby przetwarzanych obiektów. Można powiedzieć, że metoda wszystkich par, której używamy do wyszukiwania sąsiednich pojazdów, działa w czasie O (n2). Oznacza to, że wraz ze wzrostem liczby pojazdów czas potrzebny do ich porównania rośnie proporcjonalnie do kwadratu ich liczby. Możesz łatwo zobaczyć, jak szybko ten czas się eskaluje. Jeśli przetworzenie jednego obiektu zajmie 10 sekund, wówczas przetworzenie 10 obiektów zajmie 100 sekund. Nie dobrze, jeśli chcesz stada kilkuset ptaków! Można dokonać dużych ulepszeń prędkości, dzieląc przestrzeń świata. Istnieje wiele różnych technik do wyboru. Prawdopodobnie słyszałeś o wielu z nich - drzewach BSP, poczwórnych, ośmiornicach itp. - a może nawet z nich korzystałeś, w takim przypadku zapoznasz się z ich zaletami. Metodę, której tu używam, nazywa się partycjonowaniem przestrzeni komórkowej, czasem nazywaną partycjonowaniem przestrzeni bin (tak przy okazji, to nie jest skrótem do partycjonowania przestrzeni binarnej; w tym przypadku "bin" naprawdę oznacza bin). Dzięki tej metodzie przestrzeń 2D jest dzielona na kilka komórek (lub pojemników). Każda komórka zawiera listę wskaźników do wszystkich zawartych w niej encji. Jest aktualizowany (w metodzie aktualizacji jednostki) za każdym razem, gdy jednostka zmienia pozycję. Jeśli jednostka przenosi się do nowej komórki, jest usuwana z listy starej komórki i dodawana do bieżącej. W ten sposób, zamiast konieczności testowania każdego pojazdu względem siebie, możemy po prostu ustalić, które komórki leżą w sąsiedztwie pojazdu i przetestować pojazdy znajdujące się w tych komórkach. Oto jak to zrobić krok po kroku:

1. Po pierwsze, promień ograniczający jednostki jest przybliżany za pomocą ramki.
2. Komórki przecinające się z tym polem są testowane, aby sprawdzić, czy zawierają jakiekolwiek elementy.
3. Wszystkie byty zawarte w komórkach z kroku 2 są badane, aby sprawdzić, czy znajdują się w promieniu sąsiedztwa. Jeśli tak, są dodawane do listy okolic.



UWAGA. Jeśli pracujesz w 3D, po prostu utwórz kostki komórek i użyj kuli jako regionu sąsiedztwa.

Jeśli jednostki utrzymują minimalną odległość od siebie, to liczba jednostek, które każda komórka może zawierać, jest skończona, a podział przestrzeni komórki będzie działał w czasie O (n). Oznacza to, że czas potrzebny na przetworzenie algorytmu jest wprost proporcjonalny do liczby obiektów, na których działa. Jeśli liczba obiektów jest podwojona, czas jest podwojony i nie jest zwiększany do kwadratu, jak w przypadku algorytmów O (n2). Oznacza to, że korzyść, jaką zyskujesz dzięki partycjonowaniu przestrzeni w porównaniu ze standardową techniką wszystkich par, zależy od liczby agentów, których poruszasz się. W przypadku małych liczb, powiedzmy mniej niż pięćdziesiąt, nie ma prawdziwej przewagi; ale w przypadku dużych liczb partycjonowanie w przestrzeni komórkowej może być znacznie szybsze. Nawet jeśli jednostki nie utrzymują minimalnej odległości separacji i czasami dochodzi do nakładania się, średnio algorytm będzie działał znacznie lepiej niż O (n2). Zaimplementowałem partycjonowanie przestrzeni komórkowej jako szablon klasy: CellSpacePartition. Ta klasa używa innego szablonu klasy, Komórka, do zdefiniowania struktury komórki.

template
struct Cell
{
// wszystkie byty zamieszkujące tę komórkę
std::list Members;
// obwiednia komórki (jest odwrócona, ponieważ domyślna
systemu Windows
// układ współrzędnych ma oś y, która rośnie w miarę opadania)
InvertedAABBox2D BBox;
Cell(Vector2D topleft,
Vector2D botright):BBox(InvertedAABBox2D(topleft, botright))
{}
};

Cell jest bardzo prostą strukturą. Zawiera instancję klasy ramki ograniczającej, która określa jej zakres, oraz listę wskaźników dla wszystkich tych jednostek, które znajdują się w tym obszarze ograniczającym. Definicja klasy CellSpacePartition jest następująca:

emplate
class CellSpacePartition
{
private:
//wymagana liczba komórek w przestrzeni
std::vector > m_Cells;
// służy to do przechowywania ważnych sąsiadów, gdy agent wyszukuje
// jego sąsiednia przestrzeń
std::vector m_Neighbors;
// ten iterator zostanie użyty przez n
std::vector::iterator m_curNeighbor;
//szerokość i wysokość przestrzeni świata, w której żyją byty
double m_dSpaceWidth;
double m_dSpaceHeight;
//liczba komórek, na które ma zostać podzielona przestrzeń
int m_iNumCellsX;
int m_iNumCellsY;
double m_dCellSizeX;
double m_dCellSizeY;
// biorąc pod uwagę pozycję w obszarze gry, ta metoda określa
// odpowiedni indeks komórki
inline int PositionToIndex(const Vector2D& pos)const;
public:
CellSpacePartition(double width, // szerokość środowiska
double height, // wysokość …
int cellsX, // liczba komórek w poziomie
int cellsY, // liczba komórek w pionie
int MaxEntitys); // maksymalna liczba podmiotów do dodania
// dodaje jednostki do klasy, przydzielając je do odpowiedniej komórki
inline void AddEntity(const entity& ent);
// zaktualizuj komórkę jednostki, wywołując ją z metody Update swojej jednostki
inline void UpdateEntity(const entity& ent, Vector2D OldPos);
// ta metoda oblicza wszystkich sąsiadów celu i przechowuje je w
// wektor sąsiada. Po wywołaniu tej metody użyj przycisku begin,
// next i zakończ metody iteracji po wektorze.
inline void CalculateNeighbors(Vector2D TargetPos, double QueryRadius);
// zwraca odwołanie do encji z przodu wektora sąsiada
inline entity& begin();
// zwraca to następny byt w wektorze sąsiednim
inline entity& next();
// zwraca true, jeśli znaleziono koniec wektora (zerowa wartość oznacza koniec)
inline bool end();
// opróżnia komórki bytów
void EmptyCells();
};

Klasa inicjuje m_Neighbors, aby mieć maksymalny rozmiar równy całkowitej liczbie bytów na świecie. Metody iteratora rozpoczynają się, kończą i kończą, a metoda CalculateNeighbors ręcznie śledzi prawidłowe elementy wewnątrz tego wektora. Zapobiega to spowolnieniu związanemu z kosztami alokacji pamięci i delokalizacji wielokrotnego wywoływania std :: vector :: clear () i std :: vector :: push_back () wiele razy na sekundę. Zamiast tego poprzednie wartości są po prostu nadpisywane, a wartość zerowa służy do oznaczenia końca wektora. Oto lista metod CalculateNeighbors. Zwróć uwagę, w jaki sposób postępuje zgodnie z krokami opisanymi wcześniej, aby określić sąsiadów pojazdu.

template
void CellSpacePartition::CalculateNeighbors(Vector2D TargetPos,
double QueryRadius)
{
// utwórz iterator i ustaw go na początku listy sąsiadów
std::list::iterator curNbor = m_Neighbors.begin();
// utwórz pole zapytania, które jest ramką graniczną zapytania celu
//powierzchnia
InvertedAABBox2D QueryBox(TargetPos - Vector2D(QueryRadius, QueryRadius),
TargetPos + Vector2D(QueryRadius, QueryRadius));
// iteruj po każdej komórce i sprawdź, czy jej obwiednia się pokrywa // z polem zapytania. Jeśli tak, a także zawiera byty, to
// wykonaj dalsze testy zbliżeniowe.
std::vector >::iterator curCell;
for (curCell=m_Cells.begin(); curCell!=m_Cells.end(); ++curCell)
{
// przetestuj, czy ta komórka zawiera elementy i czy pokrywa się z
// pole zapytania
if (curCell->pBBox->isOverlappedWith(QueryBox) &&
!curCell->Members.empty())
{
// dodaj wszelkie jednostki znalezione w promieniu zapytania do listy sąsiadów
std::list::iterator it = curCell->Members.begin();
for (it; it!=curCell->Members.end(); ++it)
{
if (Vec2DDistanceSq((*it)->Pos(), TargetPos) <
QueryRadius*QueryRadius)
{
*curNbor++ = *it;
}
}
}
}// następna komórka
// zaznacz koniec listy zerem.
*curNbor = 0;
}

CellSpacePartition.h. Dodałem partycjonowanie przestrzeni komórkowej do wersji demo Big_Shoal.exe. Teraz nazywa się Another_Big_Shoal.exe. Możesz włączać i wyłączać partycjonowanie i zobaczyć różnicę, jaką robi to dla liczby klatek na sekundę. Istnieje również opcja podglądu podziału przestrzeni (domyślnie jest to 7 x 7 komórek) oraz pola zapytania i promienia sąsiedztwa jednego z agentów.

WSKAZÓWKA. Podczas przykładania siły kierowania do niektórych typów pojazdów przydatne może być rozdzielenie wektora kierowania na części przednie i boczne. Na przykład w przypadku samochodu byłoby to analogiczne do wytworzenia odpowiednio przepustnicy i sił kierujących

Wygładzanie

Podczas gry z wersjami demonstracyjnymi możesz zauważyć, że czasami pojazd może lekko drgać lub drgać, gdy znajdzie się w sytuacji, w której występują sprzeczne reakcje różnych zachowań. Na przykład, jeśli uruchomisz jedną z wersji Big Shoal i włączysz przeszkody i ściany, zobaczysz, że czasami, gdy agent "rekina" zbliży się do ściany lub przeszkody, nos drży lub drży. Wynika to z tego, że w jednym kroku aktualizacji zachowanie polegające na unikaniu przeszkód przywraca siłę kierującą od przeszkody, ale w następnym kroku aktualizacji nie ma zagrożenia z powodu przeszkody, więc jednym z innych aktywnych zachowań agenta mogą przywrócić siłę kierującą, pociągając jego kurs z powrotem w kierunku przeszkody i tak dalej, powodując niepożądane oscylacje w kierunku pojazdu. Rysunek poniższy pokazuje, jak można rozpocząć te oscylacje za pomocą tylko dwóch sprzecznych zachowań: unikania przeszkód i poszukiwania.



Ta drżenie zwykle nie jest zbyt zauważalne. Czasami jednak zdarza się, że lepiej będzie, gdy wstrząsanie nie nastąpi. Jak to zatrzymać? Ponieważ prędkość pojazdu jest zawsze zgodna z jego kierunkiem, zatrzymanie wstrząsów nie jest trywialne. Aby pomyślnie i bezproblemowo negocjować scenariusz przedstawiony na powyższym rysunku, pojazd musi być w stanie przewidzieć konflikt z wyprzedzeniem i odpowiednio zmienić zachowanie. Chociaż można to zrobić, rozwiązanie może wymagać wielu obliczeń i dodatkowego bagażu. Prostą alternatywą sugerowaną przez Robina Greena z Sony jest oddzielenie kursu od wektora prędkości i uśrednienie jego wartości w kilku krokach aktualizacji. Chociaż to rozwiązanie nie jest idealne, daje odpowiednie wyniki przy niskim koszcie (w porównaniu do każdego innego rozwiązania, które znam o). Aby to ułatwić, do klasy Vehicle dodaje się inną zmienną składową: m_vSmoothedHeading. Ten wektor rejestruje średnią wektora kursu pojazdu i jest aktualizowany na każdym etapie symulacji (w Vehicle :: Update), wykorzystując wywołanie do instancji Smoother - klasy, która pobiera próbkę wartości z zakresu i zwraca średnią. Tak wygląda połączenie:

if (SmoothingIsOn())
{
m_vSmoothedHeading = m_pHeadingSmoother->Update(Heading());
}

Ten wygładzony wektor kursowy jest używany przez funkcję transformacji świata w wywołaniu renderowania do przekształcania wierzchołków pojazdu w ekran. Liczba kroków aktualizacji, których używa Smoother do obliczenia średniej, jest ustawiona w params.ini i jest przypisana do zmiennej NumSamplesForSmoothing Dostosowując tę wartość, należy starać się utrzymywać ją na możliwie najniższym poziomie, aby uniknąć niepotrzebnych obliczeń i zużycia pamięci. Używanie bardzo wysokich wartości powoduje dziwne zachowanie. Spróbuj użyć wartości 100 dla NumSamplesForSmoothing, a zobaczysz, co mam na myśli. Przypomina mi cytat z Poradnika Autostopowicza po Galaktyce: "Wiesz," powiedział Arthur z lekkim kaszlem, "jeśli to Southend, jest w tym coś dziwnego…" "Masz na myśli sposób, w jaki morze pozostaje stabilne, a budynki ciągle się umywają?", Powiedział Ford. "Tak, myślałem, że to również dziwne." Możesz zobaczyć różnicę wygładzania, jeśli uruchomisz plik Another_Big_Shoal z plikiem wygładzającym.

Praktyka czyni mistrza

W swoim artykule "Zachowania sterujące dla postaci autonomicznych" Reynolds opisuje zachowanie zwane liderem. Podążanie za liderem to zachowanie, które tworzy siłę kierującą, która utrzymuje wiele pojazdów w ruchu w jednym pliku za pojazdem lidera. Jeśli kiedykolwiek widziałeś, jak pisklęta gęsie podążają za ich matką, będziesz wiedział, co mam na myśli. Aby stworzyć tego rodzaju zachowanie, obserwujący muszą dojść do pozycji przesuniętej za pojazdem z przodu, używając separacji, aby pozostać oddzielone od siebie.



Podążanie za liderem można jeszcze ulepszyć, tworząc zachowanie, które kieruje pojazd w bok od lidera, jeśli znajdzie się na ścieżce lidera. Utwórz grupę 20 pojazdów, które zachowują się jak stado owiec. Teraz dodaj pojazd kontrolowany przez użytkownika, którym możesz sterować za pomocą klawiatury. Zaprogramuj swoje owce, aby wierzyły, że pojazd to pies. Czy potrafisz sprawić, by zachowanie stada wyglądało realistycznie?