Kiedy / gdzie aktualizować komponenty

10

Zamiast moich zwykłych, ciężkich silników gier, bawię się podejściem opartym na komponentach. Mam jednak trudności z uzasadnieniem, gdzie pozwolić komponentom na wykonanie swoich zadań.

Powiedzmy, że mam prosty byt, który ma listę komponentów. Oczywiście jednostka nie wie, jakie są te elementy. Może być obecny element, który nadaje bytowi pozycję na ekranie, inny może być tam, aby narysować byt na ekranie.

Aby te komponenty działały, muszą aktualizować każdą klatkę, najłatwiej to zrobić, przechodząc przez drzewo sceny, a następnie dla każdej encji aktualizować każdy komponent. Ale niektóre komponenty mogą wymagać nieco więcej zarządzania. Na przykład komponentem, który powoduje, że jednostka jest kolidowalna, musi zarządzać coś, co może nadzorować wszystkie kolidujące elementy. Składnik, który sprawia, że ​​element jest rysowalny, potrzebuje kogoś, kto nadzoruje wszystkie inne elementy do rysowania, aby ustalić kolejność rysowania itp.

Więc moje pytanie brzmi: gdzie mam zaktualizować komponenty, jaki jest czysty sposób na przekazanie ich menedżerom?

Zastanawiałem się nad użyciem obiektu menedżera singletonów dla każdego typu komponentów, ale ma to zwykle wady związane z używaniem singletonu, sposobem na złagodzenie tego problemu jest zastosowanie wstrzykiwania zależności, ale dla tego problemu brzmi to jak przesada. Mógłbym także przejść po drzewie sceny, a następnie zebrać różne komponenty w listy, używając jakiegoś wzorca obserwatora, ale wydaje się to trochę marnotrawstwem w przypadku każdej klatki.

Roy T.
źródło
1
Czy korzystasz w jakiś sposób z systemów?
Asakeron
Systemy składowe są typowym sposobem na to. Osobiście po prostu wywołuję aktualizację na wszystkich obiektach, która wywołuje aktualizację na wszystkich komponentach, i mam kilka „specjalnych” przypadków (takich jak menedżer przestrzenny do wykrywania kolizji, który jest statyczny).
ashes999
Systemy komponentowe? Nigdy wcześniej o nich nie słyszałem. Zacznę googlować, ale chętnie przyjmę wszelkie polecane linki.
Roy T.
1
Systemy Entity to przyszłość rozwoju MMOG, który jest świetnym zasobem. I, szczerze mówiąc, nazwy tych architektur zawsze mnie mylą. Różnica w stosunku do sugerowanego podejścia polega na tym, że komponenty przechowują tylko dane, a systemy je przetwarzają. Ta odpowiedź jest również bardzo istotna.
Asakeron
1
Napisałem coś w rodzaju meandrującego posta na blogu na ten temat tutaj: gamedevrubberduck.wordpress.com/2012/12/26/...
AlexFoxGill

Odpowiedzi:

15

Proponuję zacząć od przeczytania 3 wielkich kłamstw Mike'a Actona, ponieważ naruszysz dwa z nich. Mówię poważnie, to zmieni sposób projektowania kodu: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html

Więc co naruszasz?

Kłamstwo 3 - Kod jest ważniejszy niż dane

Mówisz o iniekcji zależności, która może być przydatna w niektórych (i tylko niektórych) przypadkach, ale zawsze powinnaś zadzwonić wielkim wielkim dzwonkiem alarmowym, jeśli go użyjesz, szczególnie przy tworzeniu gier! Dlaczego? Ponieważ jest to często niepotrzebna abstrakcja. A abstrakcje w niewłaściwych miejscach są okropne. Więc masz grę. Gra ma menedżerów różnych komponentów. Wszystkie komponenty są zdefiniowane. Stwórz więc klasę gdzieś w głównym kodzie pętli gry, która „ma” menedżerów. Lubić:

private CollissionManager _collissionManager;
private BulletManager _bulletManager;

