Dlaczego przechowywanie metod w jednostkach i komponentach jest złym pomysłem? (Wraz z innymi pytaniami dotyczącymi Systemu Entity.)

16

Jest to kontynuacja tego pytania, na które odpowiedziałem, ale to dotyczy bardziej szczegółowego tematu.

Ta odpowiedź pomogła mi zrozumieć Entity Systems nawet lepiej niż ten artykuł.

Przeczytałem (tak,) artykuł o systemach Entity Systems i powiedział mi, co następuje:

Encje to tylko identyfikator i tablica komponentów (artykuły mówią, że przechowywanie encji w komponentach nie jest dobrym sposobem robienia rzeczy, ale nie stanowi alternatywy).
Składniki to fragmenty danych, które wskazują, co można zrobić z określonym bytem.
Systemy są „metodami”, które manipulują danymi na bytach.

Wydaje się to bardzo praktyczne w wielu sytuacjach, ale niepokoi mnie to, że komponenty są tylko klasami danych. Na przykład, jak mogę zaimplementować moją klasę Vector2D (Pozycja) w systemie Entity?

Klasa Vector2D przechowuje współrzędne data: xiy, ale ma także metody , które są kluczowe dla jej użyteczności i odróżniają klasę od tablicy tylko dwóch elementów. Przykładowe metody są: add(), rotate(point, r, angle), substract(), normalize()i wszystkie inne standardowe, użyteczny i nie jest to absolutnie konieczne metody, które pozycje (które są instancje klasy Vector2D) powinna posiadać.

Gdyby komponent był tylko posiadaczem danych, nie byłby w stanie mieć tych metod!

Jednym z rozwiązań, które prawdopodobnie mogłoby się pojawić, byłoby wdrożenie ich w systemach, ale wydaje się to bardzo sprzeczne z intuicją. Te metody to rzeczy, które chcę teraz wykonać, aby były kompletne i gotowe do użycia. Nie chcę czekać na MovementSystemprzeczytanie jakiegoś drogiego zestawu komunikatów, które nakazują mu wykonać obliczenia dotyczące pozycji bytu!

I artykuł bardzo wyraźnie stwierdza, że tylko systemy powinny mieć jakąkolwiek funkcjonalność, a jedynym wyjaśnieniem tego, co mogłem znaleźć, było „unikanie OOP”. Przede wszystkim nie rozumiem, dlaczego powinienem powstrzymywać się od stosowania metod w jednostkach i komponentach. Narzut pamięci jest praktycznie taki sam, a w połączeniu z systemami powinny być bardzo łatwe do wdrożenia i łączenia w interesujący sposób. Na przykład systemy mogą zapewniać podstawową logikę tylko jednostkom / komponentom, które same znają implementację. Jeśli mnie o to zapytasz - to w zasadzie pobieranie korzyści zarówno z ES, jak i OOP, czego według autora artykułu nie można zrobić, ale wydaje mi się, że to dobra praktyka.

Pomyśl o tym w ten sposób; w grze istnieje wiele różnych typów obiektów do rysowania. Plain Old obrazy, animacje ( update(), getCurrentFrame()itp), kombinacje tych typów pierwotnych, a wszystkie z nich może po prostu stanowić draw()metodę do systemu render, który wtedy nie trzeba się martwić o jak sprite podmiotu jest realizowany tylko na temat interfejsu (rysowanie) i pozycji. A potem potrzebowałbym tylko systemu animacji, który nazwałby metody specyficzne dla animacji, które nie mają nic wspólnego z renderowaniem.

I jeszcze jedna rzecz ... Czy naprawdę istnieje alternatywa dla tablic, jeśli chodzi o przechowywanie komponentów? Nie widzę innego miejsca do przechowywania komponentów niż tablice wewnątrz klasy Entity ...

Być może jest to lepsze podejście: przechowuj komponenty jako proste właściwości jednostek. Na przykład element pozycji byłby przyklejony entity.position.

