Czy rozsądne jest budowanie aplikacji (nie gier) przy użyciu architektury element-jednostka-system?

24

Wiem, że podczas tworzenia aplikacji (natywnych lub internetowych), takich jak te w sklepie Apple AppStore lub Google Play, bardzo często stosuje się architekturę Model-View-Controller.

Czy jednak rozsądne jest również tworzenie aplikacji przy użyciu architektury Component-Entity-System wspólnej w silnikach gier?

Andrew De Andrade
źródło
1
Sprawdź architekturę Light Table: chris-granger.com/2013/01/24/the-ide-as-data
Hakan Deryal

Odpowiedzi:

39

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”).

wprowadź opis zdjęcia tutaj

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:

wprowadź opis zdjęcia tutaj

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.

wprowadź opis zdjęcia tutaj

Przy takim podejściu transformpowyż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 IMotionzawsze 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ą QueryInterfacefunkcję 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).

wprowadź opis zdjęcia tutaj

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 IMotionmoż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ś IMotioncałkowicie abstrakcyjnego i bezpaństwowego, chyba że widzimy korzyść z posiadania szerokiej gamy implementacji. W naszym przypadku IMotionposiadanie 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 IMotiondo użycia tej samej implementacji i powiązanego stanu, abyśmy nie „ powielone cele.

Interfejsy stały się bardziej podobne do szerokiego Behaviorsskojarzonego z bytem. IMotionstał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. QueryInterfacestał 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.

wprowadź opis zdjęcia tutaj

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 Motionimplementacji, 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 IRenderablekomponentu abstrakcyjnego przymocowalibyśmy obiekt za pomocą betonu Meshlub PointSpriteskomponentu. System renderowania wiedziałby, jak renderować Meshi PointSpriteskomponenty, 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 SceneLabelw 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:

  1. Formalizacja „systemów” do przetwarzania „komponentów”.
  2. „Składniki” to surowe dane, a nie obiekty behawioralne złożone razem w większy obiekt.
  3. 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).

wprowadź opis zdjęcia tutaj

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 .

Thomas Owens
źródło
1) Co zrobił Twój przykładowy program VFX z perspektywy użytkownika? 2) Nad jakim projektem ECS obecnie pracujesz? ♥ Dziękujemy za napisanie tego! ♥
szczenię
1
Bardzo dokładne wyjaśnienie - dziękuję. Wydaje mi się, że dochodzę do wielu takich samych wniosków co do tego, jak ECS ma zastosowanie poza grami; w moim przypadku szczególnie złożone GUI. Na początku wydaje się to naprawdę dziwne, dlatego w przeciwieństwie do tego, co zwykle się robi (hierarchie głębokiego dziedziczenia są szczególnie widoczne w ramach interfejsu użytkownika), ale pocieszające jest obserwowanie innych, którzy uważają to podejście za bardziej skuteczne.
Danny Jarosławski
1
Dziękuję za ten wspaniały ... artykuł! W przypadku GUI opartego na komponentach poleciłbym przejrzeć UGUI Unity3d. Jest niewiarygodnie elastyczny i rozszerzalny w porównaniu do tych opartych na dziedzictwie, takich jak CocoaTouch.
Ivan Mir,
16

Architektura Component-Entity-System dla silników gier działa w grach ze względu na charakter oprogramowania do gier oraz jego unikalne cechy i wymagania jakościowe. Na przykład byty zapewniają jednolite środki adresowania i pracy z rzeczami w grze, które mogą być drastycznie różne pod względem celu i zastosowania, ale muszą być renderowane, aktualizowane lub serializowane / deserializowane przez system w jednolity sposób. Poprzez włączenie modelu komponentu do tej architektury, pozwalasz im zachować prostą strukturę rdzenia, a jednocześnie dodawać więcej funkcji i funkcjonalności w razie potrzeby, z niskim sprzężeniem kodu. Istnieje wiele różnych systemów oprogramowania, które mogłyby skorzystać z właściwości tego projektu, takich jak aplikacje CAD, kodeki A / V,

