2013-03-13 9 views
11

Pracuję nad systemem komponentów podmiotu dla silnika gry. Jednym z moich celów jest zastosowanie podejścia zorientowanego na dane w celu optymalnego przetwarzania danych. Innymi słowy, chcę podążać za wskazówkami raczej potrzebnymi strukturami tablic niż tablicami struktur. Jednak moim problemem jest to, że nie udało mi się wymyślić dla mnie tego rozwiązania.Dostęp zorientowany na dane do wielu indeksowanych macierzy danych

Mój dotychczasowy pomysł jest taki, że każdy element systemu jest odpowiedzialny za określoną część logiki gry, powiedzmy, że komponent grawitacji dba o obliczanie sił każdej klatki w zależności od masy, prędkości itp. I innych komponentów. innych rzeczy. Stąd każdy komponent jest zainteresowany różnymi zestawami danych. Komponent Gravity może być zainteresowany masą i szybkością, podczas gdy Komponent Kolizji może być zainteresowany ograniczeniem pola i pozycji itp.

Do tej pory pomyślałem, że mogę mieć menedżera danych, który zapisuje jedną tablicę dla każdego atrybutu. Powiedzmy, że jednostki mogą mieć jeden lub więcej ciężarów, pozycji, prędkości itp. I będą miały unikalny identyfikator. Dane w menedżerze danych byłyby reprezentowane przez następujące gdzie każda liczba reprezentuje identyfikator jednostki:

weightarray -> [0,1,2,3] 
positionarray -> [0,1,2,3] 
velocityarray -> [0,1,2,3] 

Podejście to działa dobrze, gdyby wszystkie jednostki mają każdy z atrybutów. Jednak jeśli tylko jednostka 0 i 2 posiada wszystkie atrybuty drzewa i inne podmioty te są tego rodzaju, że nie porusza się, nie będą one miały prędkość i dane będzie wyglądać:

weightarray -> [0,1,2,3] 
positionarray -> [0,1,2,3] 
velocityarray -> [0,2]  //either squash it like this 
velocityarray -> [0 ,2 ]  //or leave "empty gaps" to keep alignment 

Nagle to nie jest tak łatwe, iterować przez to. Komponent zainteresowany jedynie iterowaniem i manipulowaniem prędkością musiałby albo w jakiś sposób pominąć puste luki, gdybym podszedł do drugiego podejścia. Pierwsze podejście polegające na utrzymywaniu krótkiego szyku nie sprawdzi się dobrze ani w bardziej skomplikowanych sytuacjach. Powiedz, jeśli mam jedną jednostkę 0 ze wszystkimi trzema atrybutami, inną jednostkę 1 mającą tylko wagę i pozycję oraz jednostkę 2, która ma tylko pozycję i prędkość. Wreszcie istnieje jedna ostatnia istota 3, która ma tylko wagę. Macierze zgniecione wyglądałby następująco:

weightarray -> [0,1,3] 
positionarray -> [0,1,2] 
velocityarray -> [0,2] 

Inne podejście byłoby zostawić luki tak:

weightarray -> [0,1, ,3] 
positionarray -> [0,1,2, ] 
velocityarray -> [0, ,2, ] 

Obie te sytuacje są nieszablonowe iteracyjne jeśli jesteś zainteresowany tylko w iteracji po zbiorze podmiotów ma tylko kilka atrybutów. Dany komponent X byłby zainteresowany np. Przetwarzaniem jednostek z pozycją i prędkością. W jaki sposób mogę wyodrębnić iterowalne wskaźniki tablicowe, aby nadać temu komponentowi jego obliczenia? Chciałbym nadać mu tablicę, w której elementy są tuż obok siebie, ale wydaje się to niemożliwe.

Zastanawiam się nad rozwiązaniami, takimi jak posiadanie pola bitowego dla każdej tablicy, opisywanie, które plamy są poprawne i które są lukami, lub systemu, który kopiuje dane do tymczasowych tablic, które nie mają dziur i są następnie przekazywane do komponenty i inne pomysły, ale żadna z tych, które uważałem za eleganckie i nie miała dodatkowych kosztów związanych z przetwarzaniem (takich jak dodatkowe sprawdzenie, czy dane są poprawne lub dodatkowe kopiowanie danych).

