Grupowanie elementów tego samego komponentu w pamięci liniowej

12

Zaczynamy od podstawowego podejścia system-komponenty-byty .

Stwórzmy zespoły (termin wywodzący się z tego artykułu) jedynie na podstawie informacji o typach komponentów . Odbywa się to dynamicznie w czasie wykonywania, tak jak dodawalibyśmy / usuwaliśmy komponenty do encji jeden po drugim, ale nazwijmy to bardziej precyzyjnie, ponieważ dotyczą one tylko informacji o typie.

Następnie konstruujemy byty określające zestawienie dla każdego z nich. Po utworzeniu bytu jego składanie jest niezmienne, co oznacza, że ​​nie możemy go bezpośrednio zmodyfikować w miejscu, ale nadal możemy uzyskać podpis istniejącego bytu na lokalnej kopii (wraz z treścią), wprowadzić odpowiednie zmiany i utworzyć nowy byt z tego.

Teraz kluczowa koncepcja: za każdym razem, gdy tworzony jest byt, jest on przypisywany do obiektu o nazwie wiadro asemblażu , co oznacza, że ​​wszystkie byty o tym samym podpisie będą w tym samym kontenerze (np. W std :: vector).

Teraz systemy tylko iterują przez każdy segment ich zainteresowań i wykonują swoją pracę.

Takie podejście ma kilka zalet:

  • komponenty są przechowywane w kilku (dokładnie: liczbie segmentów) ciągłych porcjach pamięci - poprawia to łatwość obsługi pamięci i łatwiej jest zrzucić cały stan gry
  • systemy przetwarzają komponenty w sposób liniowy, co oznacza lepszą spójność pamięci podręcznej - słowniki pa pa i losowe skoki pamięci
  • tworzenie nowego elementu jest tak proste, jak mapowanie zestawu do segmentu i wypychanie potrzebnych komponentów do jego wektora
  • usunięcie encji jest tak proste jak jedno wywołanie do std :: move, aby zamienić ostatni element na usunięty, ponieważ kolejność nie ma znaczenia w tym momencie

wprowadź opis zdjęcia tutaj

Jeśli mamy wiele podmiotów z zupełnie innymi sygnaturami, korzyści z koherencji pamięci podręcznej nieco się zmniejszają, ale nie sądzę, aby miało to miejsce w większości aplikacji.

Istnieje również problem z unieważnieniem wskaźnika po przeniesieniu wektorów - można to rozwiązać, wprowadzając taką strukturę:

struct assemblage_bucket {
    struct entity_watcher {
        assemblage_bucket* owner;
        entity_id real_index_in_vector;
    };

    std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;

    //...
};

Tak więc za każdym razem, gdy z jakiegoś powodu w naszej logice gry chcemy śledzić nowo utworzony byt, w wiadrze rejestrujemy byt_wglądnika bytu , a gdy byt musi być std :: move'd podczas usuwania, sprawdzamy jego obserwatorów i aktualizujemy ich real_index_in_vectordo nowych wartości. W większości przypadków wymusza to tylko jedno wyszukiwanie słownika dla każdego usunięcia encji.

Czy takie podejście ma jeszcze wady?

Dlaczego nigdzie nie wymieniono rozwiązania, mimo że jest dość oczywiste?

EDYCJA : Edytuję pytanie, aby „odpowiedzieć na odpowiedzi”, ponieważ komentarze są niewystarczające.

tracisz dynamiczną naturę elementów wtykowych, które zostały stworzone specjalnie po to, aby uciec od konstrukcji klasy statycznej.

Ja nie. Może nie wyjaśniłem tego wystarczająco jasno:

auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket

To tak proste, jak pobranie podpisu istniejącej jednostki, zmodyfikowanie jej i ponowne przesłanie jako nowej jednostki. Wtykowa, dynamiczna natura ? Oczywiście. Chciałbym tutaj podkreślić, że jest tylko jedna klasa „assemblage” i jedna klasa „bucket”. Wiadra są sterowane danymi i tworzone w czasie wykonywania w optymalnej ilości.