Tylko inny sposób byłoby mieć jakąś dziwną tabeli odnośników wewnątrz systemów, która odwołuje różne podmioty. Ale wydaje się to bardzo nieefektywne i bardziej skomplikowane do opracowania niż zwykłe przechowywanie komponentów w bycie.

jcora
źródło
Alexandre, czy właśnie wprowadzasz wiele zmian, aby uzyskać kolejną odznakę? Ponieważ jest to niegrzeczne, niegrzeczne, wciąż obija mnóstwo starożytnych wątków.
jhocking

Odpowiedzi:

25

Myślę, że w porządku jest mieć proste metody uzyskiwania dostępu do danych w komponentach, ich aktualizowania lub manipulowania nimi. Myślę, że funkcjonalność, która powinna pozostać poza komponentami, to logiczna funkcjonalność. Funkcje narzędziowe są w porządku. Pamiętaj, że system element-jednostka jest jedynie wytyczną, a nie ścisłymi zasadami, których musisz przestrzegać. Nie staraj się ich przestrzegać. Jeśli uważasz, że bardziej sensowne jest to zrobić w jedną stronę, zrób to w ten sposób :)

EDYTOWAĆ

Aby to wyjaśnić, Twoim celem nie jest unikanie OOP . Byłoby to dość trudne w większości popularnych języków używanych obecnie. Próbujesz zminimalizować dziedziczenie , które jest dużym aspektem OOP, ale nie jest wymagane. Chcesz się pozbyć dziedziczenia typu Object-> MobileObject-> Creature-> Bipedal-> Human.

Jednak dziedziczenie jest w porządku! Masz do czynienia z językiem, na który ma duży wpływ dziedziczenie, bardzo trudno go nie używać. Na przykład możesz mieć Componentklasę lub interfejs, który wszystkie inne komponenty rozszerzają lub implementują. To samo dotyczy twojej Systemklasy. To sprawia, że ​​jest o wiele łatwiej. Zdecydowanie polecam przyjrzeć się ramom Artemis . Jest open source i ma kilka przykładowych projektów. Otwórz te rzeczy i zobacz, jak to działa.

W przypadku Artemidy byty są przechowywane w tablicy, proste. Jednak ich komponenty są przechowywane w tablicy lub tablicach (oddzielnie od encji). Tablica najwyższego poziomu grupuje tablicę niższego poziomu według typu komponentu. Dlatego każdy typ komponentu ma własną tablicę. Tablica niższego poziomu jest indeksowana według identyfikatora jednostki. (Teraz nie jestem pewien, czy zrobiłbym to w ten sposób, ale tak to się tutaj robi). Artemis ponownie wykorzystuje identyfikatory encji, więc maksymalny identyfikator encji nie będzie większy niż bieżąca liczba encji, ale nadal możesz mieć rzadkie tablice, jeśli komponent nie jest często używanym komponentem. W każdym razie nie będę tego zbytnio rozbierał. Ta metoda przechowywania bytów i ich komponentów wydaje się działać. Myślę, że byłby to świetny pierwszy krok przy wdrażaniu własnego systemu.

Jednostki i komponenty są przechowywane w osobnym menedżerze.

Wspomniana strategia polegająca na tym, że byty przechowują własne komponenty ( entity.position), jest w pewnym sensie sprzeczna z kompozycją komponentu encji, ale jest całkowicie akceptowalna, jeśli uważasz, że jest to najbardziej sensowne.