Proszę o to tutaj, ponieważ mam nadzieję, że ktoś z was może mieć doświadczenie z czymś podobnym lub może mieć pomysły lub przemyślenia pomocne w realizacji tego problemu. :) Także, jeśli ten cały pomysł jest bzdurny i niemożliwy do uzyskania, a ty masz o wiele lepszy pomysł, proszę powiedz mi. Mam nadzieję, że pytanie nie jest zbyt długie i nie jest zbyt głośne.

Dzięki.

+0

wybierz konkretną wartość, jak -1, aby przedstawić lukę. – tp1

+0

@ tp1, który działa, gdy przechowujesz pewne wartości, na przykład int dla punktów zdrowia na przykład. Jednak jeśli przechowujesz prędkość lub pozycję lub inne wartości za pomocą wartości zmiennoprzecinkowych lub atrybutów, które mogą mieć wartość 0 i wartość ujemną, to podejście nie działa. – Tobias

+0

@ tp1 - i dlatego mamy wartości zerowe – Duniyadnd

Odpowiedz

2

Zamiast zajmować się strukturą danych, chciałbym przedstawić perspektywę, jak robiłem takie rzeczy w przeszłości.

Silnik gry zawiera listę menedżerów odpowiedzialnych za różne systemy w grze (InputManager, PhysicsManager, RenderManager, etc ...).

Większość rzeczy w świecie 3D reprezentowana jest przez klasę Object, a każdy obiekt może mieć dowolną liczbę komponentów. Każdy komponent odpowiada za różne aspekty zachowania obiektu (RenderComponent, PhysicsComponent, itp.).

Komponent fizyki był odpowiedzialny za ładowanie siatki fizyki i nadawanie jej wszystkich niezbędnych właściwości, takich jak masa, gęstość, środek masy, dane odpowiedzi na bezwładność i inne. Ten komponent zawierał również informacje o modelu fizyki, który kiedyś istniał na świecie, jak pozycja, obrót, prędkość liniowa, prędkość kątowa i inne.

The PhysicsManager posiadał wiedzę o każdej siatce fizyki, która została załadowana przez dowolne komponenty fizyczne, co pozwoliło menedżerowi obsłużyć wszystkie zadania związane z fizyką, takie jak wykrywanie kolizji, wysyłanie wiadomości o zderzeniach, wykonywanie rzutów fizyki.

Jeśli potrzebowalibyśmy wyspecjalizowanych zachowań, których potrzebowaliby tylko nieliczne obiekty, utworzylibyśmy dla niego komponent i zmusilibyśmy ten komponent do manipulowania danymi takimi jak prędkość lub tarcie, a te zmiany byłyby widoczne przez PhysicsManager i uwzględnione w symulacja fizyki.

Jeśli chodzi o strukturę danych, możesz mieć system, o którym wspomniałem powyżej i zorganizować go na kilka sposobów. Zasadniczo Obiekty przechowywane są w Wektorze lub Mapie, a Komponenty w Wektorze lub Liście w Obiekcie. Jeśli chodzi o informacje fizyki, PhysicsManager ma listę wszystkich obiektów fizycznych, które mogą być przechowywane w tablicy/wektorze, a PhysicsComponent ma kopię swojej pozycji, prędkości i innych danych, dzięki czemu może zrobić wszystko, musi mieć te dane zmanipulowane przez menedżera fizyki. Na przykład, jeśli chcesz zmienić prędkość obiektu, który właśnie powiedziałeś PhysicsComponent, zmieniłby on jego wartość prędkości, a następnie powiadomi PhysicsManager.

rozmawiam więcej na ten temat konstrukcji silnika obiekt/składnika tutaj: https://gamedev.stackexchange.com/a/23578/12611

3

Rozwiązaniem jest faktycznie przyjmując, że istnieją granice, jak daleko można zoptymalizować.

Rozwiązanie problemu luki spowoduje jedynie następujących zostać wprowadzone:

  • if (oddziały), aby obsłużyć wyjątki danych (podmioty, które są brakującym elementem).
  • Wprowadzanie otworów oznacza, że ​​możesz również losowo przeglądać listy. Moc DoD polega na tym, że wszystkie dane są ściśle spakowane i uporządkowane w taki sposób, w jaki będą przetwarzane.

Co możesz zrobić:

Tworzenie różnych list zoptymalizowanych dla różnych systemów/przypadkach. Każda klatka: kopiuje właściwości z jednego systemu do drugiego tylko dla encji, które tego wymagają (które mają ten konkretny komponent).