musisz przejrzeć wszystkie segmenty, które mogą zawierać prawidłowy cel. Bez zewnętrznej struktury danych wykrywanie kolizji może być równie trudne.

Właśnie dlatego mamy wyżej wspomniane zewnętrzne struktury danych . Obejście tego problemu jest tak proste, jak wprowadzenie iteratora w klasie System, który wykrywa, kiedy przeskoczyć do następnego segmentu. Skoki byłoby czysto przejrzyste dla logiki.

Patryk Czachurski
źródło
Przeczytałem również artykuł Randy'ego Gaula o przechowywaniu wszystkich komponentów w wektorach i pozwoliłem, aby ich systemy po prostu je przetwarzały. Widzę tam dwa duże problemy: co zrobić, jeśli chcę zaktualizować tylko podzbiór jednostek (na przykład wyrównywanie). Z tego powodu komponenty zostaną ponownie połączone z jednostkami. Dla każdego kroku iteracji komponentu musiałbym sprawdzić, czy jednostka, do której należy, została wybrana do aktualizacji. Drugi problem polega na tym, że niektóre systemy muszą przetwarzać wiele różnych typów komponentów, co ponownie pozbawia koherencję pamięci podręcznej. Wszelkie pomysły, jak poradzić sobie z tymi problemami?
tiguchi

Odpowiedzi:

7

Zasadniczo zaprojektowałeś system obiektów statycznych z alokatorem puli i klasami dynamicznymi.

Napisałem system obiektowy, który działa prawie identycznie z twoim systemem „zespołów” w czasach szkolnych, chociaż zawsze nazywam „zespoły” albo „planami”, albo „archetypami” w moich własnych projektach. Architektura bardziej bolała w tyłek niż naiwne systemy obiektowe i nie miała wymiernych korzyści w zakresie wydajności w porównaniu z bardziej elastycznymi projektami, z którymi ją porównywałem. Możliwość dynamicznego modyfikowania obiektu bez konieczności jego poprawiania lub ponownego przydzielania jest niezwykle ważna podczas pracy nad edytorem gier. Projektanci będą chcieli przeciągać i upuszczać komponenty na definicje obiektów. Kod wykonawczy może nawet wymagać efektywnej modyfikacji komponentów w niektórych projektach, chociaż osobiście mi się to nie podoba. W zależności od sposobu łączenia odniesień do obiektów w edytorze,

Będziesz uzyskiwać gorszą spójność pamięci podręcznej niż myślisz w większości nietrywialnych przypadkach. Twój system sztucznej inteligencji na przykład nie dba o Renderkomponenty, ale kończy się iteracją nad nimi jako częścią każdego bytu. Powtarzane obiekty są większe, a żądania w pamięci podręcznej kończą się zbieraniem niepotrzebnych danych, a przy każdym żądaniu zwracanych jest mniej całych obiektów). Nadal będzie lepszy niż metoda naiwna, a kompozycja obiektów metody naiwnej jest używana nawet w dużych silnikach AAA, więc prawdopodobnie nie potrzebujesz lepiej, ale przynajmniej nie myśl, że nie możesz jej dalej ulepszać.

Twoje podejście ma dla niektórych senskomponenty, ale nie wszystkie. Nie podoba mi się ECS, ponieważ zaleca się umieszczanie każdego komponentu w osobnym pojemniku, co ma sens w fizyce lub grafice, ale w ogóle nie ma sensu, jeśli pozwalasz na wiele komponentów skryptu lub składaną AI. Jeśli pozwolisz, aby system komponentowy był używany nie tylko do wbudowanych obiektów, ale również jako sposób na komponowanie zachowania obiektów przez projektantów i programistów rozgrywki, warto zgrupować wszystkie komponenty AI (które często będą oddziaływać) lub cały skrypt składniki (ponieważ chcesz je wszystkie zaktualizować w jednej partii). Jeśli chcesz mieć najbardziej wydajny system, będziesz potrzebować kombinacji schematów alokacji i przechowywania komponentów i poświęć czas na ostateczne ustalenie, który z nich jest najlepszy dla każdego konkretnego typu komponentu.