Daj mu kilka funkcji pobierających, aby uzyskać każdą klasę menedżera (getBulletManager ()). Może ta klasa sama w sobie jest singletonem lub jest osiągalna z jednego (prawdopodobnie masz gdzieś centralny singlet gry). Nie ma nic złego w dobrze określonych, zakodowanych danych i zachowaniu.

Nie twórz menedżera ManagerManager, który pozwala rejestrować menedżerów za pomocą klucza, który można odzyskać za pomocą tego klucza przez inne klasy, które chcą korzystać z menedżera. To świetny system i bardzo elastyczny, ale tutaj mowa o grze. Wiesz dokładnie, jakie systemy są w grze. Po co udawać, że nie? Ponieważ jest to system dla osób, które uważają, że kod jest ważniejszy niż dane. Powiedzą: „Kod jest elastyczny, dane go wypełniają”. Ale kod to tylko dane. System, który opisałem, jest znacznie łatwiejszy, bardziej niezawodny, łatwiejszy w utrzymaniu i dużo bardziej elastyczny (na przykład, jeśli zachowanie jednego menedżera różni się od innych menedżerów, wystarczy zmienić tylko kilka wierszy zamiast przerabiać cały system)

Kłamstwo # 2 - Kod powinien być zaprojektowany wokół modelu świata

Więc masz byt w świecie gry. Jednostka ma wiele składników określających jej zachowanie. Tworzysz więc klasę Entity z listą obiektów Component i funkcją Update (), która wywołuje funkcję Update () każdego Component. Dobrze?

Nie :) To projektowanie wokół modelu świata: masz kulę w swojej grze, więc dodajesz klasę Bullet. Następnie aktualizujesz każdy pocisk i przechodzisz do następnego. To absolutnie zabije twoją wydajność i da ci strasznie zawiłą bazę kodów ze zduplikowanym kodem wszędzie i bez logicznej struktury podobnego kodu. (Sprawdź moją odpowiedź tutaj, aby uzyskać bardziej szczegółowe wyjaśnienie, dlaczego tradycyjny projekt OO jest do bani, lub sprawdź Projekt zorientowany na dane)

Spójrzmy na sytuację bez naszego uprzedzenia OO. Chcemy, co następuje, nie mniej więcej (pamiętaj, że nie ma wymogu tworzenia klasy dla encji lub obiektu):

  • Masz grupę bytów
  • Jednostki składają się z szeregu składników, które określają zachowanie jednostki
  • Chcesz aktualizować każdy element gry w każdej klatce, najlepiej w kontrolowany sposób
  • Poza identyfikacją komponentów jako należących do siebie, sam byt nie musi nic robić. Jest to link / ID dla kilku komponentów.

I spójrzmy na sytuację. Twój komponent będzie aktualizował zachowanie każdego obiektu w grze w każdej klatce. To zdecydowanie krytyczny system twojego silnika. Wydajność jest tutaj ważna!

Jeśli znasz zarówno architekturę komputerową, jak i projektowanie zorientowane na dane, wiesz, jak osiągnąć najlepszą wydajność: ciasno upakowana pamięć i grupowanie wykonywania kodu. Jeśli wykonasz fragmenty kodu A, B i C w następujący sposób: ABCABCABC, nie uzyskasz takiej samej wydajności, jak w przypadku wykonania w następujący sposób: AAABBBCCC. Nie tylko dlatego, że pamięć podręczna instrukcji i danych będzie efektywniej wykorzystywana, ale także dlatego, że wykonując wszystkie „A” jeden po drugim, istnieje wiele miejsca na optymalizację: usunięcie duplikatu kodu, wstępne obliczenie danych używanych przez wszystkie „A” itp.

Więc jeśli chcemy zaktualizować wszystkie komponenty, nie róbmy z nich klas / obiektów z funkcją aktualizacji. Nie nazywajmy tej funkcji aktualizacji dla każdego komponentu w każdej jednostce. To rozwiązanie „ABCABCABC”. Zgrupujmy razem wszystkie identyczne aktualizacje komponentów. Następnie możemy zaktualizować wszystkie komponenty A, a następnie B, itd. Czego potrzebujemy, aby to zrobić?