uwzględniając następujące uproszczone wykazy i ich atrybuty:

  • bryła sztywna (siła, szybkość, przekształcać)
  • kolizja (boundingbox, przekształcać)
  • odkształcalne (texture_id, shader_id , przekształcenie)
  • rigidbody_to_collision (rigidbody_index, collision_index)
  • collision_to_rigidbody (collision_index, rigidbody_index)
  • rigidbody_to_drawable (rigidbody_index, drawable_index)

etc ...

dla procesów/Praca może chcesz następujące:

  • RigidbodyApplyForces (...), zastosuj siły (np. grawitacja) do prędkości obrotowej
  • Sztywny korpusIntegrate (...), stosuje prędkości do transformacji.
  • RigidbodyToCollision (...), kopia bryły sztywnej przekształca się w transformację kolizyjną tylko dla obiektów, które mają komponent kolizji. Lista "rigidbody_to_collision" zawiera indeksy, które powinny być skopiowane ID kontenera, do którego należy identyfikacja kolizji. Dzięki temu lista kolizji jest szczelnie zapakowana.
  • RigidbodyToDrawable (...), kopiowanie rigidbody przekształca się w transformacje do rysowania dla elementów, które mają komponent do rysowania. Lista "rigidbody_to_drawable" zawiera indeksy, które powinny być skopiowane ID kontenera, do którego można narysować ID. Dzięki temu lista drawabkl jest mocno zapakowana.
  • CollisionUpdateBoundingBoxes (...), zaktualizuj pola ograniczające za pomocą nowych transformacji.
  • CollisionRecalculateHashgrid (...), aktualizacja hashgrid przy użyciu pól ograniczających. Możesz wykonać ten podział na kilka ramek, aby rozdzielić obciążenie.
  • CollisionBroadphaseResolve (...), obliczyć ewentualne kolizje z użyciem hashgrid itp ....
  • CollisionMidphaseResolve (...), obliczyć kolizji przy użyciu obwiedni dla broadphase itp ....
  • CollisionNarrowphaseResolve (...), obliczanie kolizji za pomocą wielokątów z fazy środkowej itd. ...
  • CollisionToRigidbody (...), dodaj siły reaktywne zderzających się obiektów z siłami sztywnego ciała. Lista "collision_to_rigidbody" zawiera indeksy, z których identyfikatory kolizji powinny zostać dodane, do którego ID rigidbody. Możesz również utworzyć kolejną listę o nazwie "reactive_forces_to_be_added". Możesz użyć tego do opóźnienia dodania sił.
  • RenderDrawable (...), renderuj rysunki do ekranu (renderowanie jest tylko uproszczone).

Oczywiście potrzebujesz dużo więcej procesów/zadań. Prawdopodobnie chcesz zamknąć i posortować rysunki, dodać system wykresów transformacyjnych między fizyką a rysunkami (zobacz prezentację firmy Sony na temat tego, jak możesz to zrobić) itd. Wykonanie zadań może być wykonywane w wielu rdzeniach. Jest to bardzo łatwe, gdy wszystko jest tylko listą, ponieważ można je podzielić na wiele list.

Podczas tworzenia obiektu dane komponentu będą również tworzone razem i przechowywane w tej samej kolejności. Oznacza to, że listy pozostaną w większości w tej samej kolejności.

W przypadku procesów "kopiowania obiektu do obiektu". Jeśli pomijanie otworów naprawdę staje się problemem, zawsze można utworzyć proces "ponownego porządkowania obiektów", który na końcu każdej klatki, rozdzielony na wiele ramek, uporządkuje obiekty w najbardziej optymalną kolejność. Kolejność, która wymaga najmniejszego pomijania otworów. Pomijanie dziur jest ceną, jaką trzeba zapłacić, aby wszystkie listy były tak ciasno spakowane, jak to tylko możliwe, a także pozwala je zamówić w taki sposób, w jaki będzie przetwarzane.

10

Dobre pytanie. Jednak, o ile mogę powiedzieć, nie ma prostego rozwiązania tego problemu. Istnieje wiele rozwiązań (niektóre z nich wspomniałeś), ale nie widzę natychmiastowego rozwiązania srebrnej kuli.

