Praca z dużą ilością kostek. Poprawiasz wydajność?

12

Edycja: Podsumowując pytanie, mam świat oparty na wokselach (styl Minecraft (Dzięki Communist Duck)), który cierpi z powodu słabej wydajności. Nie jestem pozytywny co do źródła, ale chciałbym uzyskać wszelkie porady, jak się go pozbyć.

Pracuję nad projektem, w którym świat składa się z dużej liczby kostek (dałbym wam liczbę, ale są to światy zdefiniowane przez użytkownika). Mój testowy to około (48 x 32 x 48) bloków.

Zasadniczo te bloki same w sobie nic nie robią. Po prostu tam siedzą.

Zaczynają być używane, gdy chodzi o interakcję gracza.

Muszę sprawdzić, z jakimi sześcianami wchodzi interakcja myszy użytkownika (najechanie myszką, kliknięcie itp.) Oraz wykrywanie kolizji podczas ruchu gracza.

Teraz na początku miałem ogromne opóźnienie, przechodząc przez każdy blok.

Udało mi się zmniejszyć to opóźnienie, zapętlając wszystkie bloki i znajdując, które bloki znajdują się w określonym zakresie postaci, a następnie zapętlając tylko te bloki w celu wykrycia kolizji itp.

Nadal jednak mam depresyjne 2 kl./s.

Czy ktoś ma jakieś pomysły na to, jak mogę zmniejszyć to opóźnienie?

