Autor Wątek: Podejście Entity component system (ECS) w celu unikniecia BLOBa  (Przeczytany 3966 razy)

Offline Adam B

  • Użytkownik

# Lipiec 29, 2015, 11:00:37
Hej zaczynam pisać gierke:
http://www.supertowerdefence.pl/

Ogólnie to ma być tower defence ale chce na niego patrzeć trochę szerzej. Zacząłem się zastanawiać co potrzebują moje obiekty aby móc odtworzyć przynajmniej część funkcjonalności jakie są w grze Starcraft/Starcraft2.

Przykłady:
 - Budynek produkuje "cos" ale są budynki co: latają, poruszają się, strzelają, mają kilka trybów itp.
 - Jednostki: strzelaja, lub atakują w zwarciu, mogą mieć różne prędkości w zależności od tego czy latają czy poruszają się po ziemi (wodę też można dodać), produkują w sumie naboje (budynki np. jednostki)
 - Pociski: Zadają obrażenia, poruszają się, mają zasięg

Ogólnie wychodzi na, że potencjalnie budynek i pocisk mogą posiadać wszystkie właściwości jednostki. Ale przecież tylko 1 na 20 budynków umie latać i się poruszać, 1 na 20 umie strzelać oraz są budynki, które nic nie produkują np. bunkier. Tak samo z jednostkami.


Zaciekawiło mnie podejście ECS ale nie znalazłem konkretnych przykładów jak to wygląda w praktyce.

Zakładam, że:
 - Obiekt "Entity" ma id i liste "Components",
 - Posiadam listę, w której mam wszystkie "Entity"
 - Posiadam managery, które potrafią reagować na określony "Component" np. Manager ruchu porusza obiekty na podstawie komponentu ruchu (MoveManager i MoveComponent)

I tutaj zaczynam mieć pytania:
 - Czy komponent ruchu powinien mieć pozycje X, Y ? Co wtedy z komponentem Pozycji ? A może jakoś powiązać te komponenty...
 - Przeważnie jak pisałem jakiś projekt za poruszanie był odpowiedzialny moduł fizyki, który też obsługiwał kolizje i reakcje na nie.
 - Zastanawia mnie też kod głównej pętli przeglądania Entity, czy wygląda to tak?:
Jeżeli Komponent ruchu to wywołaj manager ruchu,
Jeżeli Komponent fizyki to wywołaj manager fizyki,
Jeżeli Komponent strzelania to wywołaj manager strzelania.

A może to managery uruchamiają się w kolejności i przeszukują wszystkie obiekty i jak obiekt ma komponent, na którym pracuje dany manager to manager wywołuje na nim odpowiedni kod/funkcje? Jednak w tym założeniu widzę zagrożenie ze strony zmniejszenia wydajności - każdy nowy komponent to kolejna iteracja po elementach. Z drugiej strony można mieć sublisty dla tych komponentów, które są używane bardzo rzadko.

Ogólnie widzę trudność w podziale projektu na dobre komponenty, które nie będą się dublować i które będą od siebie niezależne.


Może ktoś pracował w tym podejściu i ma jakieś cenne rady ?
Może dla mojego problemu warto zainteresować się jeszcze jakim innym podejściem ? np. FRP?
Dobre materiały związane z grami typu RTS mile widziane.

Pozdrawiam,
Adam

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline lukasz1985

  • Użytkownik

# Lipiec 29, 2015, 12:09:59
Ja bym nie wchodził w ten cały bałagan z ECS. Od tego są wzorce projektowe, jak choćby "Strategy" w tym przypadku. ECS to po prostu przerośnięte wielodziedziczenie i tylko komplikuje życie, i źle wpływa na wydajność.

Offline Paweł

  • Użytkownik

# Lipiec 29, 2015, 12:23:00
Pracuję nad swoim ECS i po (bardzo wielu) próbach doszedłem do czegoś takiego:
każdy obiekt składa się z komponentów, tworzących "mieszankę" komponentów (EntityKind).
Dla każdej mieszanki mam osobny kontener (EntityStorage). Każdy system przetwaeza dane z wielu kontenerów. Jako że dany komponent może mieć inny offset w każdeu "mieszance" EntityKind trzyma lookup table mapujące id komponentu na offset (zwykła tablica shortów). Do tego podczas tworzenia EntityKind dla każdego komponentu zliczam użycia przez systemy i rozmieszczam "pola" (komponenty)  w kolejności od najpopularniejszego.

