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.