Najpierw spójrzmy na cel. Celem nie jest umieszczenie wszystkich danych w liniowych tablicach, które są jedynie środkiem do osiągnięcia celu. Celem jest optymalizacja wydajności poprzez zminimalizowanie chybienia pamięci podręcznej. To wszystko. Jeśli korzystasz z obiektów OOP, dane Twoich jednostek zostaną otoczone danymi, których nie potrzebujesz. Jeśli twoja architektura ma rozmiar linii pamięci podręcznej 64 bajty i potrzebujesz tylko wagi (float), position (vec3) i velocity (vec3), używasz 28 bajtów, ale pozostałe 36 bajtów i tak zostanie załadowane. Jeszcze gorzej, gdy te 3 wartości nie są w pamięci obok siebie lub struktura danych pokrywa się z granicą linii pamięci podręcznej, załadujesz wiele linii pamięci podręcznej tylko na 28 bajtów faktycznie używanych danych.

To nie jest takie złe, gdy robisz to kilka razy. Nawet jeśli robisz to sto razy, prawie tego nie zauważysz. Jednak jeśli robisz to tysiące razy na sekundę, może to stanowić problem. Wejdź więc do DOD, gdzie optymalizujesz wykorzystanie pamięci podręcznej, zwykle tworząc liniowe tablice dla każdej zmiennej, w sytuacjach, w których istnieją liniowe wzorce dostępu. W twoim przypadku tablice masy, pozycji, prędkości. Po załadowaniu pozycji jednej encji ponownie ładujemy 64 bajty danych. Ale ponieważ twoje dane pozycji są obok siebie w tablicy, nie ładujesz 1 wartości pozycji, ładujesz dane dla 5 sąsiednich obiektów. Następna iteracja pętli aktualizacji będzie prawdopodobnie wymagać następnej wartości pozycji, która była już załadowana do pamięci podręcznej, i tak dalej, aż tylko do szóstej jednostki będzie musiała załadować nowe dane z pamięci głównej.

Celem DOD nie jest wykorzystanie tablic liniowych, ale maksymalizacja wykorzystania pamięci podręcznej poprzez umieszczenie danych dostępnych w (około) tym samym czasie sąsiadującym z pamięcią. Jeśli prawie zawsze uzyskujesz dostęp do 3 zmiennych w tym samym czasie, nie musisz tworzyć 3 tablic dla każdej zmiennej, możesz równie łatwo utworzyć strukturę, która zawiera tylko te 3 wartości i utworzyć tablicę tych struktur. Najlepsze rozwiązanie zawsze zależy od sposobu korzystania z danych. Jeśli twoje wzorce dostępu są liniowe, ale nie zawsze używasz wszystkich zmiennych, idź do oddzielnych macierzy liniowych. Jeśli twoje wzorce dostępu są bardziej nieregularne, ale zawsze używasz wszystkich zmiennych w tym samym czasie, umieść je w strukturze razem i utwórz tablicę tych struktur.

Twoja odpowiedź jest krótka: wszystko zależy od wykorzystania danych. Z tego powodu nie mogę bezpośrednio odpowiedzieć na twoje pytanie.Mogę dać ci kilka pomysłów na to, jak sobie poradzić z twoimi danymi, i sam możesz zdecydować, który byłby najbardziej przydatny (jeśli którykolwiek z nich jest) w twojej sytuacji, a może możesz go dostosować/zmylić.

Można zachować najbardziej dostępne dane w ciągłej tablicy. Na przykład pozycja jest często używana przez wiele różnych komponentów, więc jest to główny kandydat do ciągłej tablicy. Z drugiej strony waga jest używana tylko przez komponent grawitacyjny, więc tutaj mogą występować przerwy. Zoptymalizowano pod kątem najczęściej używanego przypadku i uzyskasz mniejszą wydajność w przypadku danych, które są rzadziej używane. Nadal nie jestem wielkim fanem tego rozwiązania z wielu powodów: jest wciąż nieefektywny, załadujesz zbyt dużo pustych danych, im niższy stosunek # określonych komponentów/# całkowitych elementów, tym gorzej. Jeśli tylko jedna na osiem jednostek ma komponenty grawitacji, a te jednostki są rozmieszczone równomiernie w macierzach, nadal otrzymuje się jedną brakującą pamięć podręczną dla każdej aktualizacji. Zakłada również, że wszystkie jednostki będą miały pozycję (lub cokolwiek innego, co jest wspólną zmienną), trudno jest dodawać i usuwać byty, jest nieelastyczne i brzydkie (w każdym razie). To może być jednak najłatwiejsze rozwiązanie.