Nie mam przesyłania komunikatów, jedynie modyfikacja danych z komponentów.

Jak skończę to mogę upublicznić kod, pytanie kto się w tym połapie.

Offline Radomiej

  • Użytkownik
    • Blog

  • +1
# Lipiec 29, 2015, 13:15:03
Jeśli chodzi o ECS to bardzo ciekawe sprawa, też zacząłem tworzyć gry w oparciu o ten model + system zdarzeń. Być może wydajnościowo stoi to gorzej od pro-optymized code, ale za to jest o wiele bardziej łatwiejszy w utrzymaniu. Sam korzystam z https://github.com/libgdx/ashley.

I tutaj zaczynam mieć pytania:
 - Czy komponent ruchu powinien mieć pozycje X, Y ? Co wtedy z komponentem Pozycji ? A może jakoś powiązać te komponenty...

Ogólnie idea jest taka że tworzysz obiekt Entity do którego wrzucasz swoje komponenty, powiedzmy MoveComponent i PositionComponent. Takie obiekt Entity wrzucasz do Engine(silnika ECS), następnie tworzysz systemy p. PhysicSystem - system odpowiedzialny za przesuwanie jednostki. W nim pobierasz listę obiektów Entity które posiadają MoveComponent i PositionComponent(oba naraz). A następnie wykonujesz logikę, czyli sprawdzasz czy ma się poruszyć i czy może się poruszyć.
Potem tworzysz kolejny system np. AiSystem - który odpowiada za akcje jakie ma podjąć jednostka, gdzie iść, co zrobić. Tam też pobierasz listę Entity które mają MoveComponent i PositionComponent i sobie przetwarzasz dane z nich.

Potem jak chcesz dodać kolizje na podstawie ścieżki/mapy to dodajesz Entity reprezentującą mapę np. MapComponent albo Entity reprezentujące nawet poszczególne pola mapy i dodajesz je do silnika a w Systemach je pobierasz.

- Przeważnie jak pisałem jakiś projekt za poruszanie był odpowiedzialny moduł fizyki, który też obsługiwał kolizje i reakcje na nie.

Ogólnie obiekty dziedziczące po System traktujesz jako moduły które wykonują logikę a w Component trzymasz same dane. Jeśli chcesz wymieniać informacje między modułami to powinieneś to robić poprzez jakiś luźny system. Np. system zdarzeń, wiadomości, ewentualnie za pomocą danych w Component.

- Zastanawia mnie też kod głównej pętli przeglądania Entity, czy wygląda to tak?:
Jeżeli Komponent ruchu to wywołaj manager ruchu,
Jeżeli Komponent fizyki to wywołaj manager fizyki,
Jeżeli Komponent strzelania to wywołaj manager strzelania.

A może to managery uruchamiają się w kolejności i przeszukują wszystkie obiekty i jak obiekt ma komponent, na którym pracuje dany manager to manager wywołuje na nim odpowiedni kod/funkcje? Jednak w tym założeniu widzę zagrożenie ze strony zmniejszenia wydajności - każdy nowy komponent to kolejna iteracja po elementach. Z drugiej strony można mieć sublisty dla tych komponentów, które są używane bardzo rzadko.


Ogólnie jest to zrobione tak że do systemu wrzucasz sobie dowolne Entity reprezentujące np. planszę gry czyli budynki, pola, wrogów. Mogą to też być oczywiście jakieś menusy/buttony/animacje itp.
Następnie rejestrujesz Systemy np. PhysicSystem, RenderSystem.
Potem na silniku wywołujesz funkcję update() i silnik sam uruchamia metody update() w twoich systemach. A to system zarządza jakie Entity chce pobrać i co z nimi zrobić. Oczywiście główną zaletą takie systemu jest to że możesz pobrać konkretne Entity zawierające dane komponenty np. w silniku fizyki pobierasz Entity zawierajace PhysicComponent i PositionComponent, w silniku do grafiki pobierasz RenderComponent i PositionComponent i tak jeden raz aktualizujesz ten sam obiekt w systemie od fizyki a w drugim go rysujesz. Nie obchodzi ciebie jakie dodatkowe Componenty on posiada, dzięki temu możesz go dowolnie modyfikować. Np. jeśli budynek składa się z komponentów: BuildingComponent i PositionComponent, to chcąc zrobić latający budynek dodajesz jedynie FlyComponent i tworzysz system w którym obsługujesz latanie. Co teoretycznie powinno cię zwolnić w modyfikowaniu innych systemów(choć w praktyce często trzeba poprawiać niektóre kwestie).