TL; DR - Wzorce projektowe działają dobrze tylko wtedy, gdy dziedzina problemowa jest wystarczająco dostosowana do funkcji i wad, które narzucają projektowi.

Strzelba Ninja
źródło
8

Jeśli domena problemowa jest do tego dobrze dostosowana, z pewnością.

Moja obecna praca dotyczy aplikacji, która musi obsługiwać różne funkcje w zależności od wielu czynników uruchomieniowych. Korzystanie z encji opartych na komponentach w celu oddzielenia wszystkich tych możliwości i umożliwienia rozszerzenia i testowania w izolacji było dla nas idylliczne.

edycja: Moja praca polega na zapewnieniu łączności z zastrzeżonym sprzętem (w języku C #). W zależności od tego, jaki jest rozmiar urządzenia, jakie oprogramowanie jest na nim zainstalowane, jaki poziom usług zakupił klient itp., Musimy zapewnić różne poziomy funkcjonalności urządzenia. Nawet niektóre funkcje mające ten sam interfejs mają różne implementacje w zależności od wersji urządzenia.

Poprzednie bazy kodu miały bardzo szerokie interfejsy, z których wiele nie zostało zaimplementowanych. Niektóre miały wiele cienkich interfejsów, które następnie zostały statycznie skomponowane w jedną klasę. Niektóre po prostu używały łańcuchów -> słowników łańcuchowych do modelowania. (mamy wiele działów, którzy wszyscy myślą, że mogą to zrobić lepiej)

Wszystkie mają swoje wady. Szerokie interfejsy to półtorej bólu, aby skutecznie kpić / testować. Dodanie nowych funkcji oznacza zmianę interfejsu publicznego (i wszystkich istniejących implementacji). Wiele cienkich interfejsów prowadziło do bardzo brzydkiego kodu, ale odkąd skończyliśmy na przekazywaniu dużego, grubego obiektu testowego wciąż cierpiał. Ponadto cienkie interfejsy nie radziły sobie dobrze ze swoimi zależnościami. Słowniki łańcuchowe mają typowe problemy z analizą i istnieniem, a także dziury w wydajności, czytelności i łatwości konserwacji.

To, czego używamy teraz, to bardzo wąska jednostka, której komponenty zostały odkryte i skomponowane na podstawie informacji o środowisku wykonawczym. Zależności są deklarowane i automatycznie rozwiązywane przez szkielet podstawowego komponentu. Same komponenty mogą być testowane w izolacji, ponieważ działają bezpośrednio z ich zależnościami, a problemy z brakującymi zależnościami są wykrywane wcześnie - w jednym miejscu, a nie przy pierwszym użyciu zależności. Można dodawać nowe (lub testowe) komponenty i nie ma to wpływu na istniejący kod. Konsumenci proszą jednostkę o interfejs do komponentu, więc możemy swobodnie przekręcać różne implementacje (i sposób, w jaki implementacje są mapowane na dane środowiska wykonawczego) ze względną swobodą.

W takiej sytuacji, w której skład obiektu i jego interfejsów może zawierać (bardzo zróżnicowany) podzbiór typowych komponentów, działa on bardzo dobrze.

Telastyn
źródło
1
Zakładając, że masz taką możliwość, czy możesz podać więcej szczegółów na temat bieżącej pracy? Jestem ciekawy, w jaki sposób CES był idylliczny dla tego, co budujesz.
Andrew De Andrade,
Czy jest jakiś artykuł, artykuł lub blog na temat twoich wrażeń? też chciałbym mieć więcej technicznych szczegółów na ten temat :)
user1778770
@ user1778770 - niedostępne publicznie, nie. Jakie masz pytania?
Telastyn
Cóż, zacznijmy od czegoś prostego, czy twoja koncepcja obejmuje cały stos aplikacji (np. Od biznesu do frontendu)? czy tylko pojedyncza warstwa pojedynczego przypadku użycia?
user1778770,
@ user1778770 - w mojej implementacji byty / komponenty istnieją w jednej warstwie. Różne byty mogą istnieć na różnych warstwach, ale często nie są to 1: 1 (w przeciwnym razie warstwy nie dają żadnych korzyści).
Telastyn