Wprowadzenie
Systemy encja-komponent są zorientowaną obiektowo techniką architektoniczną.
Nie ma uniwersalnego konsensusu co do tego, co oznacza ten termin, tak samo jak programowanie obiektowe. Oczywiste jest jednak, że systemy encja-komponent są specjalnie przeznaczone jako architektoniczna alternatywa dla dziedziczenia . Hierarchie dziedziczenia są naturalne do wyrażania tego, co obiekt jest , ale w niektórych rodzajów oprogramowania (takich jak gry), czy raczej wyrazić to, co obiekt robi .
Jest to inny model obiektowy niż „klasy i dziedziczenie”, do którego najprawdopodobniej jesteś przyzwyczajony do pracy w C ++ lub Javie. Jednostki są tak wyraziste jak klasy, podobnie jak prototypy jak w JavaScript lub Self - wszystkie te systemy mogą być zaimplementowane względem siebie.
Przykłady
Powiedzmy Powiedzmy, że Player
jest to jednostka z Position
, Velocity
i KeyboardControlled
komponentów, które wykonują oczywistych rzeczy.
entity Player:
Position
Velocity
KeyboardControlled
Wiemy, że Position
musi mieć wpływ Velocity
, a Velocity
przez KeyboardControlled
. Pytanie brzmi, w jaki sposób chcielibyśmy modelować te efekty.
Podmioty, komponenty i systemy
Załóżmy, że składniki nie mają ze sobą żadnych odniesień; system zewnętrzny Physics
przechodzi przez wszystkie Velocity
komponenty i aktualizuje Position
odpowiedni byt; Input
System przemierza wszystkie KeyboardControlled
komponenty i aktualizuje Velocity
.
Player
+--------------------+
| Position | \
| | Physics
/ | Velocity | /
Input | |
\ | KeyboardControlled |
+--------------------+
Spełnia to kryteria:
Systemy są teraz odpowiedzialne za obsługę zdarzeń i wprowadzanie zachowania opisanego przez komponenty. Są również odpowiedzialne za obsługę interakcji między jednostkami, takich jak kolizje.
Podmioty i komponenty
Załóżmy jednak, że składniki zrobić mają odniesienia do siebie. Teraz istota jest po prostu konstruktorem, który tworzy niektóre komponenty, łączy je ze sobą i zarządza ich żywotnością:
class Player:
construct():
this.p = Position()
this.v = Velocity(this.p)
this.c = KeyboardControlled(this.v)
Jednostka może teraz wysyłać dane wejściowe i aktualizować zdarzenia bezpośrednio do swoich komponentów. Velocity
odpowiadałby na aktualizacje i KeyboardControlled
odpowiadałby na dane wejściowe. To nadal spełnia nasze kryteria:
Istota jest „głupim” pojemnikiem, który przekazuje zdarzenia tylko do komponentów.
Każdy składnik ma swoje własne zachowanie.
W tym przypadku interakcje komponentów są jawne, a nie narzucane z zewnątrz przez system. Dane opisujące zachowanie (jaka jest prędkość?) I kod, który je wprowadza (czym jest prędkość?) Są sprzężone, ale w naturalny sposób. Dane mogą być przeglądane jako parametry zachowania. Niektóre elementy w ogóle nie działają - Position
jest to zachowanie przebywania w miejscu .
Interakcje mogą być obsługiwane na poziomie bytu („gdy Player
zderza się z Enemy
...”) lub na poziomie poszczególnych składników („gdy byt Life
zderza się z bytem z Strength
…”).
składniki
Jaki jest powód istnienia bytu? Jeśli jest to tylko konstruktor, możemy go zastąpić funkcją zwracającą zestaw komponentów. Jeśli później chcemy zapytać o jednostki według ich typu, równie dobrze możemy mieć Tag
komponent, który pozwala nam to zrobić:
function Player():
t = Tag("Player")
p = Position()
v = Velocity(p)
c = KeyboardControlled(v)
return {t, p, v, c}
Istoty są tak głupie, jak tylko mogą być - to tylko zestawy komponentów.
Komponenty reagują bezpośrednio na zdarzenia jak poprzednio.
Interakcje muszą być teraz obsługiwane przez abstrakcyjne zapytania, całkowicie oddzielając zdarzenia od typów jednostek. Nie ma więcej typów jednostek do zapytania - Tag
do debugowania prawdopodobnie lepiej są wykorzystywać dowolne dane niż logika gry.
Wniosek
Jednostki nie są funkcjami, regułami, aktorami ani kombinatorami przepływu danych. Są rzeczownikami, które modelują konkretne zjawiska - innymi słowy, są obiektami. Jest tak, jak mówi Wikipedia - systemy encji-komponentów są wzorcem architektury oprogramowania do modelowania obiektów ogólnych.
NIE. I jestem zaskoczony, ilu ludzi głosowało inaczej!
Paradygmat
Jest to zorientowane na dane, czyli oparte na danych, ponieważ mówimy o architekturze, a nie o języku, w którym jest napisany. Architektury są realizacjami stylów programowania lub paradygmatów , które zwykle można omijać w danym języku.
Funkcjonalny?
Porównanie z programowaniem funkcjonalnym / proceduralnym jest trafnym i znaczącym porównaniem. Należy jednak pamiętać, że język „funkcjonalny” różni się od paradygmatu „proceduralnego” . I można wdrożyć ECS w języku Functional jak Haskell , co ludzie zrobili.
Gdzie zachodzi spójność
Twoje spostrzeżenie jest istotne i spot-on :
ECS / ES nie jest EC / CE
Istnieje różnica między architekturami opartymi na komponentach, „Entity-Component” i „Entity-Component-System”. Ponieważ jest to ewoluujący wzorzec projektowy, widziałem te definicje używane zamiennie. Architektury „EC” lub „CE” lub „Entity-Component” umieszczają zachowanie w komponentach , podczas gdy architektury „ES” lub „ECS” umieszczają zachowanie w systemach . Oto kilka artykułów ECS, z których oba wykorzystują wprowadzającą w błąd nomenklaturę, ale przedstawiają ogólny pomysł:
Jeśli próbujesz zrozumieć te warunki w 2015 r., Upewnij się, że czyjaś wzmianka o „systemie elementów encji” nie oznacza „architektury elementów encji”.
źródło
Systemy elementów encji (ECS) można programować w trybie OOP lub funkcjonalnie, w zależności od sposobu zdefiniowania systemu.
Sposób OOP:
Pracowałem nad grami, w których byt był obiektem złożonym z różnych elementów. Jednostka ma funkcję aktualizacji, która modyfikuje obiekt, wywołując kolejno aktualizację wszystkich jego składników. Ma to wyraźny styl OOP - zachowanie jest powiązane z danymi, a dane można modyfikować. Istoty to obiekty z konstruktorami / destruktorami / aktualizacjami.
Bardziej funkcjonalny sposób:
Alternatywą jest, aby jednostka była danymi bez żadnych metod. Ten podmiot może istnieć samodzielnie lub być po prostu identyfikatorem powiązanym z różnymi komponentami. W ten sposób możliwe jest (ale nie jest to powszechnie robione) bycie w pełni funkcjonalnym i posiadanie niezmiennych bytów i czystych systemów, które generują nowe stany składowe.
Wydaje się (z własnego doświadczenia), że ten drugi sposób zyskuje większą przyczepność i nie bez powodu. Oddzielenie danych encji od zachowania skutkuje bardziej elastycznym kodem wielokrotnego użytku (imo). W szczególności użycie systemów do aktualizacji komponentów / jednostek w partiach może być bardziej wydajne i całkowicie eliminuje złożoność komunikatów między jednostkami, które nękają wiele OOP ECS.
TLDR: Możesz to zrobić na dwa sposoby, ale argumentowałbym, że zalety dobrych systemów komponentów bytu wynikają z ich bardziej funkcjonalnego charakteru.
źródło
Systemy komponentowe zorientowane na dane mogą współistnieć z paradygmatami zorientowanymi obiektowo: - Systemy komponentowe nadają się do polimorfizmu. - Komponentami mogą być zarówno POD (zwykłe stare dane), jak i obiekty ALSO (z klasą i metodami), a całość jest nadal „zorientowana na dane”, pod warunkiem, że metody klas komponentów przetwarzają tylko dane będące własnością obiektu lokalnego.
Jeśli wybierzesz tę ścieżkę, zalecam unikanie korzystania z metod wirtualnych, ponieważ jeśli je masz, twój komponent nie jest już danymi czysto komponentowymi, a te metody kosztują więcej za wywołanie - to nie jest COM. Z reguły utrzymuj klasy komponentów w czystości od wszelkich odniesień do czegokolwiek zewnętrznego.
Przykładem może być vec2 lub vec3, kontener danych z niektórymi metodami dotyku tych danych i niczym więcej.
źródło
Uważam ECS za zasadniczo odmienny od OOP i zwykle postrzegam go w ten sam sposób, co Ty, jako bliższy funkcjonalnemu lub szczególnie proceduralnemu charakterowi z bardzo wyraźnym oddzieleniem danych od funkcjonalności. Istnieje również pozór programowania w rodzaju centralnych baz danych. Oczywiście jestem najgorszą osobą, jeśli chodzi o formalne definicje. Martwię się tylko tym, jak się rzeczy mają, a nie tym, czym są koncepcyjnie zdefiniowane.
Zakładam rodzaj ECS, w którym komponenty agregują pola danych i udostępniają je publicznie / globalnie, podmioty agregują komponenty, a systemy zapewniają funkcjonalność / zachowanie tych danych. Prowadzi to do radykalnie trudnych cech architektonicznych z tego, co zwykle nazywamy obiektową bazą kodową.
I oczywiście jest pewne zatarcie granic w sposobie, w jaki ludzie projektują / wdrażają ECS, i jest debata na temat tego, co dokładnie stanowi ECS. Jednak takie granice są również zatarte w kodzie napisanym w języku, który nazywamy językiem funkcjonalnym lub proceduralnym. Wśród tych wszystkich zagadek podstawowa stała ECS z oddzieleniem danych od funkcjonalności wydaje mi się znacznie bliższa programowaniu funkcjonalnemu lub proceduralnemu niż OOP.
Jednym z głównych powodów, dla których nie uważam za przydatne uznanie ECS za należące do klasy OOP, jest to, że większość praktyk SE związanych z OOP obraca się wokół stabilności interfejsu publicznego, z funkcjami modelowania interfejsów publicznych , a nie danymi. Podstawową ideą jest to, że większość publicznych zależności płynie w kierunku funkcji abstrakcyjnych, a nie konkretnych danych. Z tego powodu OOP powoduje, że zmiana podstawowych zachowań projektowych jest bardzo kosztowna, a zmiana konkretnych szczegółów (takich jak dane i kod wymagane do wdrożenia funkcji) jest bardzo tania.
ECS jest pod tym względem diametralnie różny, biorąc pod uwagę sposób łączenia, ponieważ większość zależności publicznych płynie w kierunku konkretnych danych: od systemów do komponentów. W rezultacie wszelkie praktyki SE związane z ECS obracałyby się wokół stabilności danych , ponieważ najbardziej publiczne i powszechnie używane interfejsy (komponenty) są w rzeczywistości tylko danymi.
W rezultacie ECS bardzo ułatwia takie rzeczy, jak zamiana silnika renderującego OpenGL na DirectX, nawet jeśli oba są zaimplementowane z radykalnie różną funkcjonalnością i nie dzielą tych samych projektów, pod warunkiem, że zarówno silnik DX, jak i GL mieć dostęp do tych samych stabilnych danych. Tymczasem byłoby to bardzo kosztowne i wymagałoby przepisania kilku systemów, aby zmienić, powiedzmy, reprezentację danych
MotionComponent
.Jest to zupełnie odwrotne od tego, co tradycyjnie kojarzymy z OOP, przynajmniej pod względem cech sprzężenia i tego, co stanowi „interfejs publiczny” vs. „prywatne szczegóły implementacji”. Oczywiście w obu przypadkach „szczegóły implementacji” są łatwe do zmiany, ale w ECS jest to projekt danych, które są kosztowne do zmiany (dane nie są szczegółami implementacji w ECS), aw OOP to projekt funkcjonalności, która jest kosztowna do zmiany (projektowanie funkcji nie jest szczegółem implementacji w OOP). To zupełnie inny pomysł na „szczegóły implementacji”, a jednym z głównych apelacji do ECS z punktu widzenia konserwacji było to, że w mojej domenie, dane potrzebne do zrobienia rzeczy łatwiej było ustabilizować i poprawnie zaprojektować raz na zawsze, niż wszystkie różne rzeczy, które moglibyśmy zrobić z tymi danymi (co zmieniałoby się cały czas, gdy klienci zmieniali zdanie i napływały nowe sugestie użytkowników). W rezultacie zauważyłem, że koszty utrzymania gwałtownie spadły, kiedy zaczęliśmy kierować zależności od funkcji abstrakcyjnych w kierunku surowych, centralnych danych (ale nadal z troską o to, które systemy uzyskują dostęp do których komponentów, aby umożliwić utrzymywanie niezmienników w rozsądnym stopniu, pomimo wszystkich danych koncepcyjnie globalnie dostępne).
I przynajmniej w moim przypadku zestaw SDK ECS z interfejsem API i wszystkimi komponentami jest faktycznie zaimplementowany w języku C i nie jest podobny do OOP. Uważam, że C jest bardziej niż wystarczające do takiego celu, biorąc pod uwagę nieodłączny brak OO w architekturach ECS i chęć posiadania architektury wtyczek, która mogłaby być używana przez najszerszą gamę języków i kompilatorów. Systemy są nadal zaimplementowane w C ++, ponieważ C ++ sprawia, że jest tam bardzo wygodnie, a systemy modelują większość złożoności i tam znajduję zastosowanie do wielu rzeczy, które mogą być uważane za bliższe OOP, ale to dotyczy szczegółów implementacji. Sam projekt architektoniczny nadal bardzo przypomina procedurę C.
Sądzę więc, że przynajmniej nieco mylące jest stwierdzenie, że ECS jest z definicji OO. Przynajmniej podstawy wykonują rzeczy, które są całkowicie obrócone o 180 stopni w stosunku do wielu podstawowych zasad ogólnie związanych z OOP, zaczynając od enkapsulacji, a może kończąc na czymś, co można by uznać za pożądane cechy sprzężenia.
źródło