Po pierwsze potrzebujemy menedżerów komponentów. Do każdego rodzaju elementu w grze potrzebujemy klasy menedżerskiej. Posiada funkcję aktualizacji, która aktualizuje wszystkie komponenty tego typu. Ma funkcję tworzenia, która doda nowy komponent tego typu oraz funkcję usuwania, która zniszczy określony komponent. Mogą istnieć inne funkcje pomocnicze do pobierania i ustawiania danych specyficznych dla tego komponentu (np .: ustaw model 3D dla komponentu modelu). Pamiętaj, że menedżer jest w pewnym sensie czarną skrzynką dla świata zewnętrznego. Nie wiemy, jak przechowywane są dane każdego komponentu. Nie wiemy, jak każdy komponent jest aktualizowany. Nie obchodzi nas to, dopóki komponenty zachowują się tak, jak powinny.

Następnie potrzebujemy bytu. Możesz zrobić z tego zajęcia, ale nie jest to konieczne. Jednostka może być niczym więcej niż unikalnym identyfikatorem całkowitym lub ciągiem mieszanym (a więc także liczbą całkowitą). Podczas tworzenia komponentu dla encji przekazujesz identyfikator jako argument do menedżera. Kiedy chcesz usunąć komponent, ponownie przekazujesz identyfikator. Dodanie nieco więcej danych do encji może być zaletą, zamiast uczynienia jej identyfikatorem, ale będą to tylko funkcje pomocnicze, ponieważ jak wymieniłem w wymaganiach, wszystkie zachowania encji są definiowane przez same komponenty. To twój silnik, więc rób to, co ma dla ciebie sens.

Potrzebujemy menedżera jednostek. Ta klasa będzie generować unikalne identyfikatory, jeśli użyjesz rozwiązania opartego tylko na ID, lub może zostać użyta do tworzenia / zarządzania obiektami Entity. Może także przechowywać listę wszystkich bytów w grze, jeśli jest to potrzebne. Entity Manager może być centralną klasą systemu komponentów, przechowując odniesienia do wszystkich menedżerów komponentów w grze i wywołując ich funkcje aktualizacji w odpowiedniej kolejności. W ten sposób wszystko, co musi zrobić pętla, to wywołanie EntityManager.update (), a cały system jest ładnie oddzielony od reszty silnika.

To widok z lotu ptaka, spójrzmy na działanie menedżerów komponentów. Oto czego potrzebujesz:

  • Utwórz dane komponentu po wywołaniu funkcji create (entityID)
  • Usuń dane komponentu po wywołaniu metody remove (entityID)
  • Zaktualizuj wszystkie (odpowiednie) dane komponentu po wywołaniu update () (tzn. Nie wszystkie komponenty muszą aktualizować każdą ramkę)

Ostatni dotyczy definiowania zachowania / logiki komponentów i zależy całkowicie od rodzaju komponentu, który piszesz. AnimationComponent zaktualizuje dane animacji na podstawie klatki, w której się znajduje. DragableComponent zaktualizuje tylko składnik przeciągany myszą. PhysicsComponent zaktualizuje dane w systemie fizyki. Ponieważ jednak aktualizujesz wszystkie komponenty tego samego typu za jednym razem, możesz dokonać optymalizacji, które nie są możliwe, gdy każdy komponent jest osobnym obiektem z funkcją aktualizacji, którą można wywołać w dowolnym momencie.

Zauważ, że wciąż nigdy nie apelowałem o utworzenie klasy XxxComponent do przechowywania danych komponentów. To zależy od Ciebie. Czy lubisz projektowanie zorientowane na dane? Następnie uporządkuj dane w osobnych tablicach dla każdej zmiennej. Czy lubisz projektowanie obiektowe? (Nie poleciłbym tego, wciąż zabije twoją wydajność w wielu miejscach). Następnie utwórz obiekt XxxComponent, który będzie przechowywał dane każdego komponentu.