MichaelHouse
źródło
1
Hmm, to znacznie upraszcza sytuację, dzięki! Myślałem, że dzieje się jakaś magia „będziesz tego później żałować” i po prostu nie mogłem tego zobaczyć!
jcora
1
Nie, całkowicie używam ich w moim systemie komponentów bytu. Mam nawet niektóre elementy, które dziedziczą po wspólnym rodzicu, z trudem łapiąc powietrze . Myślę, że jedyne czego żałujesz, to gdybyś próbował obejść się, nie używając takich metod. Chodzi o robienie tego, co jest dla ciebie najbardziej sensowne. Jeśli warto zastosować dziedziczenie lub umieścić niektóre metody w komponencie, skorzystaj z niego.
MichaelHouse
2
Nauczyłem się z mojej ostatniej odpowiedzi na ten temat. Zastrzeżenie: Nie mówię, że to jest sposób to zrobić. :)
MichaelHouse
1
Tak, wiem, jak trudne może być nauczenie się nowego paradygmatu. Na szczęście możesz użyć aspektów starego paradygmatu, aby ułatwić sobie życie! Zaktualizowałem odpowiedź o informacje o pamięci. Jeśli spojrzysz na Artemis, sprawdź, EntityManagerjak tam są przechowywane rzeczy.
MichaelHouse
1
Ładny! To będzie całkiem słodki silnik, gdy zostanie ukończony. Powodzenia z tym! Dziękujemy za zadawanie interesujących pytań.
MichaelHouse
10

Ten „artykuł” nie jest tym, z którym się szczególnie zgadzam, więc myślę, że moja odpowiedź będzie nieco krytyczna.

Wydaje się to bardzo praktyczne w wielu sytuacjach, ale niepokoi mnie to, że komponenty są tylko klasami danych. Na przykład, jak mogę zaimplementować moją klasę Vector2D (Pozycja) w systemie Entity?

Pomysł nie polega na tym, aby upewnić się, że nic w twoim programie nie jest niczym innym jak identyfikatorem encji, komponentem lub systemem - ma na celu zapewnienie, że dane encji i zachowanie są tworzone przez komponowanie obiektów, a nie przez użycie złożonego drzewa dziedziczenia lub gorzej umieść wszystkie możliwe funkcje w jednym obiekcie. Aby wdrożyć te komponenty i systemy, na pewno będziesz mieć normalne dane, takie jak wektory, które w większości języków najlepiej reprezentowane są jako klasa.

Zignoruj ​​ten fragment w artykule, który sugeruje, że to nie jest OOP - jest tak samo OOP, jak każde inne podejście. Kiedy większość kompilatorów lub środowisk wykonawczych języka implementuje metody obiektowe, jest to w zasadzie jak każda inna funkcja, z wyjątkiem tego, że istnieje ukryty argument o nazwie thislub self, który jest wskaźnikiem do miejsca w pamięci, w którym przechowywane są dane tego obiektu. W systemie opartym na komponentach można użyć identyfikatora encji, aby znaleźć, gdzie odpowiednie komponenty (a tym samym dane) są dla danej encji. Zatem identyfikator jednostki jest równoważny wskaźnikowi to / ja, a koncepcje są w zasadzie takie same, tylko nieco zmienione.

I artykuł bardzo wyraźnie stwierdza, że ​​tylko systemy powinny mieć jakąkolwiek funkcjonalność, a jedynym wyjaśnieniem tego, co mogłem znaleźć, było „unikanie OOP”. Przede wszystkim nie rozumiem, dlaczego powinienem powstrzymywać się od stosowania metod w jednostkach i komponentach.

Dobry. Metody to skuteczny sposób organizowania kodu. Ważną rzeczą, którą należy odrzucić od idei „unikaj OOP”, jest unikanie wszędzie używania dziedziczenia w celu rozszerzenia funkcjonalności. Zamiast tego podziel funkcjonalność na komponenty, które można łączyć, aby zrobić to samo.

Pomyśl o tym w ten sposób; w grze istnieje wiele różnych typów obiektów do rysowania. Zwykłe stare obrazy, animacje (update (), getCurrentFrame () itp.), Kombinacje tych prymitywnych typów i wszystkie z nich mogłyby po prostu zapewnić metodę draw () systemowi renderowania [...]

Idea systemu opartego na komponentach polega na tym, że nie miałbyś dla nich osobnych klas, ale miałbyś pojedynczą klasę Object / Entity, a obraz byłby Object / Entity, który ma ImageRenderer, Animacje byłyby Object / Podmiot, który ma AnimationRenderer itp. Odpowiednie systemy będą wiedziały, jak renderować te komponenty, a więc nie będzie żadnej klasy bazowej z metodą Draw ().

