Porady dotyczące łączenia między systemem komponentów encji w C ++

10

Po przeczytaniu kilku dokumentów na temat systemu encji-komponentów postanowiłem wdrożyć mój. Do tej pory mam klasę World, która zawiera byty i zarządcę systemu (systemy), klasę Entity, która zawiera komponenty w postaci std :: map oraz kilka systemów. Trzymam byty jako std :: wektor w świecie. Jak dotąd żaden problem. To, co mnie dezorientuje, to iteracja bytów, nie mogę mieć w tym krystalicznie czystego umysłu, więc nadal nie mogę wdrożyć tej części. Czy każdy system powinien zawierać lokalną listę podmiotów, którymi są zainteresowani? A może powinienem po prostu iterować przez jednostki klasy światowej i utworzyć zagnieżdżoną pętlę, aby iterować przez systemy i sprawdzać, czy jednostka zawiera komponenty, którymi system jest zainteresowany? Mam na myśli :

for (entity x : listofentities) {
   for (system y : listofsystems) {
       if ((x.componentBitmask & y.bitmask) == y.bitmask)
             y.update(x, deltatime)
       }
 }

ale myślę, że system maski bitowej blokuje elastyczność w przypadku osadzenia języka skryptowego. Lub posiadanie list lokalnych dla każdego systemu zwiększy wykorzystanie pamięci dla klas. Jestem strasznie zmieszany.

deniz
źródło
Dlaczego oczekujesz, że podejście do maski bitowej utrudni powiązania skryptów? Na marginesie, używaj referencji (const, jeśli to możliwe) w pętlach dla każdego, aby uniknąć kopiowania jednostek i systemów.
Benjamin Kloster,
użycie maski bitowej na przykład int, pomieści tylko 32 różne komponenty. Nie sugeruję, że będzie więcej niż 32 elementy, ale co, jeśli mam? Będę musiał utworzyć inny int lub 64-bitowy int, nie będzie dynamiczny.
deniz 19.07.13
Możesz użyć std :: bitset lub std :: vector <bool>, w zależności od tego, czy chcesz, aby był dynamiczny w czasie wykonywania.
Benjamin Kloster,

Odpowiedzi:

7

Posiadanie lokalnych list dla każdego systemu zwiększy wykorzystanie pamięci dla klas.

To tradycyjny kompromis czasoprzestrzenny .

Podczas gdy iteracja po wszystkich obiektach i sprawdzanie ich podpisów odbywa się bezpośrednio do kodu, może stać się nieefektywna w miarę wzrostu liczby systemów - wyobraź sobie wyspecjalizowany system (niech to będzie dane wejściowe), który szuka prawdopodobnie jednego interesującego podmiotu spośród tysięcy niepowiązanych podmiotów .

To powiedziawszy, to podejście może być nadal wystarczająco dobre w zależności od twoich celów.

Chociaż, jeśli martwisz się szybkością, musisz oczywiście rozważyć inne rozwiązania.

Czy każdy system powinien zawierać lokalną listę podmiotów, którymi są zainteresowani?

Dokładnie. Jest to standardowe podejście, które powinno zapewnić przyzwoitą wydajność i jest stosunkowo łatwe do wdrożenia. Narzut pamięci jest moim zdaniem pomijalny - mówimy o przechowywaniu wskaźników.

Teraz, jak utrzymać te „listy zainteresowań”, może nie być tak oczywiste. Jeśli chodzi o kontener danych, std::vector<entity*> targetswystarczy klasa wewnętrzna systemu. Teraz robię to:

  • Podmiot jest pusty podczas tworzenia i nie należy do żadnego systemu.
  • Ilekroć dodam komponent do encji:

    • uzyskać bieżącą sygnaturę bitową ,
    • zamapuj rozmiar komponentu na pulę światową o odpowiedniej wielkości porcji (osobiście używam boost :: pool) i tam alokuj komponent
    • uzyskaj nową sygnaturę bitową encji (która jest po prostu „bieżącą sygnaturą bitową” plus nowy komponent)
    • iterację wszystkich systemów na świecie, a jeśli nie jest to system, którego podpis nie pasuje do aktualnej podpis podmiotu i nie pasuje do nowego podpisu, staje się oczywiste, powinniśmy push_back wskaźnik do naszej jednostki tam.

          for(auto sys = owner_world.systems.begin(); sys != owner_world.systems.end(); ++sys)
                  if((*sys)->components_signature.matches(new_signature) && !(*sys)->components_signature.matches(old_signature)) 
                          (*sys)->add(this);

Usuwanie bytu jest całkowicie analogiczne, z tą jedyną różnicą, którą usuwamy, jeśli system pasuje do naszego obecnego podpisu (co oznacza, że ​​byt tam był) i nie pasuje do nowego podpisu (co oznacza, że ​​byt nie powinien już tam być ).

Teraz możesz zastanawiać się nad użyciem std :: list, ponieważ usunięcie z wektora to O (n), nie wspominając już o tym, że będziesz musiał przesunąć dużą część danych za każdym razem, gdy usuwasz ze środka. W rzeczywistości nie musisz - ponieważ nie zależy nam na przetwarzaniu zamówienia na tym poziomie, możemy po prostu wywołać std :: remove i żyć z faktem, że przy każdym usunięciu musimy jedynie wykonać O (n) wyszukiwania naszego podmiot do usunięcia.