Wspaniałą rzeczą w menedżerach jest enkapsulacja. Teraz enkapsulacja jest jedną z najstraszniej nadużywanych filozofii w świecie programowania. Tak należy go używać. Tylko menedżer wie, gdzie przechowywane są dane komponentu, jak działa logika komponentu. Istnieje kilka funkcji pobierania / ustawiania danych, ale to wszystko. Możesz przepisać cały menedżer i jego podstawowe klasy, a jeśli nie zmienisz publicznego interfejsu, nikt nawet tego nie zauważy. Zmieniłeś silnik fizyki? Po prostu przepisz PhysicsComponentManager i gotowe.

Jest jeszcze jedna ostatnia rzecz: komunikacja i wymiana danych między komponentami. Teraz jest to trudne i nie ma jednego uniwersalnego rozwiązania. Można utworzyć funkcje get / set w menedżerach, aby na przykład umożliwić komponentowi kolizji uzyskanie pozycji z komponentu pozycji (tj. PositionManager.getPosition (entityID)). Możesz użyć systemu zdarzeń. Możesz przechowywać niektóre wspólne dane w encji (moim zdaniem najbrzydsze rozwiązanie). Możesz użyć (często używanego) systemu przesyłania wiadomości. Lub użyj kombinacji wielu systemów! Nie mam czasu ani doświadczenia, aby wejść do każdego z tych systemów, ale google i wyszukiwarka stosów są Twoimi przyjaciółmi.

Targowisko
źródło
Uważam tę odpowiedź za bardzo interesującą. Tylko jedno pytanie (mam nadzieję, że ty lub ktoś może mi odpowiedzieć). Jak udaje ci się wyeliminować byt w systemie opartym na komponentach DOD? Nawet Artemidy używają Entity jako klasy, nie jestem pewien, czy to bardzo unik.
Wolfrevo Kcats,
1
Co masz na myśli przez to wyeliminowanie? Czy masz na myśli system encji bez klasy Entity? Powodem, dla którego Artemis ma byt, jest to, że w Artemisie klasa Entity zarządza własnymi komponentami. W zaproponowanym przeze mnie systemie klasy ComponentManager zarządzają komponentami. Zamiast więc potrzebować klasy Entity, możesz mieć unikalny identyfikator liczby całkowitej. Powiedzmy, że masz byt 254, który ma element pozycji. Gdy chcesz zmienić pozycję, możesz wywołać PositionCompMgr.setPosition (int id, Vector3 newPos), z parametrem id jako 254.
Mart
Ale jak zarządzasz identyfikatorami? Co zrobić, jeśli chcesz usunąć komponent z encji, aby przypisać go później do innej? Co jeśli chcesz usunąć encję i dodać nową? Co jeśli chcesz, aby jeden komponent był współdzielony przez co najmniej dwa podmioty? Naprawdę mnie to interesuje.
Wolfrevo Kcats
1
EntityManager może służyć do wydawania nowych identyfikatorów. Można go również wykorzystać do stworzenia kompletnych bytów na podstawie wstępnie zdefiniowanych szablonów (np. Stworzyć „EnemyNinja”, który generuje nowy identyfikator i tworzy wszystkie komponenty, które składają się na wrogiego ninja, takie jak renderowalny, kolizja, AI, może jakiś komponent do walki w zwarciu itp.). Może także mieć funkcję removeEntity, która automatycznie wywołuje wszystkie funkcje usuwania ComponentManager. ComponentManager może sprawdzić, czy zawiera dane komponentu dla danej encji, a jeśli tak, usuń te dane.
Mart
1
Przenieś komponent z jednego elementu do drugiego? Wystarczy dodać funkcję swapComponentOwner (int oldEntity, int newEntity) do każdego ComponentManager. Dane znajdują się w ComponentManager, wszystko czego potrzebujesz to funkcja zmiany właściciela, do którego należy. Każdy ComponentManager będzie miał coś w rodzaju indeksu lub mapy do przechowywania danych, które należą do którego identyfikatora encji. Wystarczy zmienić identyfikator jednostki ze starego na nowy. Nie jestem pewien, czy dzielenie się komponentami jest łatwe w systemie, który wymyśliłem, ale jak trudne może być? Zamiast jednego łącza Entity ID <-> Component Data w tabeli indeksów jest wiele.
Mart
3