Innym sposobem rozwiązania tego problemu jest użycie indeksów. Każda tablica dla komponentu zostanie spakowana, ale istnieją dwie dodatkowe tablice, jedna do uzyskania identyfikatora jednostki z indeksu tablicy komponentów, a druga do uzyskania indeksu tablicy komponentów z identyfikatora jednostki. Powiedzmy, że pozycja jest dzielona przez wszystkie jednostki, podczas gdy ciężar i prędkość są używane tylko przez Grawitację. Możesz teraz wykonywać iteracje na spakowanych tablicach wagi i prędkości, a aby uzyskać/ustawić odpowiednią pozycję, możesz uzyskać wartość istotności grawitacji -> entityID, przejść do komponentu Position, użyć jej entityID -> positionIndex, aby uzyskać poprawny indeks w Tablica pozycji. Zaletą jest to, że dostęp do waszej wagi i prędkości nie będzie już powodować braków w pamięci podręcznej, ale nadal otrzymacie pomyłki w pamięci podręcznej dla pozycji, jeśli stosunek składników # grawitacji/# elementów pozycji jest niski. Otrzymasz również dodatkowe 2 wyszukiwania tablicowe, ale 16-bitowy indeks unsigned int powinien wystarczyć w większości przypadków, więc te tablice będą ładnie pasować do pamięci podręcznej, co oznacza, że ​​w większości przypadków może to nie być bardzo kosztowna operacja. Nadal profil profilu, aby być tego pewnym!

Trzecią opcją jest powielanie danych. Teraz jestem pewien, że nie będzie to warte wysiłku w przypadku twojego komponentu Gravity, myślę, że jest to bardziej interesujące w ciężkich sytuacjach obliczeniowych, ale weźmy to jako przykład. W tym przypadku komponent Gravity ma 3 upakowane tablice określające wagę, prędkość i położenie. Ma również podobną tabelę indeksu do tego, co widziałeś w drugiej opcji. Po uruchomieniu aktualizacji komponentu Gravity najpierw aktualizujesz tablicę pozycji z oryginalnej tablicy pozycji w elemencie Position, używając tabeli indeksów jak w przykładzie 2. Teraz masz 3 upakowane tablice, które możesz wykonywać obliczenia liniowo z maksymalną pamięcią podręczną wykorzystanie. Po zakończeniu skopiuj położenie z powrotem do oryginalnego komponentu Pozycja za pomocą tabeli indeksów. Teraz nie będzie to szybsze (w rzeczywistości prawdopodobnie wolniejsze) niż druga opcja, jeśli użyjesz go do czegoś takiego jak Gravity, ponieważ tylko raz czytasz i piszesz pozycję. Załóżmy jednak, że masz komponent, w którym jednostki współdziałają ze sobą, przy czym każda aktualizacja wymaga wielu odczytów i zapisów, może być szybsza. Jednak wszystko zależy od wzorców dostępu.

Ostatnią opcją, którą wymienię, jest system oparty na zmianach. Możesz łatwo dostosować to do systemu komunikacyjnego. W takim przypadku aktualizujesz tylko te dane, które zostały zmienione. W twoim komponencie Gravity większość obiektów będzie leżała na podłodze bez zmian, ale kilka z nich spada. Komponent Gravity ma upakowane tablice określające pozycję, prędkość, masę. Jeśli pozycja jest aktualizowana podczas pętli aktualizacji, dodaje się identyfikator jednostki i nową pozycję do listy zmian. Po zakończeniu przesyłasz te zmiany do dowolnego innego komponentu, który zachowuje wartość pozycji. Ta sama zasada, jeśli jakikolwiek inny komponent (na przykład komponent kontrolujący gracza) zmienia pozycję, przesyła nowe pozycje zmienionych podmiotów, komponent Gravity może go odsłuchiwać i aktualizować tylko te pozycje w swojej tablicy pozycji. Będziesz duplikować wiele danych, tak jak w poprzednim przykładzie, ale zamiast odnawiać wszystkie dane w każdym cyklu aktualizacji, aktualizujesz dane tylko po ich zmianie. Bardzo przydatne w sytuacjach, w których niewielkie ilości danych faktycznie zmieniają każdą klatkę, ale mogą stać się nieskuteczne, jeśli zmienią się duże ilości danych.

