To jest trudne. Spróbuję rozwiązać niektóre pytania w oparciu o moje szczególne doświadczenia (YMMV):
Komponenty muszą mieć dostęp do danych innych komponentów. Np. Metoda rysowania komponentu renderującego musi mieć dostęp do pozycji komponentu transformacji. To tworzy zależności w kodzie.
Nie lekceważ tutaj ilości i złożoności (a nie stopnia) sprzężenia / zależności. Możesz patrzeć na różnicę między tym (a ten schemat jest już absurdalnie uproszczony do poziomów podobnych do zabawek, a prawdziwy przykład miałby między nimi interfejsy do poluzowania sprzężenia):
... i to:
... albo to:
Składniki mogą być polimorficzne, co dodatkowo wprowadza pewną złożoność. Np. Może istnieć komponent renderowania sprite, który przesłania wirtualną metodę rysowania komponentu renderowania.
Więc? Analogiczny (lub dosłowny) odpowiednik vtable i wirtualnej wysyłki można wywoływać za pośrednictwem systemu, a nie obiektu ukrywającego jego stan / dane. Polimorfizm jest nadal bardzo praktyczny i wykonalny dzięki „czystej” implementacji ECS, gdy analogiczny wskaźnik (wskaźniki) funkcji lub funkcji zmienia się w „dane”, które system może wywołać.
Ponieważ zachowanie polimorficzne (np. Do renderowania) musi być gdzieś zaimplementowane, jest ono po prostu zlecane zewnętrznym systemom. (np. system renderowania duszków tworzy węzeł renderowania duszków, który dziedziczy węzeł renderowania i dodaje go do silnika renderowania)
Więc? Mam nadzieję, że nie idzie to w parze z sarkazmem (nie moim zamiarem, chociaż często mnie o to oskarżano, ale chciałbym móc lepiej komunikować emocje za pomocą tekstu), ale zachowanie „polimorficzne” outsourcingu w tym przypadku niekoniecznie wiąże się z dodatkowym koszt wydajności.
Komunikacja między systemami może być trudna do uniknięcia. Np. System kolizji może potrzebować obwiedni, która jest obliczana na podstawie dowolnego betonowego elementu renderującego.
Ten przykład wydaje mi się szczególnie dziwny. Nie wiem, dlaczego mechanizm renderujący wyprowadza dane z powrotem na scenę (w tym kontekście uważam, że renderery są tylko do odczytu), lub żeby mechanizm renderujący wymyślił AABB zamiast jakiegoś innego systemu, który mógłby to zrobić zarówno dla mechanizmu renderującego, jak i kolizja / fizyka (może się tu rozłączać nazwa „komponentu renderowania” tutaj). Jednak nie chcę się zbytnio rozłączać na tym przykładzie, ponieważ zdaję sobie sprawę, że nie o to ci chodzi. Mimo to komunikacja między systemami (nawet w pośredniej formie odczytu / zapisu do centralnej bazy danych ECS z systemami zależnymi raczej bezpośrednio od transformacji dokonanych przez innych) nie powinna być częsta, jeśli to w ogóle konieczne. Że'
Może to prowadzić do problemów, jeśli kolejność wywoływania funkcji aktualizacji systemu nie jest określona.
To absolutnie powinno być zdefiniowane. ECS nie jest ostatecznym rozwiązaniem do zmiany kolejności przetwarzania oceny systemu dla każdego możliwego systemu w bazie kodu i uzyskania dokładnie tego samego rodzaju wyników dla użytkownika końcowego zajmującego się ramkami i FPS. Jest to jedna z rzeczy, przy projektowaniu ECS, którą przynajmniej zdecydowanie sugeruję, że należy się spodziewać nieco z góry (choć z dużą ilością wybaczającego oddechu, aby zmienić zdanie później, pod warunkiem, że nie zmienia to najbardziej krytycznych aspektów porządkowania wywołanie / ocena systemu).
Ponowne obliczenie całej mapy tilemap każdej klatki jest jednak drogie. Dlatego potrzebna byłaby lista do śledzenia wszystkich wprowadzonych zmian, a następnie ich aktualizacji w systemie. W sposób OOP może to być zawarte w elemencie mapy kafelkowej. Np. Metoda SetTile () aktualizowałaby tablicę wierzchołków przy każdym jej wywołaniu.
Nie do końca to zrozumiałem, poza tym, że dotyczy to danych. I nie ma żadnych pułapek dotyczących reprezentowania i przechowywania danych w ECS, w tym zapamiętywania, w celu uniknięcia takich pułapek wydajnościowych (największe z ECS mają tendencję do odwoływania się do systemów takich jak zapytania o dostępne wystąpienia poszczególnych typów komponentów, które są jednym z najtrudniejsze aspekty optymalizacji uogólnionego ECS). Fakt, że logika i dane są rozdzielone w „czystym” ECS, nie oznacza, że musisz nagle ponownie obliczyć rzeczy, które w innym przypadku byłyby buforowane / zapamiętane w reprezentacji OOP. To kwestia dyskusyjna / nieistotna, chyba że pochyliłem się nad czymś bardzo ważnym.
Dzięki „czystemu” ECS możesz nadal przechowywać te dane w elemencie mapy kafelków. Jedyna istotna różnica polega na tym, że logika aktualizacji tej tablicy wierzchołków przenosi się gdzieś do systemu.
Możesz nawet polegać na ECS, aby uprościć unieważnianie i usuwanie tej pamięci podręcznej z encji, jeśli utworzysz oddzielny komponent, taki jak TileMapCache
. W tym momencie, gdy pamięć podręczna jest pożądana, ale niedostępna w encji ze TileMap
składnikiem, możesz ją obliczyć i dodać. Kiedy jest unieważniony lub nie jest już potrzebny, możesz go usunąć za pomocą ECS bez konieczności pisania dodatkowego kodu specjalnie dla takiego unieważnienia i usunięcia.
Zależności między komponentami wciąż istnieją, chociaż są ukryte w systemach
Nie ma zależności między komponentami w „czystym” powtórzeniu (nie sądzę, że słuszne jest twierdzenie, że zależności są ukrywane przez systemy). Dane nie zależą od danych, że tak powiem. Logika zależy od logiki. A „czysty” ECS ma tendencję do promowania logiki, która ma być napisana w taki sposób, aby zależeć od absolutnie minimalnego podzbioru danych i logiki (często żadnej), której system wymaga do działania, w przeciwieństwie do wielu alternatyw, które często zachęcają w zależności od znacznie więcej funkcjonalności niż jest to wymagane w przypadku rzeczywistego zadania. Jeśli korzystasz z czystego prawa ECS, jedną z pierwszych rzeczy, które powinieneś docenić, są korzyści z oddzielania płatności, jednocześnie kwestionując wszystko, co nauczyłeś się doceniać w OOP na temat enkapsulacji, a zwłaszcza ukrywania informacji.
Odsprzęgając, mam na myśli w szczególności, jak mało informacji potrzebuje twój system. System ruchu nawet nie musi wiedzieć o coś znacznie bardziej skomplikowane jak Particle
lub Character
(deweloper systemu nie musi nawet wiedzieć takie pomysły podmiot w ogóle istnieją w systemie). Musi tylko wiedzieć o absolutnie minimalnych danych, takich jak element pozycji, który może być tak prosty, jak kilka liczb zmiennoprzecinkowych w strukturze. To jeszcze mniej informacji i mniej zależności zewnętrznych niż to, co zwykły mieć ze sobą czysty interfejs IMotion
. Wynika to przede wszystkim z tej minimalnej wiedzy, którą każdy system wymaga do pracy, co sprawia, że ECS często tak wybacza sobie radzenie sobie z bardzo nieoczekiwanymi zmianami projektowymi z perspektywy czasu, bez narażania się na kaskadowe uszkodzenia interfejsu w dowolnym miejscu.
Podejście „nieczyste”, które sugerujesz, nieco zmniejsza tę korzyść, ponieważ teraz twoja logika nie jest zlokalizowana ściśle w systemach, w których zmiany nie powodują kaskadowych awarii. Logika byłaby teraz do pewnego stopnia scentralizowana w komponentach, do których ma dostęp wiele systemów, które muszą teraz spełniać wymagania interfejsów wszystkich różnych systemów, które mogłyby z niej korzystać, a teraz jest tak, jakby każdy system musiał mieć wiedzę (zależną od) więcej informacje, które są ściśle potrzebne do pracy z tym komponentem.
Zależności od danych
Jedną z rzeczy, które budzą kontrowersje w ECS, jest to, że zwykle zastępuje on zależności od abstrakcyjnych interfejsów tylko surowymi danymi i jest to ogólnie uważane za mniej pożądaną i ściślejszą formę łączenia. Ale w takich domenach, jak gry, w których ECS może być bardzo korzystne, często łatwiej jest zaprojektować reprezentację danych z góry i utrzymać ją na stałym poziomie, niż zaprojektować, co można zrobić z tymi danymi na pewnym centralnym poziomie systemu. Jest to coś, co boleśnie zaobserwowałem nawet wśród doświadczonych weteranów w bazach kodowych, które wykorzystują bardziej czyste podejście oparte na interfejsie COM z takimi rzeczami IMotion
.
Programiści wciąż szukali powodów, aby dodawać, usuwać lub zmieniać funkcje tego centralnego interfejsu, a każda zmiana była koszmarna i kosztowna, ponieważ miałaby tendencję do psucia każdej pojedynczej klasy, która zaimplementowała się IMotion
wraz z każdym odtąd miejscem w używanym systemie IMotion
. Tymczasem przez cały czas, z tak wieloma bolesnymi i kaskadowymi zmianami, wszystkie zaimplementowane obiekty IMotion
po prostu przechowywały macierz pływaków 4x4, a cały interfejs był po prostu zainteresowany tym, jak przekształcić i uzyskać dostęp do tych pływaków; reprezentacja danych była stabilna od samego początku i można było uniknąć wielu problemów, gdyby ten scentralizowany interfejs, tak podatny na zmiany przy nieprzewidzianych potrzebach projektowych, nawet nie istniał.
To wszystko może brzmieć niemal tak obrzydliwie jak zmienne globalne, ale natura organizacji ECS tych danych w komponenty pobierane jawnie według typów przez systemy sprawia, że tak jest, podczas gdy kompilatory nie mogą wymuszać ukrywania informacji, miejsc, do których dostęp i mutowanie dane są ogólnie bardzo wyraźne i wystarczająco oczywiste, aby nadal skutecznie utrzymywać niezmienniki i przewidywać, jakie transformacje i skutki uboczne następują z jednego systemu do drugiego (w rzeczywistości w sposób, który może być prawdopodobnie prostszy i bardziej przewidywalny niż OOP w niektórych domenach, biorąc pod uwagę, w jaki sposób system zamienia się w płaski rurociąg).
Na koniec chcę zadać pytanie, jak poradziłbym sobie z animacją w czystym ECS. Obecnie zdefiniowałem animację jako funktor, który manipuluje bytem w oparciu o pewien postęp od 0 do 1. Komponent animacji ma listę animatorów, która ma listę animacji. W swojej funkcji aktualizacji stosuje następnie animacje, które są aktualnie aktywne dla encji.
Wszyscy tu jesteśmy pragmatykami. Nawet w gamedev prawdopodobnie otrzymasz sprzeczne pomysły / odpowiedzi. Nawet najczystszy ECS to stosunkowo nowe zjawisko, pionierskie terytorium, dla którego ludzie niekoniecznie sformułowali najsilniejsze opinie na temat skórowania kotów. Moja reakcja brzucha to system animacji, który zwiększa tego rodzaju postęp animacji w komponentach animowanych do wyświetlenia w systemie renderowania, ale ignoruje to tak wiele niuansów dla konkretnej aplikacji i kontekstu.
Dzięki ECS nie jest to srebrna kula i wciąż mam tendencję do wchodzenia i dodawania nowych systemów, usuwania niektórych, dodawania nowych komponentów, zmiany istniejącego systemu, aby wybrać ten nowy typ komponentu itp. Nie dostaję wszystko w porządku za pierwszym razem. Ale różnica w moim przypadku polega na tym, że nie zmieniam niczego centralnie, kiedy nie przewiduję z góry pewnych potrzeb projektowych. Nie dostaję efektu falowania kaskadowych awarii, które wymagają ode mnie wszędzie i zmieniania tak dużej ilości kodu, aby zaspokoić nowe potrzeby, które się pojawiają, i to jest dość oszczędność czasu. Ułatwia mi to również mózg, ponieważ kiedy siedzę z konkretnym systemem, nie muszę wiedzieć / pamiętać tyle o niczym innym niż odpowiednie komponenty (które są tylko danymi), aby nad nim pracować.