Aby te komponenty działały, muszą aktualizować każdą klatkę, najłatwiej to zrobić, przechodząc przez drzewo sceny, a następnie dla każdej encji aktualizować każdy komponent.

Jest to typowe naiwne podejście do aktualizacji komponentów (i nie ma nic złego w tym, że jest naiwne, jeśli działa dla ciebie). Jeden z dużych problemów, z którymi się zetknął - działasz przez interfejs komponentu (na przykład IComponent), więc nie wiesz nic o tym, co właśnie zaktualizowałeś. Prawdopodobnie nie wiesz też nic na temat zamawiania komponentów w ramach jednostki, więc

  1. prawdopodobnie często aktualizujesz komponenty różnych typów (zasadniczo słaba lokalizacja kodu odniesienia)
  2. ten system nie nadaje się dobrze do jednoczesnych aktualizacji, ponieważ nie jesteś w stanie zidentyfikować zależności danych, a tym samym podzielić aktualizacje na lokalne grupy niepowiązanych obiektów.

Zastanawiałem się nad użyciem obiektu menedżera singletonów dla każdego typu komponentów, ale ma to zwykle wady związane z używaniem singletona, sposobem na złagodzenie tego problemu jest zastosowanie wstrzykiwania zależności, ale dla tego problemu brzmi to jak przesada.

Singleton nie jest tu tak naprawdę potrzebny, dlatego powinieneś go unikać, ponieważ ma w sobie wspomniane wady. Wstrzykiwanie zależności nie jest przesadą - sedno koncepcji polega na tym, że przekazujesz do obiektu rzeczy, których potrzebuje obiekt, najlepiej w konstruktorze. Nie potrzebujesz do tego ciężkiego frameworka DI (takiego jak Ninject ) - po prostu przekaż gdzieś dodatkowy parametr konstruktorowi.

Renderowanie jest podstawowym systemem i prawdopodobnie wspiera tworzenie i zarządzanie czasem życia renderowanych obiektów, które odpowiadają elementom wizualnym w grze (prawdopodobnie sprite'om lub modelom). Podobnie silnik fizyki prawdopodobnie ma dożywotnią kontrolę nad rzeczami reprezentującymi byty, które mogą się poruszać w symulacji fizyki (ciała sztywne). Każdy z tych odpowiednich systemów powinien posiadać, w pewnym zakresie, te obiekty i być odpowiedzialny za ich aktualizację.

Komponenty używane w systemie tworzenia elementów gry powinny być owijane wokół instancji z tych systemów niższego poziomu - twój element pozycji może po prostu owinąć sztywne ciało, twój element wizualny po prostu owinie dający się renderować duszek lub model, i tak dalej.

Następnie sam system, który jest właścicielem obiektów niższego poziomu, jest odpowiedzialny za ich aktualizację i może to zrobić zbiorczo oraz w sposób, który pozwala na wielowątkowość tej aktualizacji, jeśli to konieczne. Główna pętla gry kontroluje prostą kolejność aktualizacji tych systemów (najpierw fizyka, potem renderer lub cokolwiek innego). Jeśli masz podsystem, który nie ma żywotności ani kontroli aktualizacji nad instancjami, które rozdaje, możesz zbudować proste opakowanie do obsługi aktualizacji wszystkich komponentów istotnych dla tego systemu, a także zdecydować, gdzie umieścić to aktualizacja w stosunku do reszty aktualizacji systemu (zdarza się, jak się zdarza, z komponentami „skryptowymi”).

Takie podejście jest czasami znane jako podejście do komponentów zaburtowych , jeśli potrzebujesz więcej szczegółów.


źródło