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.
źródło
Odpowiedzi:
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.
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*> targets
wystarczy klasa wewnętrzna systemu. Teraz robię to:Ilekroć dodam komponent do encji:
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.
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.
źródło
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)
Entity
klasa wygląda następująco:Kiedy powiesz, że
RigidBody
składnik jest podłączony doEntity
, żądasz go od swojegoPhysics
systemu. System tworzy komponent i pozwala bytowi utrzymywał do niego wskaźnik. Twój system wygląda wtedy następująco: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
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ć
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
World
jedyne pętle przechodzą przez systemy i wywołująupdate
je 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.
źródło
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.
źródło