Ogólnie widzę trudność w podziale projektu na dobre komponenty, które nie będą się dublować i które będą od siebie niezależne.
Trudność ta wynika z luźnego podejścia jakie daje komponentowy system, ale to kwestia po prostu praktyki:)

Offline DanielMz25

  • Użytkownik

# Lipiec 29, 2015, 13:42:58
Tak się zastanawiam. Bo im dłużej myślę, tym bardziej nie widzę powodu dla którego ECS miałoby być jakieś bardzo powolne, gdyby jeszcze moduły trzymać oddzielnie, i powiązać z głównym obiektem jakoś przez wskaźnik, to mielibyśmy data oriented design, czyli optymalizację na maxa, gdybyśmy zredukowali albo całkiem pozbyli się metod wirtualnych... Hmm, może niech ktoś mądrzejszy wskaże mi jakieś wady tego rozwiązania, dlaczego miałoby to być powolne.

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

  • +2
# Lipiec 29, 2015, 14:04:07
Cytuj
Ogólnie wychodzi na, że potencjalnie budynek i pocisk mogą posiadać wszystkie właściwości jednostki. Ale przecież tylko 1 na 20 budynków umie latać i się poruszać, 1 na 20 umie strzelać oraz są budynki, które nic nie produkują np. bunkier. Tak samo z jednostkami.
Wniosek: zrobić na wszystko jedną klasę obiekt/encja/whatever która będzie posiadała wszystkie potrzebne pola.

Cytuj
Może ktoś pracował w tym podejściu i ma jakieś cenne rady ?
Nie przejmować się rozmiarem encji dopóki rzeczywiście nie będzie to realnym problemem. Podkreślam: realnym.

Wiem, że pisanie nieoptymalnego kodu bardzo boli mentalnie, ale ile może zająć pamięci nawet najbardziej nieoptymalna encja? 10kB? Wątpię, a nawet jeśli, to 1000 takich encji zajmie 10MB, co jest obecnie niczym nawet na platformach mobilnych.

Roztrzepanie encji na komponenty bardzo ogranicza produktywność, jak widać po dywagacjach w tym wątku, a zysku da może kilka MB pamięci. Nieopłacalna wymiana. :)

Offline Adam B

  • Użytkownik

# Lipiec 29, 2015, 22:17:54
Tak się zastanawiam. Bo im dłużej myślę, tym bardziej nie widzę powodu dla którego ECS miałoby być jakieś bardzo powolne, gdyby jeszcze moduły trzymać oddzielnie, i powiązać z głównym obiektem jakoś przez wskaźnik, to mielibyśmy data oriented design, czyli optymalizację na maxa, gdybyśmy zredukowali albo całkiem pozbyli się metod wirtualnych... Hmm, może niech ktoś mądrzejszy wskaże mi jakieś wady tego rozwiązania, dlaczego miałoby to być powolne.

Bardziej chodzi o zapis i odczyt - jak masz id elementow to latwo to zapisac i wczytac ze wskaznikami i referencjami jest duzo ciezej...

Oczywiscie mozna miec ID i referencje.. Wtedy zapisujesz ID a jak odczytujesz nadajesz obiekta id i potem robisz system, ktory na podstawie id powiazuje obiekty za pomoca referencji..

Offline Adam B

  • Użytkownik

# Lipiec 30, 2015, 15:30:24
Wniosek: zrobić na wszystko jedną klasę obiekt/encja/whatever która będzie posiadała wszystkie potrzebne pola.
Nie przejmować się rozmiarem encji dopóki rzeczywiście nie będzie to realnym problemem. Podkreślam: realnym.