Sean Middleditch
źródło
Powiedziałem: nie możemy zmienić podpisu podmiotu, a miałem na myśli, że nie możemy go bezpośrednio zmodyfikować w miejscu, ale nadal możemy po prostu uzyskać istniejący zestaw do kopii lokalnej, wprowadzić zmiany i przesłać go ponownie jako nowy obiekt - i te operacje są dość tanie, jak pokazałem w pytaniu. Jeszcze raz - jest tylko JEDNA klasa „wiaderkowa”. „Zespoły” / „Podpisy” / „nazwijmy to, jak chcemy” można tworzyć dynamicznie w czasie wykonywania, ponieważ w standardowym podejściu posunęłbym się nawet do myślenia o bycie jako „podpisie”.
Patryk Czachurski,
I powiedziałem, że niekoniecznie chcesz poradzić sobie z reifikacją. „Utworzenie nowego elementu” może potencjalnie oznaczać zerwanie wszystkich istniejących uchwytów elementu, w zależności od tego, jak działa system uchwytów. Twoje połączenie, jeśli są wystarczająco tanie, czy nie. Uznałem, że to tylko ból w tyłku, z którym muszę sobie poradzić.
Sean Middleditch,
Okej, teraz mam twój punkt widzenia na ten temat. W każdym razie myślę, że nawet jeśli dodawanie / usuwanie było nieco droższe, zdarza się to tak okazjonalnie, że nadal warto znacznie uprościć proces dostępu do komponentów, co dzieje się w czasie rzeczywistym. Zatem koszty związane z „zmianą” są znikome. Jeśli chodzi o twój przykład sztucznej inteligencji, czy nadal nie jest wart tych kilku systemów, które i tak potrzebują danych z wielu komponentów?
Patryk Czachurski
Chodzi mi o to, że AI było miejscem, w którym twoje podejście jest lepsze, ale w przypadku innych komponentów niekoniecznie tak jest.
Sean Middleditch,
4

To, co zrobiłeś, to przeprojektowanie obiektów C ++. Powodem, dla którego wydaje się to oczywiste, jest to, że jeśli zamienisz słowo „encja” na „klasa”, a „komponent” na „element członkowski”, jest to standardowy projekt OOP z wykorzystaniem mixin.

1) tracisz dynamiczną naturę elementów wtykowych, które zostały stworzone specjalnie po to, aby uciec od konstrukcji klasy statycznej.

2) spójność pamięci jest najważniejsza w typie danych, a nie w obiekcie jednoczącym wiele typów danych w jednym miejscu. Jest to jeden z powodów, dla których stworzono systemy składowe +, aby uciec od fragmentacji pamięci klasy + obiektu.

3) ten projekt również powraca do stylu klasy C ++, ponieważ myślisz o bycie jako spójnym obiekcie, gdy w projekcie komponentu + systemu byt jest jedynie znacznikiem / identyfikatorem, aby uczynić wewnętrzne funkcjonowanie zrozumiałym dla ludzi.

4) szeregowanie siebie przez komponent jest tak samo łatwe jak serializowanie obiektu w szereg wielu komponentach, o ile nie jest łatwiejsze do śledzenia jako programista.

5) kolejnym logicznym krokiem w dół tej ścieżki jest usunięcie Systemów i umieszczenie tego kodu bezpośrednio w jednostce, w której znajdują się wszystkie dane potrzebne do działania. Wszyscy możemy zobaczyć, co to oznacza =)