Więc nie ma srebrnej kuli. Istnieje wiele opcji. Najlepsze rozwiązanie zależy całkowicie od Twojej sytuacji, od twoich danych i sposobu ich przetwarzania. Może żaden z podanych przykładów nie jest dla ciebie odpowiedni, może wszystkie z nich są. Nie każdy komponent musi działać w taki sam sposób, niektórzy mogą korzystać z systemu zmiany/wiadomości, podczas gdy inni korzystają z opcji indeksów. Pamiętaj, że chociaż wiele wskazówek dotyczących wydajności DOD jest świetnych, jeśli potrzebujesz wydajności, przydaje się tylko w określonych sytuacjach. DOD nie polega na tym, by zawsze używać tablic, nie chodzi o maksymalizację wykorzystania pamięci podręcznej, powinieneś robić to tylko wtedy, gdy ma to naprawdę znaczenie. Profil profilu profilu. Poznaj swoje dane. Poznaj wzorce dostępu do danych. Znaj swoją architekturę (pamięć podręczną). Jeśli to wszystko zrobisz, rozwiązania staną się oczywiste, gdy będziesz o tym mówił :)

Mam nadzieję, że to pomoże!

2

Opieram się na dwóch strukturach dla tego problemu. Mam nadzieję, że schematy są wystarczająco jasne (mogę dodać dalszych wyjaśnień tego inaczej):

enter image description here

Rzadki tablica pozwala nam powiązać dane równolegle do drugiego bez wyginanie się zbyt dużo pamięci z nieużywanych indeksów i bez pogarszania miejscowości przestrzennego w ogóle (ponieważ każdy blok przechowuje kilka elementów w sposób ciągły).

Możesz użyć mniejszego rozmiaru bloku niż 512, ponieważ może on być dość duży dla danego typu komponentu. Coś jak 32 może być rozsądne lub możesz dostosować rozmiar bloku w locie w oparciu o sizeof(ComponentType). Dzięki temu możesz po prostu powiązać swoje komponenty równolegle z twoimi jednostkami bez nadmiernego obciążania pamięci za dużo z niezajętej przestrzeni, ale nie używam jej w ten sposób (używam reprezentacji typu pionowego, ale mój system ma wiele typów komponentów - - jeśli masz tylko kilka, możesz po prostu przechowywać wszystko równolegle).

Potrzebujemy jednak innej struktury podczas iteracji, aby dowiedzieć się, które wskaźniki są zajęte. Nie używam hierarchiczną bitset (I miłosny i skorzystać z tej struktury danych dużo, ale nie wiem, czy istnieje formalna nazwa dla niego, ponieważ jest to po prostu coś, co zrobiłem, nie wiedząc, jak to się nazywa):

enter image description here

Umożliwia to dostęp do elementów, które są zajęte, zawsze w kolejności (podobnie jak w przypadku sortowanych indeksów). Ta struktura jest niezwykle szybka dla sekwencyjnej iteracji, ponieważ testowanie pojedynczego bitu może wskazywać, że milion przylegających elementów może być przetwarzanych bez sprawdzania miliona bitów lub konieczności przechowywania i dostępu do miliona wskaźników do kontenera.

Jako bonus umożliwia także ustawienie skrzyżowań w najlepszym scenariuszu Log(N)/Log(64) (np. Możliwość znalezienia ustawionego przecięcia między dwoma gęstymi zestawami indeksów zawierającymi miliony elementów w 3-4 iteracjach) jeśli kiedykolwiek będziesz potrzebował szybkich skrzyżowań, które często mogą być bardzo przydatne dla ECS.

Te dwie struktury są trzonami mojego silnika ECS. Są dość szybkie, ponieważ mogę przetworzyć 2 miliony cząstek (uzyskując dostęp do dwóch różnych składników) bez buforowania zapytania o elementy z obydwoma komponentami po zaledwie 30 FPS. Oczywiście, że jest to grubiańska liczba klatek na sekundę dla zaledwie 2 milionów cząstek, ale wtedy reprezentujemy je jako całe byty z dwoma komponentami połączonymi ze sobą (ruch i sprite) z układem cząsteczek wykonującym zapytanie co pojedynczą ramkę, bez pamięci - coś, co ludzie normalnie nigdy nie będą (lepiej użyć komponentu ParticleEmitter, który reprezentuje wiele cząsteczek dla danej jednostki, zamiast tworzyć cząstkę jako oddzielną całość).

Mam nadzieję, że diagramy są wystarczająco jasne, aby zaimplementować własną wersję, jeśli jesteś zainteresowany.

Powiązane problemy