Wiem, że pisanie nieoptymalnego kodu bardzo boli mentalnie, ale ile może zająć pamięci nawet najbardziej nieoptymalna encja? 10kB? Wątpię, a nawet jeśli, to 1000 takich encji zajmie 10MB, co jest obecnie niczym nawet na platformach mobilnych.

Roztrzepanie encji na komponenty bardzo ogranicza produktywność, jak widać po dywagacjach w tym wątku, a zysku da może kilka MB pamięci. Nieopłacalna wymiana. :)

Gierka jest w przeglądarce i wszystkie dane stanu gry musza być zapisywane na serwerze (JSON - potem przejde na format binarny). Bardziej patrze na to, ze jak będę miał w obiekcie średnio 40% niepotrzebnych danych to zapis gry będzie znacznie większy..

Aczkolwiek z drugiej strony jeżeli coś ma mieć wartość domyślną bo nie jest przez dany obiekt w ogóle brane pod uwagę to w zapisie mogę tego w ogóle nie uwzględniać..

Offline Kos

  • Użytkownik
    • kos.gd

# Lipiec 30, 2015, 15:40:12
Bardziej patrze na to, ze jak będę miał w obiekcie średnio 40% niepotrzebnych danych to zapis gry będzie znacznie większy..

Gzip :-)

BTW: Zapis stanu gry to jest taki trochę poboczny podsystem. O architekturze kodu decyduj mając na uwadze ważniejsze sprawy (jak najwygodniej zakodować samą grę?)

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

  • +1
# Lipiec 30, 2015, 15:48:28
Cytuj
Bardziej patrze na to, ze jak będę miał w obiekcie średnio 40% niepotrzebnych danych to zapis gry będzie znacznie większy..
To zapisuj tylko te pola, które mają wartość różną od wartości domyślnej. W efekcie zapiszesz tylko to, czego dana encja rzeczywiście używa.

Cytuj
Aczkolwiek z drugiej strony jeżeli coś ma mieć wartość domyślną bo nie jest przez dany obiekt w ogóle brane pod uwagę to w zapisie mogę tego w ogóle nie uwzględniać..
Ano właśnie.

Offline Adam B

  • Użytkownik

  • +1
# Lipiec 31, 2015, 18:20:58
Zdecydowałem się na wrzucenie wszystkiego do podstawowego obiektu i zobaczę co z tego wyjdzie.

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

  • +3
# Lipiec 31, 2015, 18:28:17
Zdecydowałem się na wrzucenie wszystkiego do podstawowego obiektu i zobaczę co z tego wyjdzie.
Co wyjdzie? Pewnie się uda grę napisać dla odmiany zamiast tylko silnika jak to często bywa. :)

Offline nameczanin

  • Użytkownik
    • devlog

  • +1
# Sierpień 02, 2015, 17:12:51
ECS ma swoje plusy, ale nie zaliczałbym mu poprawy wydajności, bo architektura za wiele SAMA tutaj nie załatwia, bez dyscypliny łatwo o cache missy. Ale - jak to Krzysiek powiedział - z reguły to nie jest realny problem. Unikaj złożoności kwadratowej, a reszta przyjdzie z czasem.

Moje spostrzeżenia odnośnie ECS:

1. Podejście jest odmienne od OOP, zamiast hierarchii mamy płaską strukturę: zestawy danych (entity+lista komponentów) i systemy. Konkretne systemy operują na konkretnych zestawach danych - ot, taka architektura.
2. Łatwo jest refaktoryzować coś, co nie ma hierarchii. Bardzo pomocne przy prototypowaniu. Można wszystkie dane wrzucić do jednej klasy, ale to jest do czasu :)
3. Jak wszystko jest w komponentach, w których są czyste dane, to łatwiej o inspekcję (https://github.com/Namek/artemis-odb-entity-tracker) czy serializację świata gry.

Radomiej wspomniał o Ashley, ja korzystam z artemis-odb.

Najlepiej popatrzeć w różne rozwiązania i sobie je ocenić. Niektóre gry z tej listy są open source: https://github.com/junkdog/artemis-odb/wiki/Game-Gallery

Dla ogólnego zarysu nt. ECS warto zajrzeć też tutaj: https://github.com/junkdog/artemis-odb/wiki/Introduction-to-Entity-Systems
« Ostatnia zmiana: Sierpień 02, 2015, 17:14:34 wysłana przez nameczanin »

Offline Adam B

  • Użytkownik

# Sierpień 12, 2015, 01:27:19
Kontynuuję watek w tym poście.

Bez akademickiego podejścia do dziedziczenia (refaktoryzuję jak jest potrzeba) itp udało mi się osiągnąć kilka nowych funkcjonalności.

Mam natomiast problem z szukaniem drogi oraz o wykrywaniem i reagowaniem na kolizje podczas poruszania się po mapie. Temat podzieliłem na:
 - mapę - poruszanie się po mapie bez przeciwników,
 - reagowanie gdy na mapie pojawia się budowle
 - reagowanie na inne jednostki - w mojej grze są to robaczki.
 - ocenianie czy cel podróży został osiągnięty

Odrobiłem zadanie domowe i zbadałem jak to jest w StarCrafcie2.
1. Jednostki bez problemu poruszają sią po mapie nawet jak jest to labirynt.
2. Wybudowanie na mapie budynku powoduje przeliczenie możliwych ścieżek na mapie - robienie z budynków labiryntu nie powoduje, że jednostki nie mogą przejść mapy
3. Ustawienie z jednostek np. zerglingów na holdzie labiryntu - powoduje ze inne jednostki nie potrafią się wydostać z tak stworzonego labiryntu - pewnie postawienie jednostki na hold nie ma wpływu na pathfinding.
4. Jeżeli jednostki się stykają/sa bardzo blisko siebie - to strają się wyminąć
5. Duże grupy jednostek po chwili dotarcia na punkt docelowy staja w miejscu i już nie "przepychają się".
6. Jeżeli jednostki nie są na holdzie to przepychają się - o dziwo kilka zerglingów może przepchnąć ultraliska.. dlatego nie wiem czy jest w tym algorytmie brana pod uwagę masa jednostek.

Na teraz nie chce być jeszcze skupiony na punktach 1, 2  - chciałem zająć się punktami 3, 4, 5.
Punkt 4 mam zaimplementowany w wersji podstawowej:

Zachęcam do sprawdzenia na: supertowerdefence.pl

Mam następujące pytania:
 - czemu ustawienie zerglingów (i innych jednostek też) na holdzie powoduje wyżej opisany błąd - taki projekt gry czy bardziej mogło chodzić o wydajność ??
 - kiedy uznać, że jednostki dotarły do punku końcowego lub że dotarły do określonego waypointu na trasie ?? U mnie jednostki się kotłują i wszystkie chce wejść w finalny punkt dlatego nigdy się nie zatrzymują :)
 - czy jak jednostki nachodzą na siebie warto zastosować algorytm podobny do tych z sliników fizycznych polegający na rozpychaniu się od siebie obiektów ?
 - nawet ustawienie jednostek w prostą lterke V powoduje, ze u mnie nie zostanie znaleziona droga - są tutaj znane jakieś rozwiązania ?

Pozdrawiam,
Adam

Offline fn2000

  • Użytkownik

  • +1
# Sierpień 12, 2015, 13:22:47
Do pytań:

ad 1) zależy to od implementacji, prawdopodobnie szukanie drogi nie działa dla mobilnych jednostek - dla nich są tylko proste obliczenia niewchodzenia na siebie i poruszania się w grupie.

ad 2) fuzzy-logic ku pomocy :) załóż, że jeśli mob jest w odległości X jednostek od celu i np. jest w otoczeniu kilku innych z własnej grupy, to uznaje, że dotarł na miejsce i nie próbuje osiągnąć punktowo x.y.z. celu

ad 3) jak odp. wyżej - zależy od tego czy akceptowalny jest efekt przenikania się graficznych reprezentacji mobów.

ad 4) musisz stosować tutaj algorytmy poszukiwania drogi dla formacji - wtedy drogi nie szuka każda z jednostek oddzielnie tylko arbitralnie założony "środek ciężkości" grupy, a reszta podąża za nim, zachowując formację.