Trudno mi znaleźć sposób na zorganizowanie obiektów w grze, aby były polimorficzne, ale jednocześnie nie były polimorficzne.
Oto przykład: zakładając, że chcemy, aby wszystkie nasze obiekty były update()
i draw()
. W tym celu musimy zdefiniować klasę podstawową, GameObject
która ma te dwie wirtualne czyste metody i wpuszcza polimorfizm:
class World {
private:
std::vector<GameObject*> objects;
public:
// ...
update() {
for (auto& o : objects) o->update();
for (auto& o : objects) o->draw(window);
}
};
Metoda aktualizacji ma za zadanie dbać o to, jaki stan konkretny obiekt klasy musi zaktualizować. Faktem jest, że każdy przedmiot musi wiedzieć o otaczającym go świecie. Na przykład:
- Kopalnia musi wiedzieć, czy ktoś się z nią koliduje
- Żołnierz powinien wiedzieć, czy żołnierz innego zespołu znajduje się w pobliżu
- Zombie powinien wiedzieć, gdzie znajduje się najbliższy mózg w promieniu
W przypadku interakcji pasywnych (jak ta pierwsza) myślałem, że wykrycie kolizji może przekazać zadanie do wykonania w konkretnych przypadkach kolizji samemu obiektowi za pomocą on_collide(GameObject*)
.
Większość innych informacji (podobnie jak pozostałe dwa przykłady) może być po prostu zapytana przez świat gry przekazany tej update
metodzie. Teraz świat nie rozróżnia obiektów na podstawie ich typu (przechowuje wszystkie obiekty w pojedynczym polimorficznym pojemniku), więc to, co w rzeczywistości zwróci z ideałem, world.entities_in(center, radius)
to pojemnik GameObject*
. Ale oczywiście żołnierz nie chce atakować innych żołnierzy ze swojej drużyny, a zombie nie dba o inne zombie. Musimy więc odróżnić zachowanie. Rozwiązaniem może być:
void TeamASoldier::update(const World& world) {
auto list = world.entities_in(position, eye_sight);
for (const auto& e : list)
if (auto enemy = dynamic_cast<TeamBSoldier*>(e))
// shoot towards enemy
}
void Zombie::update(const World& world) {
auto list = world.entities_in(position, eye_sight);
for (const auto& e : list)
if (auto enemy = dynamic_cast<Human*>(e))
// go and eat brain
}
ale oczywiście liczba dynamic_cast<>
klatek na sekundę może być strasznie wysoka i wszyscy wiemy, jak powolny dynamic_cast
może być. Ten sam problem dotyczy również on_collide(GameObject*)
delegata, o którym mówiliśmy wcześniej.
Jaki jest więc idealny sposób na uporządkowanie kodu, aby obiekty były świadome innych obiektów i mogły je ignorować lub podejmować działania w zależności od ich typu?
źródło
dynamic_cast<Human*>
, zaimplementuj coś w rodzaju abool GameObject::IsHuman()
, który zwracafalse
domyślnie, ale jest zastępowany, aby zwrócićtrue
wHuman
klasie.IsA
przesłonięć okazało się dla mnie tylko nieznacznie lepsze niż casting dynamiczny. Najlepszym rozwiązaniem jest posortowanie list danych, o ile to możliwe, zamiast ślepego iterowania całej puli jednostek.TeamASoldier
iTeamBSoldier
jest naprawdę identyczna - zastrzelony na kogokolwiek z innego zespołu. Wszystkim, czego potrzebuje od innych bytów, jestGetTeam()
metoda w najbardziej swoistej formie, a na przykładzie congusbongus można ją jeszcze bardziej przekształcić wIsEnemyOf(this)
rodzaj interfejsu. Kod nie musi dbać o klasyfikacje taksonomiczne żołnierzy, zombie, graczy itp. Skoncentruj się na interakcji, a nie typach.Odpowiedzi:
Zamiast wdrażać proces decyzyjny każdego podmiotu sam w sobie, możesz alternatywnie wybrać wzorzec kontrolera. Miałbyś klasy centralnego kontrolera, które są świadome wszystkich obiektów (które są dla nich ważne) i kontrolują ich zachowanie.
MovementController obsługiwałby ruch wszystkich obiektów, które mogą się poruszać (wyszukiwanie trasy, aktualizowanie pozycji na podstawie bieżących wektorów ruchu).
MineBehaviorController sprawdzałby wszystkie miny i wszystkich żołnierzy i kazał kopalni eksplodować, gdy żołnierz zbliży się zbyt blisko.
ZombieBehaviorController sprawdziłby wszystkie zombie i żołnierzy w ich pobliżu, wybrał najlepszy cel dla każdego zombie i kazał mu się tam przenieść i zaatakować (sam ruch jest obsługiwany przez MovementController).
SoldierBehaviorController przeanalizuje całą sytuację, a następnie zaproponuje taktyczne instrukcje dla wszystkich żołnierzy (ruszasz się tam, strzelasz do niego, leczysz tego faceta ...). Rzeczywiste wykonanie tych poleceń wyższego poziomu byłyby również obsługiwane przez kontrolery niższego poziomu. Kiedy włożysz w to trochę wysiłku, możesz sprawić, że sztuczna inteligencja będzie w stanie podejmować całkiem sprytne decyzje kooperacyjne.
źródło
std::map
si, a encje są tylko identyfikatorami, a następnie musimy stworzyć jakiś rodzaj systemu typów (być może z komponentem znacznika, ponieważ renderer będzie musiał wiedzieć, co narysować); a jeśli nie chcemy tego robić, będziemy musieli mieć komponent rysunkowy: ale potrzebuje on komponentu pozycji, aby wiedzieć, gdzie zostać narysowanym, dlatego tworzymy zależności między komponentami, które rozwiązujemy za pomocą bardzo złożonego systemu przesyłania komunikatów. Czy to właśnie sugerujesz?Przede wszystkim spróbuj zaimplementować funkcje, aby obiekty pozostały niezależne od siebie, o ile to możliwe. Szczególnie chcesz to zrobić dla wielowątkowości. W pierwszym przykładzie kodu zestaw wszystkich obiektów można podzielić na zestawy pasujące do liczby rdzeni procesora i bardzo skutecznie zaktualizować.
Ale, jak powiedziałeś, interakcja z innymi obiektami jest potrzebna w przypadku niektórych funkcji. Oznacza to, że stan wszystkich obiektów musi być zsynchronizowany w niektórych punktach. Innymi słowy, aplikacja musi najpierw poczekać na zakończenie wszystkich równoległych zadań, a następnie zastosować obliczenia wymagające interakcji. Dobrze jest zmniejszyć liczbę tych punktów synchronizacji, ponieważ zawsze implikują, że niektóre wątki muszą czekać na zakończenie innych.
Dlatego sugeruję buforowanie tych informacji o obiektach, które są potrzebne z wnętrza innych obiektów. Biorąc pod uwagę taki bufor globalny, możesz aktualizować wszystkie obiekty niezależnie od siebie, ale tylko od siebie i od bufora globalnego, który jest zarówno szybszy, jak i łatwiejszy w utrzymaniu. Po ustalonym czasie, powiedzmy po każdej klatce, zaktualizuj bufor bieżącym stanem obiektów.
Więc raz na klatkę robisz: 1. buforuj globalnie stan bieżącego obiektu, 2. zaktualizuj wszystkie obiekty na podstawie siebie i bufora, 3. narysuj obiekty, a następnie zacznij od odnowienia bufora.
źródło
Użyj systemu opartego na komponentach, w którym masz podstawowy GameObject zawierający 1 lub więcej komponentów, które określają ich zachowanie.
Na przykład powiedz, że jakiś obiekt ma się cały czas poruszać w lewo i prawo (platforma), możesz utworzyć taki komponent i dołączyć go do GameObject.
Powiedzmy teraz, że obiekt gry powinien się cały czas powoli obracać, możesz utworzyć osobny komponent, który to robi i dołączyć go do GameObject.
Co jeśli chcesz mieć ruchomą platformę, która również się obraca, w tradycyjnej heirarchii klasowej, która staje się trudna bez powielania kodu.
Zaletą tego systemu jest to, że zamiast mieć klasę obrotową lub MovingPlatform, dołączasz oba te komponenty do GameObject, a teraz masz MovingPlatform, która automatycznie się obraca.
Wszystkie komponenty mają właściwość „wymaga aktualizacji”, która, gdy jest prawdziwa, GameObject wywoła metodę „aktualizacji” na tym komponencie. Na przykład, powiedzmy, że masz komponent Przeciągalny, ten komponent po najechaniu myszką (jeśli był ponad GameObject) może ustawić „wymaga aktualizacji” na true, a następnie przy najechaniu myszką ustawić na false. Zezwolenie na podążanie za myszą tylko wtedy, gdy mysz jest opuszczona.
Jeden z twórców Tony Hawk Pro Skater napisał o tym defacto i warto przeczytać: http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/
źródło
Preferuj kompozycję zamiast dziedziczenia.
Moja najsilniejsza rada poza tym brzmiałaby: nie daj się wciągnąć w sposób myślenia „Chcę, aby było to wyjątkowo elastyczne”. Elastyczność jest świetna, ale pamiętaj, że na pewnym poziomie, w dowolnym systemie skończonym, takim jak gra, istnieją części atomowe, które są używane do budowy całości. Tak czy inaczej, twoje przetwarzanie opiera się na tych predefiniowanych typach atomowych. Innymi słowy, uwzględnienie „dowolnego” typu danych (jeśli byłoby to możliwe) nie pomogłoby ci na dłuższą metę, jeśli nie masz kodu do ich przetworzenia. Zasadniczo cały kod musi analizować / przetwarzać dane w oparciu o znane specyfikacje ... co oznacza predefiniowany zestaw typów. Jak duży jest ten zestaw? Zależy od Ciebie.
Ten artykuł oferuje wgląd w zasadę Composition over Inheritance w rozwoju gier dzięki solidnej i wydajnej architekturze encji i komponentów.
Budując byty z (różnych) podzbiorów jakiegoś nadzbioru predefiniowanych komponentów, oferujesz swoim SI konkretny, fragmentaryczny sposób pojmowania świata i otaczających go aktorów, poprzez czytanie stanów tych aktorów.
źródło
Osobiście zalecam trzymanie funkcji rysowania poza samą klasą Object. Polecam nawet trzymanie lokalizacji / współrzędnych obiektów poza samym obiektem.
Ta metoda draw () będzie zajmować się API renderowania niskiego poziomu OpenGL, OpenGL ES, Direct3D, warstwą owijania na tych interfejsach API lub interfejsem API silników. Może być tak, że musisz przełączać się między nimi (jeśli na przykład chcesz obsługiwać OpenGL + OpenGL ES + Direct3D).
GameObject powinien po prostu zawierać podstawowe informacje o jego wyglądzie, takie jak siatka lub może większy pakiet zawierający dane cieniowania, stan animacji i tak dalej.
Będziesz także potrzebować elastycznego potoku graficznego. Co się stanie, jeśli chcesz zamówić obiekty na podstawie ich odległości od kamery. Lub ich rodzaj materiału. Co się stanie, jeśli chcesz narysować „wybrany” obiekt innym kolorem. Co jeśli zamiast faktycznie renderować tak soo, jak wywołujesz funkcję rysowania na obiekcie, zamiast tego umieszcza ją na liście poleceń, które ma wykonać render (może być potrzebny do wątkowania). Możesz zrobić coś takiego z innym systemem, ale jest to PITA.
To, co zalecam, to zamiast bezpośredniego rysowania, wiążesz wszystkie obiekty, które chcesz, z inną strukturą danych. Wiązanie to naprawdę musi mieć tylko odwołanie do położenia obiektów i informacji o renderowaniu.
Twoje poziomy / fragmenty / obszary / mapy / huby / cały świat / cokolwiek dostanie indeks przestrzenny, zawiera obiekty i zwraca je na podstawie zapytań o współrzędne i może być prostą listą lub czymś w rodzaju Octree. Może to być także opakowanie czegoś zaimplementowanego przez silnik fizyki innej firmy jako scena fizyki. Pozwala ci robić rzeczy takie jak „Zapytaj wszystkie obiekty, które są w polu widzenia kamery z jakimś dodatkowym obszarem wokół nich”, lub dla prostszych gier, w których możesz po prostu renderować wszystko, chwytając całą listę.
Indeksy przestrzenne nie muszą zawierać rzeczywistych informacji o położeniu. Działają, przechowując obiekty w strukturach drzewnych w stosunku do położenia innych obiektów. Mogą być traktowane jako rodzaj stratnej pamięci podręcznej, która umożliwia szybkie wyszukiwanie obiektu na podstawie jego położenia. Nie ma potrzeby kopiowania rzeczywistych współrzędnych X, Y, Z. Powiedziawszy, że możesz, jeśli chcesz zachować
W rzeczywistości twoje obiekty gry nie muszą nawet zawierać własnych informacji o lokalizacji. Na przykład obiekt, który nie został umieszczony na poziomie, nie powinien mieć współrzędnych x, y, z, co nie ma sensu. Możesz to zawrzeć w specjalnym indeksie. Jeśli chcesz sprawdzić współrzędne obiektu na podstawie jego rzeczywistego odwołania, będziesz chciał powiązać obiekt z wykresem sceny (wykresy sceny służą do zwracania obiektów opartych na współrzędnych, ale powolne są zwracanie współrzędnych na podstawie obiektów) .
Po dodaniu obiektu do poziomu. Będzie wykonywać następujące czynności:
1) Utwórz strukturę lokalizacji:
Może to być również odniesienie do obiektu w silnikach fizyki innych firm. Lub może to być współrzędna przesunięcia z odniesieniem do innej lokalizacji (dla kamery śledzącej lub dołączonego obiektu lub przykładu). W przypadku polimorfizmu może to zależeć od tego, czy jest to obiekt statyczny czy dynamiczny. Zachowując tutaj odniesienie do indeksu przestrzennego, gdy współrzędne są aktualizowane, indeks przestrzenny może być również.
Jeśli martwisz się dynamicznym przydzielaniem pamięci, użyj puli pamięci.
2) Powiązanie / powiązanie między twoim obiektem, jego lokalizacją i wykresem sceny.
3) Wiązanie jest dodawane do indeksu przestrzennego wewnątrz poziomu w odpowiednim punkcie.
Kiedy przygotowujesz się do renderowania.
1) Zdobądź kamerę (będzie to po prostu inny obiekt, z wyjątkiem tego, że jego lokalizacja będzie śledzić postać gracza, a Twój renderer będzie miał do niego specjalne odniesienie, w rzeczywistości to wszystko, czego naprawdę potrzebuje).
2) Pobierz funkcję SpacialBinding kamery.
3) Pobierz indeks przestrzenny z wiązania.
4) Zapytaj obiekty, które są (prawdopodobnie) widoczne dla kamery.
5A) Musisz przetwarzać informacje wizualne. Tekstury przesłane do GPU i tak dalej. Najlepiej byłoby to zrobić z wyprzedzeniem (na przykład na poziomie obciążenia), ale być może można to zrobić w czasie wykonywania (w otwartym świecie można ładować rzeczy, gdy zbliżasz się do porcji, ale nadal powinno to być zrobione z wyprzedzeniem).
5B) Opcjonalnie zbuduj buforowane drzewo renderowania, jeśli chcesz uporządkować głębokość / materiał lub śledzić pobliskie obiekty, które mogą być widoczne później. W przeciwnym razie możesz po prostu zapytać o indeks przestrzenny za każdym razem, gdy będzie on zależał od wymagań gry / wydajności.
Twój renderer prawdopodobnie będzie potrzebował obiektu RenderBinding, który połączy obiekt, współrzędne
Następnie podczas renderowania wystarczy uruchomić listę.
Użyłem powyższych referencji, ale mogą to być inteligentne wskaźniki, surowe wskaźniki, uchwyty obiektów i tak dalej.
EDYTOWAĆ:
Co do uczynienia siebie „świadomymi” siebie. To wykrywanie kolizji. Prawdopodobnie zostanie wdrożony w Oktree. Będziesz musiał podać zwrotny w swoim głównym obiekcie. Najlepiej radzić sobie z takim silnikiem fizyki, jak Bullet. W takim przypadku wystarczy zamienić Octree na PhysicsScene i Position na link do czegoś takiego jak CollisionMesh.getPosition ().
źródło