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
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_vector
do 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.
źródło
Odpowiedzi:
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
Render
komponenty, 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.
źródło
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 =)
źródło
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.
źródło