Patrick Hughes
źródło
2) może nie rozumiem w pełni buforowania, ale powiedzmy, że istnieje system, który działa z powiedzmy 10 komponentami. W standardowym podejściu przetwarzanie każdej jednostki oznacza 10-krotny dostęp do pamięci RAM, ponieważ komponenty są rozproszone w losowych miejscach w pamięci, nawet jeśli pule są używane - ponieważ różne komponenty należą do różnych pul. Czy nie byłoby „ważne” jednoczesne buforowanie całej encji i przetwarzanie wszystkich składników bez pojedynczego braku pamięci podręcznej, nawet bez konieczności wyszukiwania w słowniku? Dokonałem również edycji, aby uwzględnić 1) punkt
Patryk Czachurski
@Sean Middleditch ma dobry opis tego podziału pamięci podręcznej w swojej odpowiedzi.
Patrick Hughes,
3) Nie są w żaden sposób obiektami spójnymi. O tym, że komponent A jest tuż obok komponentu B w pamięci, jest to po prostu „spójność pamięci”, a nie „logiczna spójność”, jak zauważył John. Wiadra, po ich stworzeniu, mogły nawet tasować komponenty w sygnaturze do dowolnej pożądanej kolejności, a zasady byłyby nadal przestrzegane. 4) „śledzenie” może być równie łatwe, jeśli mamy wystarczającą abstrakcję - mówimy tylko o schemacie przechowywania, który jest wyposażony w iteratory, a być może mapa przesunięć bajtów może ułatwić przetwarzanie tak jak w standardowym podejściu.
Patryk Czachurski
5) I nie sądzę, by cokolwiek w tym pomyśle wskazywało na ten kierunek. Nie chodzi o to, że nie chcę się z tobą zgodzić, po prostu jestem ciekawy, dokąd ta dyskusja może prowadzić, chociaż i tak prawdopodobnie doprowadzi to do pewnego rodzaju „zmierzenia” lub znanej „przedwczesnej optymalizacji”. :)
Patryk Czachurski
@PatrykCzachurski, ale twoje systemy nie działają z 10 komponentami.
user253751,
3

Utrzymywanie podobnych bytów razem nie jest tak ważne, jak mogłoby się wydawać, dlatego trudno jest wymyślić ważny powód inny niż „ponieważ jest to jednostka”. Ale ponieważ tak naprawdę robisz to dla spójności pamięci podręcznej w przeciwieństwie do spójności logicznej, może to mieć sens.

Jedną z trudności, jakie możesz mieć, są interakcje między komponentami w różnych segmentach. Na przykład nie jest łatwo znaleźć coś, na co twoja sztuczna inteligencja może strzelać, na przykład musisz przejrzeć wszystkie wiadra, które mogą zawierać prawidłowy cel. Bez zewnętrznej struktury danych wykrywanie kolizji może być równie trudne.

Aby kontynuować organizowanie jednostek razem w celu uzyskania logicznej spójności, jedynym powodem, dla którego mogłem być zmuszony do utrzymywania podmiotów razem, są cele identyfikacyjne w moich misjach. Muszę wiedzieć, czy właśnie utworzyłeś encję typu A lub typu B, i obejdę to przez ... zgadłeś: dodając nowy komponent, który identyfikuje zestawienie, które składa tę encję razem. Nawet wtedy nie zbieram wszystkich składników razem, aby wykonać wielkie zadanie, muszę tylko wiedzieć, co to jest. Więc nie sądzę, aby ta część była bardzo przydatna.

John McDonald
źródło
Muszę przyznać, że nie do końca rozumiem twoją odpowiedź. Co rozumiesz przez „logiczną spójność”? O trudnościach w interakcji zrobiłem edycję.
Patryk Czachurski
„Spójność logiczna”, jak w: „Logicznym sensem jest utrzymywanie wszystkich komponentów, które tworzą byt drzewa, blisko siebie.
John McDonald