Projektuję silnik gry do odgórnej wieloosobowej strzelanki 2D, którą chcę w rozsądny sposób wykorzystać do innych strzelanek z góry. W tej chwili myślę o tym, jak należy zaprojektować coś w tym systemie bytu. Najpierw pomyślałem o tym:
Mam klasę o nazwie EntityManager. Powinien zaimplementować metodę o nazwie Update i inną o nazwie Draw. Powodem oddzielenia logiki od renderowania jest to, że wtedy mogę pominąć metodę Draw, jeśli działam jako samodzielny serwer.
EntityManager posiada listę obiektów typu BaseEntity. Każda encja posiada listę komponentów, takich jak EntityModel (reprezentowalna postać encji), EntityNetworkInterface i EntityPhysicalBody.
EntityManager jest także właścicielem listy menedżerów komponentów, takich jak EntityRenderManager, EntityNetworkManager i EntityPhysicsManager. Każdy menedżer komponentów przechowuje odniesienia do komponentów encji. Istnieją różne powody, aby przenieść ten kod z własnej klasy jednostki i zamiast tego zrobić to zbiorowo. Na przykład do gry używam zewnętrznej biblioteki fizyki Box2D. W Box2D najpierw dodajesz ciała i kształty do świata (w tym przypadku należącego do EntityPhysicsManager) i dodajesz wywołania zwrotne kolizji (które byłyby wysyłane do samego obiektu encji w moim systemie). Następnie uruchamiasz funkcję, która symuluje wszystko w systemie. Trudno mi znaleźć lepsze rozwiązanie, niż robienie tego w takim zewnętrznym menedżerze komponentów.
Tworzenie encji odbywa się w następujący sposób: EntityManager implementuje metodę RegisterEntity (entityClass, fabryka), która rejestruje sposób tworzenia encji tej klasy. Implementuje również metodę CreateEntity (entityClass), która zwróci obiekt typu BaseEntity.
Teraz pojawia się mój problem: w jaki sposób odniesienie do komponentu byłoby zarejestrowane dla menedżerów komponentów? Nie mam pojęcia, jak odsyłam do menedżerów komponentów z fabryki / zamknięcia.
źródło
Odpowiedzi:
Systemy powinny przechowywać parę kluczowych wartości Entity to Component w jakimś rodzaju mapy, obiektu słownikowego lub tablicy asocjacyjnej (w zależności od używanego języka). Ponadto, kiedy tworzysz swój Obiekt Entity, nie martwię się o przechowywanie go w menedżerze, chyba że będziesz w stanie wyrejestrować go z dowolnego Systemu. Jednostka jest złożona ze składników, ale nie powinna obsługiwać żadnej z aktualizacji składników. Powinno to być obsługiwane przez systemy. Zamiast tego traktuj swoją jednostkę jako klucz, który jest mapowany na wszystkie komponenty zawarte w systemach, a także jako centrum komunikacyjne dla tych komponentów, aby ze sobą rozmawiać.
Wielką częścią modeli Entity-Component-System jest to, że można dość łatwo wdrożyć możliwość przekazywania komunikatów z jednego komponentu do reszty komponentów encji. Pozwala to komponentowi na rozmowę z innym komponentem bez faktycznej wiedzy o tym, kim jest ten komponent i jak obsługiwać komponent, który zmienia. Zamiast tego przekazuje komunikat i pozwala komponentowi się zmienić (jeśli istnieje)
Na przykład, System Pozycji nie miałby w nim dużo kodu, jedynie śledziłby Obiekty Istoty odwzorowane na ich Komponenty Pozycji. Ale gdy pozycja się zmienia, mogą wysłać wiadomość do zaangażowanej Jednostki, która z kolei jest przekazywana wszystkim komponentom tej jednostki. Z jakiejkolwiek przyczyny zmienia się pozycja? System pozycji wysyła Entity wiadomość z informacją, że pozycja uległa zmianie, a gdzieś element renderujący obraz tego podmiotu otrzymuje ten komunikat i aktualizuje, gdzie się następnie narysuje.
I odwrotnie, system fizyki musi wiedzieć, co robią wszystkie jego obiekty; Musi być w stanie zobaczyć wszystkie obiekty świata w celu przetestowania kolizji. Kiedy dochodzi do kolizji, aktualizuje komponent kierunku encji, wysyłając do encji coś w rodzaju „komunikatu zmiany kierunku” zamiast odwoływać się bezpośrednio do komponentu encji. To uniezależnia menedżera od konieczności zmiany kierunków za pomocą komunikatu zamiast polegania na określonym elemencie (którego może wcale nie być, w takim przypadku wiadomość po prostu głucha zamiast jakiegoś błędu) występujące, ponieważ oczekiwany obiekt był nieobecny).
Zauważysz ogromną przewagę, ponieważ wspomniałeś, że masz interfejs sieciowy. Komponent sieciowy nasłuchi wszystkich nadchodzących wiadomości, o których wszyscy inni powinni wiedzieć. Uwielbia plotki. Następnie, gdy system sieciowy się aktualizuje, komponenty sieciowe wysyłają te wiadomości do innych systemów sieciowych na innych komputerach klienckich, które następnie wysyłają te wiadomości do wszystkich innych komponentów w celu aktualizacji pozycji odtwarzacza itp. Może być potrzebna specjalna logika, aby tylko niektóre podmioty mogły wysyłać wiadomości przez sieć, ale to jest piękno Systemu, możesz po prostu kontrolować tę logikę, rejestrując odpowiednie rzeczy.
W skrócie:
Podmiot to kompozycja składników, które mogą odbierać wiadomości. Jednostka może odbierać wiadomości, delegując je do wszystkich swoich składników, aby je zaktualizować. (Wiadomość ze zmienioną pozycją, Kierunek zmiany prędkości itp.) To jak centralna skrzynka pocztowa, w której wszystkie elementy mogą słyszeć od siebie zamiast rozmawiać bezpośrednio ze sobą.
Komponent to niewielka część encji, która przechowuje pewien stan encji. Są one w stanie analizować niektóre wiadomości i wyrzucać inne. Na przykład „komponent kierunku” będzie dbał tylko o „komunikaty zmiany kierunku”, ale nie „komunikaty zmiany pozycji”. Składniki aktualizują swój stan na podstawie komunikatów, a następnie aktualizują stany innych składników, wysyłając wiadomości ze swojego systemu.
System zarządza wszystkimi komponentami określonego typu i jest odpowiedzialny za aktualizację wymienionych komponentów w każdej ramce, a także za wysyłanie wiadomości z zarządzanych przez nich komponentów do encji, do których należą komponenty
Systemy mogą być w stanie równolegle aktualizować wszystkie swoje komponenty i przechowywać wszystkie wiadomości w miarę ich przemieszczania. Następnie, po zakończeniu wykonywania wszystkich metod aktualizacji systemów, należy poprosić każdy system o wysłanie wiadomości w określonej kolejności. Najpierw kontrole, następnie fizyka, następnie kierunek, pozycja, rendering itp. To ma znaczenie, w jakiej kolejności są wysyłane, ponieważ zmiana kierunku fizyki zawsze powinna wyważyć zmianę kierunku opartą na kontroli.
Mam nadzieję że to pomoże. To piekielnie wzorzec projektowy, ale jest absurdalnie potężny, jeśli zostanie wykonany poprawnie.
źródło
Używam podobnego systemu w silniku, a sposób, w jaki to zrobiłem, polega na tym, że każda jednostka zawiera listę komponentów. Z EntityManager mogę zapytać każdą z Encji i zobaczyć, które zawierają dany Komponent. Przykład:
Oczywiście nie jest to dokładny kod (w rzeczywistości potrzebujesz funkcji szablonu do sprawdzania różnych typów komponentów, zamiast używania
typeof
), ale koncepcja istnieje. Następnie możesz wziąć te wyniki, odnieść się do poszukiwanego komponentu i zarejestrować go w fabryce. Zapobiega to bezpośredniemu sprzężeniu między komponentami i ich menedżerami.źródło
typedef long long int Entity
; Komponent to rekord (może być zaimplementowany jako klasa obiektu lub po prostu astruct
), który zawiera odwołanie do encji, do której jest dołączony; a System byłby metodą lub zbiorem metod. Model ECS nie jest zbyt kompatybilny z modelem OOP, chociaż komponent może być (głównie) obiektem tylko danych, a system to obiekt typu singleton zawierający tylko kod, którego stan żyje w komponentach ... chociaż systemy „hybrydowe” są bardziej powszechne niż te „czyste”, tracą wiele wrodzonych korzyści.1) Do metody Factory należy przekazać odwołanie do EntityManager, który ją wywołał (użyję C # jako przykładu):
2) Niech CreateEntity otrzyma również identyfikator (np. Ciąg, liczbę całkowitą, zależy od Ciebie) oprócz klasy / typu encji i automatycznie zarejestruje utworzoną encję w Słowniku, używając tego identyfikatora jako klucza:
3) Dodaj moduł Getter do EntityManager, aby uzyskać dowolną jednostkę według identyfikatora:
I to wszystko, czego potrzebujesz, aby odwołać się do dowolnego ComponentManager z poziomu metody Factory. Na przykład:
Oprócz Id możesz także użyć jakiejś właściwości Type (niestandardowe wyliczenie lub po prostu polegać na systemie typów języka) i utworzyć moduł pobierający, który zwraca wszystkie wartości BaseEntities określonego typu.
źródło
typedef unsigned long long int EntityID;
:; Idealne jest to, że każdy system może żyć na osobnym procesorze lub hoście i wymaga tylko pobierania komponentów, które są odpowiednie dla / aktywnego w tym systemie. W przypadku obiektu Entity może być konieczne utworzenie instancji tego samego obiektu Entity na każdym hoście, co utrudnia skalowanie. Model czysto byt-komponent-system dzieli przetwarzanie na węzły (procesy, procesory lub hosty) według systemu, a nie według jednostki, zazwyczaj.