[...] który nie musi się tym przejmować sposobem implementacji duszka encji, a jedynie interfejsem (rysowaniem) i pozycją. A potem potrzebowałbym tylko systemu animacji, który wywołałby metody specyficzne dla animacji, które nie mają nic wspólnego z renderowaniem.

Jasne, ale to nie działa dobrze z komponentami. Masz 3 możliwości:

  • Każdy komponent implementuje ten interfejs i ma metodę Draw (), nawet jeśli nic nie zostanie narysowane. Jeśli zrobiłbyś to dla każdej funkcjonalności, komponenty wyglądałyby dość brzydko.
  • Tylko komponenty, które mają coś do rysowania, implementują interfejs - ale kto decyduje, na których komponentach wywołać Draw ()? Czy system musi w jakiś sposób sprawdzać każdy komponent, aby zobaczyć, który interfejs jest obsługiwany? Byłoby to podatne na błędy i potencjalnie trudne do wdrożenia w niektórych językach.
  • Komponenty są obsługiwane tylko przez ich własny system (taki jest pomysł w powiązanym artykule). W takim przypadku interfejs nie ma znaczenia, ponieważ system dokładnie wie, z jaką klasą lub typem obiektu pracuje.

I jeszcze jedna rzecz ... Czy naprawdę istnieje alternatywa dla tablic, jeśli chodzi o przechowywanie komponentów? Nie widzę innego miejsca do przechowywania komponentów niż tablice wewnątrz klasy Entity ...

Możesz przechowywać komponenty w systemie. Tablica nie jest problemem, ale tam, gdzie jest przechowywany komponent.

Kylotan
źródło
+1 Dzięki za kolejny punkt widzenia. Dobrze jest zdobyć ich kilka, zajmując się tak dwuznacznym tematem! Jeśli przechowujesz komponenty w systemie, czy to oznacza, że ​​komponenty mogą być modyfikowane tylko przez jeden system? Na przykład system rysowania i system ruchu miałyby dostęp do komponentu pozycji. Gdzie to przechowujesz?
MichaelHouse
Cóż, zapisaliby tylko wskaźnik do tych komponentów, które, o ile mnie to martwi, mogą być gdzieś ... Dlaczego mielibyście przechowywać komponenty w systemach? Czy ma to jakąś zaletę?
jcora
Czy mam rację, @Kylotan? Tak bym to zrobił, wydaje się logiczne ...
jcora,
W przykładzie Adama / T-Machine intencją jest, aby istniał 1 system na komponent, ale system z pewnością mógłby uzyskiwać dostęp i modyfikować inne komponenty. (Utrudnia to wielowątkowe korzyści komponentów, ale to inna sprawa.)
Kylotan
1
Przechowywanie komponentów w systemie pozwala na lepszą lokalizację odniesienia dla tego systemu - ten system tylko (ogólnie) współpracuje z tymi danymi, więc po co przechodzić całą pamięć komputera od jednostki do jednostki, aby ją uzyskać? Pomaga również w współbieżności, ponieważ można umieścić cały system i jego dane na jednym rdzeniu lub procesorze (lub nawet na osobnym komputerze w MMO). Ponownie, korzyści te zmniejszają się, gdy 1 system uzyskuje dostęp do więcej niż jednego rodzaju komponentu, dlatego należy wziąć to pod uwagę przy podejmowaniu decyzji, gdzie podzielić obowiązki komponentu / systemu.
Kylotan,
2

Wektor to dane. Funkcje są bardziej podobne do funkcji narzędziowych - nie są specyficzne dla tego wystąpienia danych, mogą być stosowane niezależnie do wszystkich wektorów. Dobrym sposobem myślenia jest: czy te funkcje można przepisać jako metody statyczne? Jeśli tak, to tylko narzędzie.

Matt Kemp
źródło
Wiem o tym, ale problem polega na tym, że wywoływanie metod jest szybsze i może być wykonane na miejscu przez system lub cokolwiek innego, co może wymagać manipulacji pozycją bytu. Wyjaśniłem, że, sprawdźcie, również pytanie ma znacznie więcej niż tylko to, jak sądzę.
jcora