Czy jednak rozsądne jest również tworzenie aplikacji przy użyciu architektury Component-Entity-System wspólnej w silnikach gier?
Dla mnie absolutnie. Pracuję w Visual FX i studiowałem różnorodne systemy w tej dziedzinie, ich architektury (w tym CAD / CAM), głodny SDK i wszelkich dokumentów, które dałyby mi wady i zalety pozornie nieskończonych decyzji architektonicznych, które mogą być wykonane, nawet te najbardziej subtelne, nie zawsze wywierają subtelny wpływ.
Efekty wizualne są raczej podobne do gier, ponieważ istnieje jedna centralna koncepcja „sceny” z rzutniami wyświetlającymi renderowane wyniki. Procesy pętli centralnej są często przetwarzane w sposób ciągły w tej scenie w kontekście animacji, gdzie może zachodzić fizyka, emitery cząstek spawnują cząstki, animowane i renderowane siatki, animacje ruchu itp., A ostatecznie je renderują wszystko dla użytkownika na końcu.
Inną podobną koncepcją co najmniej bardzo skomplikowanych silników gier była potrzeba aspektu „projektanta”, w którym projektanci mogliby elastycznie projektować sceny, w tym możliwość samodzielnego programowania (skrypty i węzły).
Przez lata odkryłem, że ECS najlepiej pasuje. Oczywiście to nigdy nie jest całkowicie oddzielone od podmiotowości, ale powiedziałbym, że zdecydowanie wydawało się, że daje najmniej problemów. Rozwiązało to o wiele więcej poważnych problemów, z którymi zawsze się zmagaliśmy, a w zamian dało nam tylko kilka nowych mniejszych.
Tradycyjne OOP
Bardziej tradycyjne podejścia OOP mogą być naprawdę mocne, gdy dobrze orientujesz się w wymaganiach projektowych z góry, ale nie w wymaganiach dotyczących implementacji. Niezależnie od tego, czy jest to bardziej płaskie podejście oparte na wielu interfejsach, czy bardziej zagnieżdżone hierarchiczne podejście ABC, ma on tendencję do utrwalania projektu i utrudnia zmianę, a wdrożenie jest łatwiejsze i bezpieczniejsze. Zawsze występuje potrzeba niestabilności w każdym produkcie, który przechodzi poza jedną wersję, więc podejścia OOP mają tendencję do odchylania stabilności (trudność zmiany i brak powodów zmiany) w kierunku poziomu projektu oraz niestabilności (łatwość zmiany i przyczyny zmiany) do poziomu wdrożenia.
Jednak w obliczu zmieniających się wymagań użytkowników, zarówno projektowanie, jak i implementacja mogą wymagać częstych zmian. Możesz znaleźć coś dziwnego, na przykład silną potrzebę użytkownika analogicznego stworzenia, które musi być jednocześnie rośliną i zwierzęciem, całkowicie unieważniając cały zbudowany przez ciebie model konceptualny. Normalne podejście obiektowe nie chroni cię tutaj i może czasami utrudnić tak nieoczekiwane, przełomowe zmiany. Gdy w grę wchodzą obszary krytyczne pod względem wydajności, powody zmian w projekcie są coraz większe.
Łączenie wielu granularnych interfejsów w celu utworzenia zgodnego interfejsu obiektu może bardzo pomóc w stabilizacji kodu klienta, ale nie pomaga w stabilizacji podtypów, które czasami mogą przewyższyć liczbę zależności klienta. Na przykład jeden interfejs może być używany tylko przez część systemu, ale z tysiącem różnych podtypów implementujących ten interfejs. W takim przypadku utrzymanie złożonych podtypów (złożonych, ponieważ mają tak wiele różnych obowiązków związanych z interfejsem) może stać się koszmarem, a nie kodem używającym ich przez interfejs. OOP ma tendencję do przenoszenia złożoności na poziom obiektu, podczas gdy ECS przenosi go na poziom klienta („systemów”), co może być idealne, gdy jest bardzo mało systemów, ale cała gama zgodnych „obiektów” („jednostek”).
Klasa posiada również swoje dane prywatnie, dzięki czemu może samodzielnie utrzymywać niezmienniki. Niemniej jednak istnieją „zgrubne” niezmienniki, które w rzeczywistości mogą być trudne do utrzymania, gdy obiekty oddziałują na siebie. Aby złożony system jako całość był w stanie prawidłowym, często musi brać pod uwagę złożony wykres obiektów, nawet jeśli ich indywidualne niezmienniki są odpowiednio utrzymywane. Tradycyjne podejścia w stylu OOP mogą pomóc w utrzymaniu ziarnistych niezmienników, ale w rzeczywistości mogą utrudnić utrzymanie szerokich, zgrubnych niezmienników, jeśli obiekty skupią się na malutkich aspektach systemu.
Właśnie tam takie podejścia ECS budujące klocki Lego lub warianty mogą być tak pomocne. Także z uwagi na to, że systemy są bardziej zgrubne w projektowaniu niż zwykły obiekt, łatwiej jest utrzymać tego rodzaju gruboziarniste niezmienniki w widoku z lotu ptaka na system. Wiele interakcji malutkich obiektów zamienia się w jeden duży system skupiający się na jednym szerokim zadaniu zamiast malutkich małych obiektów skupiających się na malutkich zadaniach z wykresem zależności, który obejmowałby kilometr papieru.
Jednak musiałem spojrzeć poza swoją dziedzinę, na branżę gier, aby dowiedzieć się o ECS, chociaż zawsze byłem nastawiony na dane. Co więcej, co zabawne, prawie sam przeszedłem do ECS, iterując i próbując wymyślić lepsze projekty. Nie doszedłem jednak do końca i przeoczyłem bardzo ważny szczegół, którym jest sformalizowanie części „systemowej” i zmiażdżenie komponentów aż do surowych danych.
Spróbuję przejść przez to, jak ostatecznie zdecydowałem się na ECS i jak to rozwiązało wszystkie problemy z poprzednimi iteracjami projektu. Myślę, że to pomoże dokładnie wyjaśnić, dlaczego odpowiedź tutaj może być bardzo silnym „tak”, że ECS ma potencjalnie zastosowanie daleko poza branżą gier.
Architektura Brute Force z lat 80
Pierwsza architektura, nad którą pracowałem w branży VFX, miała długą tradycję, która minęła już dekadę, odkąd dołączyłem do firmy. To było brutalne kodowanie C brutalnej siły (nie nachylenie na C, ponieważ uwielbiam C, ale sposób, w jaki został tutaj użyty, był naprawdę surowy). Miniaturowy i zbyt uproszczony kawałek przypominał takie zależności:
I to jest niezwykle uproszczony schemat jednego maleńkiego elementu systemu. Każdy z tych klientów na diagramie („Rendering”, „Fizyka”, „Ruch”) dostałby jakiś „ogólny” obiekt, przez który sprawdzałby pole typu, na przykład:
void transform(struct Object* obj, const float mat[16])
{
switch (obj->type)
{
case camera:
// cast to camera and do something with camera fields
break;
case light:
// cast to light and do something with light fields
break;
...
}
}
Oczywiście ze znacznie brzydszym i bardziej złożonym kodem niż ten. Często z tych skrzynek przełączników wywoływane byłyby dodatkowe funkcje, które rekurencyjnie wykonywałyby przełączanie raz za razem. Ten schemat i kod mogą wyglądać prawie jak ECS-lite, ale nie było wyraźnego rozróżnienia między bytem a komponentem („ czy ten obiekt jest kamerą?”, A nie „czy ten obiekt zapewnia ruch?”) I nie było sformalizowania „systemu” ( tylko kilka zagnieżdżonych funkcji rozrzuconych po całym miejscu i mieszających obowiązki). W takim przypadku prawie wszystko było skomplikowane, każda funkcja mogła potencjalnie doprowadzić do katastrofy.
Nasza procedura testowania tutaj często musiała sprawdzać rzeczy takie jak siatki oddzielone od innych rodzajów przedmiotów, nawet jeśli to samo działo się z oboma, ponieważ brutalna siła kodowania tutaj (często z dużą ilością kopiowania i wklejania) często wykonywana jest bardzo prawdopodobne, że w przeciwnym razie dokładnie ta sama logika może zawieść z jednego typu elementu na drugi. Próba rozszerzenia systemu na nowe typy przedmiotów była dość beznadziejna, mimo że istniała silnie wyrażona potrzeba użytkowników, ponieważ było to zbyt trudne, gdy tak bardzo walczyliśmy o obsługę istniejących rodzajów przedmiotów.
Niektóre zalety:
- Uhh ... chyba nie ma doświadczenia inżynierskiego? Ten system nie wymaga żadnej wiedzy na temat nawet podstawowych pojęć, takich jak polimorfizm, jest to całkowicie brutalna siła, więc sądzę, że nawet początkujący może zrozumieć część kodu, nawet jeśli profesjonalista w debugowaniu ledwo go utrzyma.
Niektóre minusy:
- Koszmar utrzymania. Nasz zespół ds. Marketingu poczuł potrzebę pochwalenia się tym, że naprawiliśmy ponad 2000 unikalnych błędów w jednym 3-letnim cyklu. Dla mnie jest to coś wstydliwego z powodu tego, że mieliśmy tak wiele błędów, a proces ten prawdopodobnie wciąż naprawił tylko około 10% wszystkich błędów, których liczba ciągle rośnie.
- O najbardziej nieelastycznym możliwym rozwiązaniu.
Architektura COM z lat 90
Większość branży VFX korzysta z tego stylu architektury z tego, co zebrałem, czytając dokumenty o ich decyzjach projektowych i przeglądając ich zestawy programistyczne.
Może nie być to COM na poziomie ABI (niektóre z tych architektur mogą mieć wtyczki napisane tylko przy użyciu tego samego kompilatora), ale ma wiele podobnych cech z zapytaniami interfejsów wykonywanymi na obiektach, aby zobaczyć, jakie interfejsy obsługują ich komponenty.
Przy takim podejściu transform
powyższa funkcja analogiczna przypominała tę postać:
void transform(Object obj, const Matrix& mat)
{
// Wrapper that performs an interface query to see if the
// object implements the IMotion interface.
MotionRef motion(obj);
// If the object supported the IMotion interface:
if (motion.valid())
{
// Transform the item through the IMotion interface.
motion->transform(mat);
...
}
}
Jest to podejście, na którym zdecydował się nowy zespół tej starej bazy kodów, aby ostatecznie zrezygnować. To była znacząca poprawa w stosunku do oryginału pod względem elastyczności i łatwości konserwacji, ale wciąż były pewne problemy, które omówię w następnym rozdziale.
Niektóre zalety:
- Dramatycznie bardziej elastyczny / rozszerzalny / łatwy w utrzymaniu niż poprzednie rozwiązanie brutalnej siły.
- Promuje silną zgodność z wieloma zasadami SOLID, czyniąc każdy interfejs całkowicie abstrakcyjnym (bezstanowy, bez implementacji, tylko czyste interfejsy).
Niektóre minusy:
- Dużo płyty kotłowej. Nasze komponenty musiały zostać opublikowane za pośrednictwem rejestru w celu utworzenia instancji obiektów, obsługiwane przez nie interfejsy wymagały zarówno dziedziczenia („implementacji” w Javie) interfejsu, jak i dostarczenia kodu wskazującego, które interfejsy były dostępne w zapytaniu.
- Promowane zduplikowane logiki w całym miejscu dzięki czystym interfejsom. Na przykład wszystkie zaimplementowane komponenty
IMotion
zawsze miałyby dokładnie ten sam stan i dokładnie taką samą implementację dla wszystkich funkcji. Aby temu zaradzić, zaczęlibyśmy centralizować klasy podstawowe i funkcje pomocnicze w całym systemie w celu zapewnienia nadmiarowej implementacji w ten sam sposób dla tego samego interfejsu i być może z wielokrotnym dziedziczeniem za maską, ale było dość bałagan pod maską, mimo że kod klienta miał to łatwe.
- Nieskuteczność: sesje vtune często pokazywały podstawową
QueryInterface
funkcję prawie zawsze wyświetlaną jako środkowy do górnego punktu dostępowego, a czasami nawet punkt pierwszy. Aby to złagodzić, robimy takie rzeczy, jak renderowanie części pamięci podręcznej bazy kodów listy obiektów, o których już wiadomo, że obsługująIRenderable
, ale to znacznie zwiększyło złożoność i koszty utrzymania. Podobnie było to trudniejsze do zmierzenia, ale zauważyliśmy pewne spowolnienia w porównaniu do kodowania w stylu C, które robiliśmy wcześniej, gdy każdy interfejs wymagał dynamicznej wysyłki. Rzeczy takie jak nieprzewidywalne oddziały i bariery optymalizacyjne są trudne do zmierzenia poza niewielkim aspektem kodu, ale użytkownicy po prostu zauważają responsywność interfejsu użytkownika i pogarszają się, porównując poprzednie i nowsze wersje oprogramowania obok siebie po stronie obszarów, w których złożoność algorytmiczna się nie zmieniła, tylko stałe.
- Nadal trudno było uzasadnić poprawność na szerszym poziomie systemu. Mimo że było to znacznie łatwiejsze niż poprzednie podejście, nadal trudno było zrozumieć złożone interakcje między obiektami w tym systemie, szczególnie w przypadku niektórych optymalizacji, które zaczęły być konieczne w stosunku do niego.
- Mieliśmy problem z poprawieniem interfejsów. Chociaż w systemie może być tylko jedno szerokie miejsce, które korzysta z interfejsu, wymagania użytkowników zmieniłyby się w zależności od wersji, a my musielibyśmy wprowadzić zmiany kaskadowe we wszystkich klasach, które implementują interfejs, aby uwzględnić nową funkcję dodaną do interfejs, np. chyba że istniała jakaś abstrakcyjna klasa bazowa, która już centralizowała logikę pod maską (niektóre z nich ujawniłyby się w środku tych kaskadowych zmian w nadziei, że nie będą powtarzały tego wielokrotnie).
Odpowiedź pragmatyczna: skład
Jedną z rzeczy, które zauważyliśmy wcześniej (a przynajmniej ja byłem), które powodowały problemy, było to, że IMotion
może być zaimplementowane przez 100 różnych klas, ale z dokładnie tą samą implementacją i powiązanym stanem. Co więcej, byłby używany tylko przez kilka systemów, takich jak rendering, ruch klatek kluczowych i fizyka.
W takim przypadku moglibyśmy mieć stosunek 3 do 1 między systemami używającymi interfejsu do interfejsu oraz stosunek 100 do 1 między podtypami implementującymi interfejs do interfejsu.
Złożoność i utrzymanie byłyby wówczas drastycznie wypaczone w stosunku do wdrożenia i utrzymania 100 podtypów, zamiast 3 zależnych systemów klienckich IMotion
. Spowodowało to przeniesienie wszystkich naszych trudności w utrzymaniu na utrzymanie tych 100 podtypów, a nie 3 miejsca korzystające z interfejsu. Aktualizowanie 3 miejsc w kodzie z kilkoma „pośrednimi łącznikami efektorowymi” (tak jak w zależnościach, ale pośrednio poprzez interfejs, a nie bezpośrednią zależnością), nic wielkiego: aktualizowanie 100 miejsc podtypów z ładunkiem „pośrednich połączeń odprowadzających” , całkiem wielka sprawa *.
* Zdaję sobie sprawę, że dziwne i błędne jest wkręcanie definicji „połączeń eferentnych” w tym sensie z punktu widzenia implementacji, po prostu nie znalazłem lepszego sposobu na opisanie złożoności utrzymania związanej z tym, gdy zarówno interfejs, jak i odpowiednie implementacje setek podtypów musi się zmienić.
Musiałem więc mocno naciskać, ale zaproponowałem, abyśmy próbowali stać się bardziej pragmatyczni i zrelaksować cały pomysł „czystego interfejsu”. Nie miało dla mnie sensu tworzenie czegoś IMotion
całkowicie abstrakcyjnego i bezpaństwowego, chyba że widzimy korzyść z posiadania szerokiej gamy implementacji. W naszym przypadku IMotion
posiadanie szerokiej gamy wdrożeń zamieniłoby się w koszmar utrzymania, ponieważ nie chcieliśmy różnorodności. Zamiast tego próbowaliśmy stworzyć implementację pojedynczego ruchu, która jest naprawdę dobra w porównaniu ze zmieniającymi się wymaganiami klienta, i często pracowaliśmy nad ideą czystego interfejsu, próbując zmusić każdego implementatora IMotion
do użycia tej samej implementacji i powiązanego stanu, abyśmy nie „ powielone cele.
Interfejsy stały się bardziej podobne do szerokiego Behaviors
skojarzonego z bytem. IMotion
stałby się po prostu Motion
„komponentem” (zmieniłem sposób, w jaki zdefiniowaliśmy „komponent” z COM, na taki, który jest bliższy zwykłej definicji elementu tworzącego „kompletny” byt).
Zamiast tego:
class IMotion
{
public:
virtual ~IMotion() {}
virtual void transform(const Matrix& mat) = 0;
...
};
Rozwinęliśmy to do czegoś takiego:
class Motion
{
public:
void transform(const Matrix& mat)
{
...
}
...
private:
Matrix transformation;
...
};
Jest to rażące naruszenie zasady inwersji zależności, aby zacząć przechodzić od abstrakcji z powrotem do konkretów, ale dla mnie taki poziom abstrakcji jest użyteczny tylko wtedy, gdy możemy przewidzieć prawdziwą potrzebę w pewnej przyszłości, ponad uzasadnioną wątpliwość, a nie wykonywanie absurdalnych scenariuszy „co jeśli” całkowicie oderwanych od doświadczenia użytkownika (co prawdopodobnie wymagałoby zmiany projektu), dla takiej elastyczności.
Zaczęliśmy ewoluować do tego projektu. QueryInterface
stał się bardziej podobny QueryBehavior
. Co więcej, stosowanie dziedziczenia w tym miejscu wydawało się bezcelowe. Zamiast tego użyliśmy kompozycji. Obiekty zamieniły się w kolekcję komponentów, których dostępność można było sprawdzać i wstrzykiwać w czasie wykonywania.
Niektóre zalety:
- W naszym przypadku było o wiele łatwiej utrzymać niż poprzedni system COM z czystym interfejsem. Nieprzewidziane niespodzianki, takie jak zmiana wymagań lub skargi związane z przepływem pracy, można łatwiej rozwiązać dzięki jednej bardzo oczywistej
Motion
implementacji, np. I nie rozproszonej w stu podtypach.
- Dał zupełnie nowy poziom elastyczności, jakiej naprawdę potrzebowaliśmy. W naszym poprzednim systemie, ponieważ dziedziczenie modeluje relację statyczną, mogliśmy tylko skutecznie definiować nowe jednostki w czasie kompilacji w C ++. Nie mogliśmy tego zrobić z poziomu języka skryptowego, np. Dzięki podejściu do komponowania moglibyśmy tworzyć w locie nowe jednostki w locie, po prostu dołączając do nich komponenty i dodając je do listy. „Istota” zamieniła się w puste płótno, na którym moglibyśmy po prostu poskładać kolaż wszystkiego, czego potrzebowaliśmy w locie, dzięki czemu odpowiednie systemy automatycznie rozpoznają i przetwarzają te istoty.
Niektóre minusy:
- Nadal mieliśmy problemy z działem wydajności i utrzymywalnością w obszarach krytycznych pod względem wydajności. Każdy system nadal chciałby buforować komponenty bytów, które zapewniły te zachowania, aby uniknąć powtarzania się w nich i sprawdzania, co jest dostępne. Każdy system wymagający wydajności zrobiłby to trochę inaczej, i był podatny na inny zestaw błędów w niepowodzeniu aktualizacji tej listy buforowanej i być może struktury danych (jeśli jakaś forma wyszukiwania była zaangażowana, jak zbieranie frustum lub raytracing) na niektórych niejasne zdarzenie zmiany sceny, np
- Wciąż było coś niezręcznego i złożonego, na co nie mogłem oprzeć palców związanych z tymi wszystkimi drobnymi ziarnistymi małymi, prostymi obiektami. Nadal zrodziło się wiele zdarzeń, które poradziły sobie z interakcjami między tymi obiektami „zachowania”, które czasami były konieczne, a rezultatem był bardzo zdecentralizowany kod. Każdy mały przedmiot można było łatwo sprawdzić pod kątem poprawności i, biorąc indywidualnie, często były całkowicie poprawne. Mimo to nadal czułem, że staramy się utrzymać ogromny ekosystem złożony z małych wiosek i próbujemy zastanowić się nad tym, co oni wszyscy indywidualnie robią i składają się na całość. Baza kodów w stylu lat 80. przypominała epickie, przeludnione megalopolis, co zdecydowanie było koszmarem konserwacyjnym,
- Utrata elastyczności z brakiem abstrakcji, ale w obszarze, w którym nigdy nie spotkaliśmy się z prawdziwą potrzebą, więc nie jest to praktyczny oszustwo (choć zdecydowanie przynajmniej teoretyczne).
- Zachowanie kompatybilności ABI zawsze było trudne, a to utrudniało wymaganie stabilnych danych, a nie tylko stabilnego interfejsu związanego z „zachowaniem”. Moglibyśmy jednak łatwo dodawać nowe zachowania i po prostu przestarzać istniejące, jeśli potrzebna była zmiana stanu, a było to prawdopodobnie łatwiejsze niż wykonywanie backflipów pod interfejsami na poziomie podtypu w celu rozwiązania problemów związanych z wersją.
Jednym ze zjawisk, które miały miejsce, było to, że ponieważ straciliśmy abstrakcję tych elementów behawioralnych, mieliśmy ich więcej. Na przykład zamiast IRenderable
komponentu abstrakcyjnego przymocowalibyśmy obiekt za pomocą betonu Mesh
lub PointSprites
komponentu. System renderowania wiedziałby, jak renderować Mesh
i PointSprites
komponenty, i znajdowałby podmioty, które dostarczają takie komponenty i rysują je. Innym razem mieliśmy różne programy renderujące SceneLabel
, które odkryliśmy, że są potrzebne z perspektywy czasu, dlatego SceneLabel
w takich przypadkach dołączamy do odpowiednich podmiotów (być może oprócz a Mesh
). Narzędzie systemu renderującego zostanie następnie zaktualizowane, aby wiedzieć, jak renderować jednostki, które je zapewniły, i to była dość łatwa zmiana.
W takim przypadku jednostka składająca się z komponentów mogłaby następnie zostać użyta jako komponent innej jednostki. W ten sposób budowalibyśmy rzeczy, łącząc klocki Lego.
ECS: Systemy i surowe składniki danych
Ten ostatni system był o tyle, o ile sam go stworzyłem, a my wciąż go dręczyliśmy COM. Wydawało się, że chciał zostać systemem składającym się z bytu, ale wtedy nie znałem go. Rozglądałem się za przykładami w stylu COM, które nasyciły moją dziedzinę, kiedy powinienem był szukać silników AAA w poszukiwaniu inspiracji architektonicznych. W końcu zacząłem to robić.
Brakowało mi kilku kluczowych pomysłów:
- Formalizacja „systemów” do przetwarzania „komponentów”.
- „Składniki” to surowe dane, a nie obiekty behawioralne złożone razem w większy obiekt.
- Podmioty są niczym więcej niż ścisłym identyfikatorem powiązanym z kolekcją komponentów.
W końcu opuściłem tę firmę i zacząłem pracować nad ECS jako indyk (wciąż pracuję nad tym, jednocześnie wyczerpując moje oszczędności), i był to zdecydowanie najłatwiejszy system do zarządzania.
Zauważyłem, że podejście ECS rozwiązało problemy, z którymi wciąż borykałem się powyżej. Co najważniejsze, czułem się, jakbyśmy zarządzali „miastami” o zdrowych rozmiarach, a nie malutkimi małymi wioskami o skomplikowanych interakcjach. Nie było tak trudne do utrzymania jak monolityczne „megalopolis”, zbyt duże w populacji, by skutecznie nim zarządzać, ale nie było tak chaotyczne jak świat pełen małych, małych wiosek wchodzących w interakcje ze sobą, gdy tylko myśli się o szlakach handlowych w między nimi powstał koszmarny wykres. ECS rozproszyło całą złożoność w kierunku dużych „systemów”, takich jak system renderowania, „miasto” zdrowej wielkości, ale nie „przeludnione megalopolis”.
Komponenty, które stają się surowymi danymi, na początku wydawały mi się naprawdę dziwne , ponieważ łamią nawet podstawowe zasady ukrywania informacji w OOP. Było to swego rodzaju wyzwanie dla jednej z największych wartości, które ceniłem OOP, a mianowicie jego zdolności do utrzymywania niezmienników, które wymagały enkapsulacji i ukrywania informacji. Zaczęło się jednak nie martwić, ponieważ szybko stało się oczywiste, co dzieje się z kilkanaście tak szerokimi systemami przekształcającymi te dane zamiast rozproszenia takiej logiki w setkach lub tysiącach podtypów wdrażających kombinację interfejsów. Staram się myśleć o tym jak w stylu OOP, z wyjątkiem sytuacji, w których systemy zapewniają funkcjonalność i implementację, która uzyskuje dostęp do danych, komponenty dostarczają dane, a jednostki dostarczają komponenty.
Wbrew intuicji stało się jeszcze łatwiejsze rozumowanie o skutkach ubocznych wywoływanych przez system, gdy tylko garstka nieporęcznych systemów przekształcała dane w szerokie przebiegi. System stał się znacznie bardziej „płaski”, moje stosy wywołań stały się płytsze niż kiedykolwiek wcześniej dla każdego wątku. Mógłbym myśleć o systemie na tym poziomie nadzorcy i nie wpadać w dziwne niespodzianki.
Podobnie, uprościło to nawet obszary krytyczne pod względem wydajności w zakresie eliminacji tych zapytań. Ponieważ idea „Systemu” stała się bardzo sformalizowana, system mógł subskrybować komponenty, którymi był zainteresowany, i po prostu otrzymać buforowaną listę podmiotów spełniających te kryteria. Każda z nich nie musiała zarządzać optymalizacją buforowania, stała się scentralizowana w jednym miejscu.
Niektóre zalety:
- Wydaje się, że po prostu rozwiązuje prawie każdy poważny problem architektoniczny, z którym się spotkałem w mojej karierze, nie czując się nigdy uwięziony w kącie projektowania, gdy spotykam się z nieoczekiwanymi potrzebami.
Niektóre minusy:
- Nadal czasami ciężko mi się to otacza i nie jest to najbardziej dojrzały ani ugruntowany paradygmat, nawet w branży gier, gdzie ludzie spierają się o to, co to znaczy i jak to robić. Zdecydowanie nie jest to coś, co mogłem zrobić z byłym zespołem, z którym pracowałem, w skład którego wchodzili członkowie głęboko przywiązani do sposobu myślenia w stylu COM lub sposobu myślenia w stylu C z lat 80. w oryginalnej bazie kodu. Czasem się mylę, to jak modelować relacje między komponentami w stylu graficznym, ale zawsze znajdowałem rozwiązanie, które nie okazało się później okropne, w którym mogłem po prostu uzależnić komponent od innego („ten ruch składnik zależy od tego drugiego jako elementu nadrzędnego, a system użyje zapamiętywania, aby uniknąć powtarzania tych samych obliczeń ruchu rekurencyjnego ”, np.)
- ABI wciąż jest trudne, ale do tej pory zaryzykowałem stwierdzenie, że jest to łatwiejsze niż podejście oparte wyłącznie na interfejsie. Jest to zmiana sposobu myślenia: stabilność danych staje się jedynym celem ABI, a nie stabilności interfejsu, i pod pewnymi względami łatwiej jest osiągnąć stabilność danych niż stabilność interfejsu (np. Nie ma pokusy, aby zmienić funkcję tylko dlatego, że potrzebuje nowego parametru. Takie rzeczy zdarzają się w gruboziarnistych implementacjach systemu, które nie psują ABI).
Czy jednak rozsądne jest również tworzenie aplikacji przy użyciu architektury Component-Entity-System wspólnej w silnikach gier?
W każdym razie powiedziałbym absolutnie „tak”, a mój osobisty przykład efektów wizualnych jest silnym kandydatem. Ale to wciąż dość podobne do potrzeb gier.
Nie zastosowałem go w bardziej odległych obszarach całkowicie oderwanych od obaw związanych z silnikami gier (efekty wizualne są dość podobne), ale wydaje mi się, że znacznie więcej obszarów jest dobrymi kandydatami na podejście ECS. Być może nawet system GUI byłby odpowiedni dla jednego, ale nadal używam tam bardziej OOP (ale bez głębokiego dziedziczenia w przeciwieństwie do Qt, np.).
Jest to szeroko niezbadane terytorium, ale wydaje mi się odpowiednie, gdy twoje byty mogą się składać z bogatej kombinacji „cech” (i dokładnie tego, jaką kombinację cech dają one podlegać zmianom) i gdzie masz garść uogólnionych systemy przetwarzające jednostki, które mają niezbędne cechy.
W takich przypadkach staje się bardzo praktyczną alternatywą dla każdego scenariusza, w którym możesz ulec pokusie użycia czegoś takiego jak wielokrotne dziedziczenie lub emulacja koncepcji (np. Mixiny) tylko w celu uzyskania setek lub więcej kombinacji w hierarchii głębokiego dziedziczenia lub setek kombinacji klas w płaskiej hierarchii implementujących określoną kombinację interfejsów, ale w których waszych systemów jest niewiele (np. kilkadziesiąt).
W takich przypadkach złożoność bazy kodowej zaczyna być bardziej proporcjonalna do liczby systemów zamiast liczby kombinacji typów, ponieważ każdy typ jest teraz tylko jednostką komponującą komponenty, które są niczym więcej niż surowymi danymi. Systemy GUI w naturalny sposób pasują do tego rodzaju specyfikacji, w których mogą mieć setki możliwych typów widgetów w połączeniu z innymi typami bazowymi lub interfejsami, ale tylko garść systemów do ich przetwarzania (układ układu, system renderowania itp.). Gdyby system GUI korzystał z ECS, prawdopodobnie łatwiej byłoby uzasadnić poprawność systemu, gdy całą funkcjonalność zapewnia garstka tych systemów zamiast setek różnych typów obiektów z odziedziczonymi interfejsami lub klasami podstawowymi. Gdyby system GUI używał ECS, widżety nie miałyby żadnej funkcji, tylko dane. Tylko garstka systemów przetwarzających jednostki widgetów miałaby funkcjonalność. Sposób, w jaki można obsłużyć nadrzędne zdarzenia dla widżetu, jest poza mną, ale na podstawie mojego dotychczasowego ograniczonego doświadczenia nie znalazłem przypadku, w którym tego rodzaju logiki nie można przenieść centralnie do danego systemu w sposób, który z perspektywy czasu otrzymałem o wiele bardziej eleganckie rozwiązanie, jakiego bym się spodziewał.
Chciałbym zobaczyć, jak działa na wielu polach, ponieważ był moim ratownikiem. Oczywiście nie jest to odpowiednie, jeśli twój projekt nie psuje się w ten sposób, od jednostek agregujących komponenty po gruboziarniste systemy przetwarzające te komponenty, ale jeśli naturalnie pasują do tego rodzaju modelu, jest to najwspanialsza rzecz, z jaką się spotkałem .