std :: list dałoby ci O (1) do usunięcia, ale z drugiej strony masz trochę dodatkowego narzutu pamięci. Pamiętaj również, że przez większość czasu będziesz przetwarzał byty, a nie je usuwałeś - a to z pewnością odbywa się szybciej za pomocą std :: vector.

Jeśli jesteś bardzo krytyczny pod względem wydajności, możesz rozważyć nawet inny wzorzec dostępu do danych , ale tak czy inaczej utrzymasz pewnego rodzaju „listy zainteresowań”. Pamiętaj jednak, że jeśli utrzymujesz wystarczająco abstrakcyjny interfejs API Entity System, nie powinno być problemu z poprawą metod przetwarzania encji systemowych, jeśli spadnie z tego powodu ilość klatek na sekundę - więc na razie wybierz metodę, która jest najłatwiejsza do zakodowania - tylko następnie profiluj i popraw w razie potrzeby.

Patryk Czachurski
źródło
5

Istnieje podejście, które warto rozważyć, gdy każdy system posiada komponenty powiązane ze sobą, a jednostki odnoszą się tylko do nich. Zasadniczo twoja (uproszczona) Entityklasa wygląda następująco:

class Entity {
  std::map<ComponentType, Component*> components;
};

Kiedy powiesz, że RigidBodyskładnik jest podłączony do Entity, żądasz go od swojego Physicssystemu. System tworzy komponent i pozwala bytowi utrzymywał do niego wskaźnik. Twój system wygląda wtedy następująco:

class PhysicsSystem {
  std::vector<RigidBodyComponent> rigidBodyComponents;
};

Teraz może to początkowo wydawać się sprzeczne z intuicją, ale zaletą jest sposób, w jaki systemy encji komponentowych aktualizują swój stan. Często będziesz iterować po swoich systemach i poprosić o aktualizację powiązanych komponentów

for(auto it = systems.begin(); it != systems.end(); ++it) {
  it->update();
}

Siła posiadania wszystkich składników należących do systemu w ciągłej pamięci polega na tym, że gdy system iteruje nad każdym komponentem i aktualizuje go, w zasadzie musi to zrobić

for(auto it = rigidBodyComponents.begin(); it != rigidBodyComponents.end(); ++it) {
  it->update();
}

Nie musi iterować wszystkich jednostek, które potencjalnie nie mają komponentu, który muszą zaktualizować, a także ma potencjał bardzo dobrej wydajności pamięci podręcznej, ponieważ wszystkie komponenty będą przechowywane w sposób ciągły. To jedna, jeśli nie największa zaleta tej metody. Często będziesz mieć setki i tysiące komponentów w danym momencie, równie dobrze możesz spróbować być tak wydajny, jak to możliwe.

W tym momencie Worldjedyne pętle przechodzą przez systemy i wywołują updateje bez potrzeby iteracji również encji. To (imho) lepszy projekt, ponieważ wtedy obowiązki systemów są o wiele jaśniejsze.

Oczywiście istnieje mnóstwo takich projektów, więc musisz dokładnie ocenić potrzeby swojej gry i wybrać najbardziej odpowiedni, ale jak widzimy tutaj, czasami małe szczegóły projektu mogą mieć znaczenie.

pwny
źródło
dobra odpowiedź, dzięki. ale komponenty nie mają funkcji (jak update ()), tylko dane. a system przetwarza te dane. więc zgodnie z twoim przykładem powinienem dodać wirtualną aktualizację dla klasy komponentu i wskaźnik encji dla każdego komponentu, mam rację?
deniz 19.07.13
@deniz Wszystko zależy od twojego projektu. Jeśli komponenty nie mają żadnych metod, a jedynie dane, system może nadal iterować nad nimi i wykonywać niezbędne działania. Jeśli chodzi o łączenie z powrotem z jednostkami, tak, możesz przechowywać wskaźnik do encji właściciela w samym komponencie lub zlecić systemowi utrzymywanie mapy między uchwytami komponentu a jednostkami. Zazwyczaj jednak chcesz, aby komponenty były jak najbardziej samodzielne. Komponent, który w ogóle nie wie o swojej jednostce nadrzędnej, jest idealny. Jeśli potrzebujesz komunikacji w tym kierunku, preferuj wydarzenia i tym podobne.
pwny
Jeśli powiesz, że będzie lepiej dla wydajności, użyję twojego wzoru.
deniz 19.07.2013
@deniz Upewnij się, że faktycznie profil kodu wcześnie i często, aby określić, co działa, a nie dla konkretnego engin :)
pwny
okej :) zrobię trochę testu warunków skrajnych
deniz
1

Moim zdaniem dobrą architekturą jest utworzenie warstwy komponentów w jednostkach i oddzielne zarządzanie każdym systemem w tej warstwie komponentów. Na przykład system logiczny ma pewne elementy logiczne, które wpływają na ich encję, i przechowują wspólne atrybuty, które są wspólne dla wszystkich komponentów w encji.

Następnie, jeśli chcesz zarządzać obiektami każdego systemu w różnych punktach lub w określonej kolejności, lepiej jest utworzyć listę aktywnych komponentów w każdym systemie. Wszystkie listy wskaźników, które można tworzyć w systemach i nimi zarządzać, to mniej niż jeden załadowany zasób.

superarce
źródło