Btw, używam XNA (C #) i tak, to jest 3d.

Joel
źródło
Masz na myśli grę typu Minecraft? To znaczy woksel?
Kaczka komunistyczna
4
Czy szukałeś oktetów? en.wikipedia.org/wiki/Octree
bummzack
1
Czy próbowałeś profilować swoją grę? Może pokazywać niektóre kluczowe obszary, w których spędza się najwięcej czasu. Może nie być tak, jak myślisz.
zwalniał, w
2
Zamiast narysować wszystkie 6 ścian każdej kostki, możesz tylko narysować twarze, które nie mają z nimi kontaktu
David Ashmore
1
@David: Tak, inaczej mógłby przestać wykonywać pojedyncze sprawdzenie losowania na kostkę, a później martwić się o poszczególne wielokąty.
Olhovsky

Odpowiedzi:

20

Wygląda na to, że chcesz się dowiedzieć o drzewach!

I mówię poważnie: jeśli obecnie zapętlasz tablicę wszystkich swoich kostek, naprawdę powinieneś przyjrzeć się różnym strukturom danych przestrzennych. W tym przypadku najlepszym sposobem na ponowne wyobrażenie sobie świata kostki jest drzewo.

Zanim przejdziemy do przyczyn, zastanówmy się nad naszym problemem. Szukamy rozwiązania, w którym przy możliwie jak najmniejszych kosztach możemy pobrać listę pobliskich kostek, z którymi gracz może się kolidować. Ta lista powinna być jak najmniejsza, ale jak najbardziej precyzyjna.

Teraz, aby określić tę strefę, musimy zmapować przestrzeń współrzędnych naszego gracza na przestrzeń współrzędnych mapy sześcianu; oznacza to, że musimy zamapować pozycję zmiennoprzecinkową odtwarzacza na dyskretny indeks wielowymiarowej tablicy kostek (przykładową notacją może być world[31][31][31], tj. dokładny środek dla tablicy wielowymiarowej 64 * 64 * 64).

Możemy po prostu obliczyć otaczające bloki przy użyciu tego samego dyskretnego indeksowania, być może próbkując tylko pobliskie kostki, ale nadal wymaga to ciągłego przeliczania i nie pozwala na obiekty, które nie są dyskretne w rozmieszczeniu (tj. Mogą nie być odwzorowane na kostkę mapa).

Idealną sytuacją jest zestaw wiader, które zawierają nasze zestawy kostek dla poszczególnych odcinków naszej mapy kostek, podzielone równo, więc zamiast ponownie obliczać otaczający obszar, po prostu wchodzimy i wychodzimy z tych stref . W przypadku wszelkich nietrywialnych obliczeń przechowywanie naszych danych w ten sposób może wyeliminować iterację wszystkich kostek i tylko tych pojedynczych zestawów, które są w pobliżu.

Pytanie brzmi: jak to zrealizować?

Dla świata 64 * 64 * 64 wyobraź sobie, że jest on podzielony na 8 * 8 * 8 stref . Oznacza to, że w twoim świecie będziesz miał 8 stref na oś (X, Y, Z). Każda z tych stref będzie zawierać 8 kostek, które można łatwo odzyskać dzięki temu nowemu uproszczonemu indeksowi.

Jeśli musisz wykonać operację na zestawie pobliskich kostek, zamiast iterować każdą kostkę w twoim świecie, możesz po prostu iterować po tych strefach , dzieląc maksymalną liczbę iteracji z oryginalnej 64 * 64 * 64 (262144) na tylko 520 (8 * 8 * 8 + 8).

Teraz pomniejszyć z tego świata stref i umieścić stref do większych Super strefach ; przy czym każda superstrefa zawiera 2 * 2 * 2 regularne strefy . Ponieważ twój świat zawiera obecnie 512 (8 * 8 * 8) stref , możemy podzielić 8 * 8 * 8 stref na 64 super-strefy (4 * 4 * 4) , dzieląc 8 stref przez 2 strefy na super-strefę . Zastosowanie tej samej logiki z góry spowodowałoby przekroczenie maksymalnej liczby iteracji z 512 na 8 w celu znalezienia superstrefy ; a następnie maksymalnie 64, aby znaleźć strefę postępowania(łącznie maks. 72)! Możesz zobaczyć, jak oszczędza to już wiele iteracji (262144: 72).

Jestem pewien, że teraz widzisz, jak przydatne są drzewa. Każda strefa jest gałęzią drzewa, a każda superstrefa jako gałąź poprzednia. Po prostu przemierzasz drzewo, aby znaleźć to, czego potrzebujesz; używając mniejszych zestawów danych, aby zminimalizować całkowity koszt

Poniższy schemat powinien pomóc w wizualizacji koncepcji. (zdjęcie z Wikipedii: Octrees ): Octree

Zrzeczenie się:

W idealnej konfiguracji, jak powyżej, w której świat wokseli jest już ułożony w wielowymiarowej tablicy o stałym rozmiarze, możesz po prostu sprawdzić pozycję gracza, a następnie indeksować otaczające bloki kosztem O (1)! (Zobacz wyjaśnienie Olhovskysa) Ale staje się to trudniejsze, gdy zaczniesz rozważać, że twój świat rzadko ma ustaloną wielkość w grze wokselowej; i może być konieczne, aby struktura danych mogła ładować całe superstrefy z dysku twardego do pamięci. W przeciwieństwie do wielowymiarowej tablicy o stałym rozmiarze, drzewa pozwalają na to bez zbytniego czasu poświęcanego na algorytmy kombinacyjne.

spowolniony
źródło
Powiedziałbym, że rozumiem, ale niestety nie rozumiem. Skrzynki kolizyjne bloków się nie poruszają, tylko skrzynka kolizyjna gracza. I mam teraz metodę, która (bez przechodzenia przez wszystkie bloki) zwraca wszystkie bloki, które znajdują się w promieniu 5 bloków od gracza. Przepraszamy za kłopot, ale jakieś wyjaśnienie? Nawiasem mówiąc, aby uprościć, czy możesz założyć, że świat to 64 x 64 x 64?
Joel
Nadal otrzymuję około 5 klatek na sekundę :(
Joel
Przeredagowałem swoją odpowiedź, daj mi znać, czy to pomogło.
spowolniony, w
Więc zawężam bloki, w których może być gracz, używając tej techniki oktawy. Jestem pewien, że to rozumiem. Ale czy sugerujesz, żebym użył mojego wykrywania kolizji, kiedy zawęziłem go do niewielkiej liczby bloków, czy masz inną metodę?
Joel
2
O ile gracz nie jest bardzo duży w stosunku do wielkości kostek, sprawdzenie kolizji wokół gracza jest prawdopodobnie najszybszym sposobem. Jeśli gracz zajmuje na przykład nie więcej miejsca niż jedną kostkę, musi tylko sprawdzić 27 otaczających kostek, aby znaleźć kolizję. Nie wymaga to drzewa, ponieważ można indeksować bezpośrednio do tych lokalizacji kostek, zakładając, że przechowuje on kostki w tablicy, w której może indeksować, z jednym miejscem przydzielonym dla każdej możliwej lokalizacji kostki.
Olhovsky
13

Zgadzam się z odpowiedzią Daniela, że ​​iteracja przez duże ilości pudeł jest najbardziej prawdopodobną przyczyną, i że dzięki partycjonowaniu przestrzennemu możesz znacznie przyspieszyć grę - ale problem może być także gdzie indziej i możesz marnować swój czas .

Aby znacznie zwiększyć szybkość gry, musisz profilować swój kod. Określ, gdzie jest wąskie gardło, pozwoli to na wprowadzenie największych ulepszeń.

Istnieje wiele sposobów profilowania kodu, możesz utworzyć własną klasę analizy wydajności (która mogłaby skorzystać z klasy Stopwatch (MSDN) ) lub użyć PIX, aby uzyskać ogólne pojęcie o tym, jak zajęty jest procesor / procesor graficzny .

Możesz również umieścić znaczniki zdarzeń PIX w kodzie, które będą wyświetlane jako kolorowe regiony w odczytach PIX. Nie ma oficjalnego interfejsu C # dla tych funkcji, ale ten wątek pokazuje, jak samemu stworzyć interfejs C #.

CiscoIPPhone
źródło
2
+1, całkowicie się zgadzam. Nie powinieneś patrzeć na algorytmy, powinieneś patrzeć na profilowanie swojego kodu. Domyślam się, że nie ma to nic wspólnego z faktem, że iterujesz przez bloki (nie jest tak wiele), to jest to, co robisz z każdym blokiem. Ostatecznie tak, musisz mieć lepszą metodę niż brutalna siła, ale jeśli nie możesz poradzić sobie z prostą iteracją na kostkach 48x32x48, musisz przemyśleć to, co robisz z każdą kostką, a nie to, jak zapętlasz.
Tim Holt
4
@Tim: O ile jego gracz nie jest wystarczająco duży, aby zajmować przestrzeń 48x32x48, to nie powinien iterować przez nigdzie w pobliżu tak wielu kostek. Jeśli iteruje przez 73000 kostek na klatkę, to mogę ci powiedzieć, nie robiąc żadnego profilowania, że ​​warto to naprawić, jeśli tylko z tego powodu, aby nauczyć się, jak unikać dziesiątek tysięcy razy więcej iteracji niż są konieczne. To nie tak nazwałbym mikro lub przedwczesną optymalizacją.
Olhovsky
Mój gracz jest mniejszy niż 1 kostka, choć może być większy na pewnym etapie (ale nie za dużo)
Joel
Randomman159: Musisz tylko przetestować jego otaczające 27 kostek, aby znaleźć kolizję. Zobacz moją odpowiedź.
Olhovsky,
6

Jeśli twój odtwarzacz jest duży w stosunku do wielkości kostek, prawdopodobnie potrzebujesz oktawy lub innej struktury podziału przestrzennego, jak sugerują inni.

Jeśli jednak twój gracz jest mały w stosunku do wielkości kostek, prawdopodobnie najszybszym sposobem na wykrycie kolizji z kostkami jest proste liniowe przeszukanie obszaru wokół gracza.

Ponieważ twój gracz ma mniej niż 1 kostkę, musisz tylko przetestować, czy nie ma kolizji z sąsiadującymi 27 kostkami.

Zakłada się, że przechowujesz kostki w tablicy, w której możesz indeksować, z jednym miejscem w tablicy dla każdej kostki.

Jak zauważyli inni, musisz wyprofilować swój kod, aby zobaczyć, co naprawdę cię spowalnia.

Gdybym jednak musiał zgadywać, powiedziałbym, że prawdopodobnie sprawdzasz losowanie każdej kostki, co zdecydowanie byłoby twoim wąskim gardłem. Aby to naprawić, powinieneś przyjrzeć się instancjom geometrii.

Olhovsky
źródło
Lub możesz mieć „ramkę ograniczającą” otaczającą gracza, a następnie po prostu sprawdzasz, z jakimi obiektami się zderza, aby określić, z którymi obiektami powinien się zderzyć. Dobry silnik fizyki będzie w stanie przeprowadzić dla ciebie wszystkie optymalizacje; pozwoli także na zderzenie się nie tylko „bloków”.
spowolniony,
Osobiście nie chciałbym polegać na szerokofazie silnika fizyki, aby przetestować dla mnie 73000 kostek, gdybym mógł po prostu napisać około dwudziestu wierszy kodu, aby skutecznie przetestować kolizję. Ponadto prawdopodobnie nie ma obecnie silnika fizyki.
Olhovsky,
1

Jeszcze jedna sugestia, aby przyspieszyć: Twoje bloki są w przybliżeniu naprawione - oznacza to, że gracz nie może zderzyć się z większością z nich. Dodaj wartość logiczną do bloków wskazujących, czy są one narażone, czy nie. (Można to przeliczyć, patrząc na sąsiadów.) Blok, który nie jest odsłonięty, nie musi być sprawdzany pod kątem kolizji.

To oczywiste, że Minecraft robi coś podobnego - uderzyłem kiedyś nieobciążoną bryłę, która dała mi widok na świat - mogłem patrzeć przez twardą ziemię, wszystko, co się pokazało, to otwarte przestrzenie (druga strona były one odsłoniętą powierzchnią i dlatego zostały renderowane).

Loren Pechtel
źródło
-1

Miałem ten problem z moim silnikiem wokseli.

Rozwiązanie: (Dużo prostsze niż oktety) Zamiast zapętlać wszystkie bloki, wystarczy użyć równania, aby określić pozycję bloku w tablicy bloków.

BlockIndex = (x * WorldWidth * WorldHeight) + (z * WorldHeight) + y;

Jeśli chcesz sprawdzić, czy istnieje blok:

Blocks[BlockIndex].Type > -1;

Lub jednak określasz, czy blok istnieje.

BMW
źródło
Problem tutaj jest bardziej skomplikowany, ponieważ masz tylko 2 współrzędne myszy do testowania w świecie 3D. Jeśli widok był z góry na dół, możesz znaleźć 2 z 3 prawych indeksów dla pozycji myszy, a następnie zapętlić dla wysokości, zaczynając od góry - bliżej aparatu i znaleźć pierwsze wystąpienie bloku.
Markus von Broady,