Mapa z 20 milionami kafelków powoduje brak pamięci w grze. Jak tego uniknąć?

11

Podczas ładowania bardzo dużych map metoda ładowania wyrzuca wyjątek z pamięci, gdy tworzone jest nowe wystąpienie kafelka mapy. Chciałbym, aby cała mapa była przetwarzana przynajmniej w aplikacji serwera (i na kliencie, jeśli to możliwe). Jak mam rozwiązać ten problem?

UPD: pytanie brzmi, jak sprawić, by gra przestała się zawieszać, gdy jest jeszcze wolna pamięć do użycia. Jeśli chodzi o dzielenie mapy na części, jest to dobre podejście, ale nie w moim przypadku.

UPD2: Na początku przypisywałem teksturę każdemu nowemu wystąpieniu klasy kafelków i to wymagało tyle pamięci (także czasu ładowania). Teraz zajmuje około cztery razy mniej miejsca. Dzięki wszystkim mogę teraz uruchamiać ogromne mapy, nie myśląc o tym, aby je teraz rozbić na części.

UPD3: Po przepisaniu kodu, aby sprawdzić, czy tablice właściwości kafelków działają szybciej lub zużywają mniej pamięci (niż te właściwości w odpowiednich kafelkach jako właściwości obiektów), odkryłem, że nie tylko zajęło mi to dużo czasu, aby to wypróbować, ale nie przyniósł żadnej poprawy wydajności i sprawił, że gra była wyjątkowo trudna do debugowania.

użytkownik1306322
źródło
8
Ładuj tylko obszary wokół graczy.
MichaelHouse
4
20 milionów kafelków przy 4 bajtach na kafelek to tylko około 80 MB - więc wygląda na to, że twoje kafelki nie są tak małe. Nie możemy magicznie dać ci więcej pamięci, więc musimy znaleźć sposób na zmniejszenie twoich danych. Pokaż nam, co jest w tych kafelkach.
Kylotan,
Co robi metoda ładowania? Kod pocztowy, czy korzystasz z potoku treści? Czy ładuje tekstury do pamięci czy tylko metadane? Jakie metadane? Typy zawartości XNA ogólnie odwołują się do niezarządzanych elementów DirectX, więc zarządzane obiekty są bardzo małe, ale po stronie niezarządzanej może istnieć mnóstwo rzeczy, których nie widać, chyba że uruchomisz również profiler DirectX. stackoverflow.com/questions/3050188/…
Oskar Duveborn,
2
Jeśli system zgłosi wyjątek braku pamięci, oznacza to, że nie ma wystarczającej ilości wolnej pamięci do użycia, pomimo tego, co myślisz. Nie będzie żadnego tajnego wiersza kodu, który możemy dać, aby włączyć dodatkową pamięć. Ważna jest zawartość każdego kafelka i sposób ich przydzielania.
Kylotan,
3
Co sprawia, że ​​myślisz, że masz wolną pamięć? Czy uruchamiasz proces 32-bitowy w 64-bitowym systemie operacyjnym?
Dalin Seivewright

Odpowiedzi:

58

aplikacja ulega awarii, gdy osiągnie 1,5 Gb.

To zdecydowanie sugeruje, że nie reprezentujesz poprawnie swoich kafelków, ponieważ oznaczałoby to, że każdy kafelek ma rozmiar ~ 80 bajtów.

Musisz zrozumieć, że należy rozdzielić koncepcję gry kafelka od kafelka wizualnego, który widzi użytkownik. Te dwie koncepcje to nie to samo .

Weźmy na przykład Terraria. Najmniejszy świat Terraria zajmuje 4200 x 1200 płytek, czyli 5 milionów płytek. Ile pamięci zajmuje reprezentowanie tego świata?

Każda płytka ma warstwę pierwszoplanową, warstwę tła (ściany tła), „warstwę drutu”, do której prowadzą przewody, i „warstwę mebli”, do której prowadzą elementy mebli. Ile pamięci zajmuje każdy kafelek? Znowu mówimy tylko koncepcyjnie, a nie wizualnie.

Kafelek pierwszego planu można łatwo przechowywać w nieoznaczonym skrócie. Nie ma więcej niż 65536 rodzajów kafelków pierwszego planu, więc nie ma sensu używać więcej pamięci niż to. Kafelki tła mogą z łatwością zawierać bajt bez znaku, ponieważ istnieje mniej niż 256 różnych rodzajów kafelków tła. Warstwa drutu jest czysto binarna: albo płytka ma drut, albo nie. To jeden bit na płytkę. A warstwa mebli może znów być bajtem niepodpisanym, w zależności od liczby możliwych różnych mebli.

Całkowity rozmiar pamięci na kafelek: 2 bajty + 1 bajt + 1 bit + 1 bajt: 4 bajty + 1 bit. Tak więc całkowity rozmiar małej mapy Terraria wynosi 20790000 bajtów lub ~ 20 MB. (uwaga: obliczenia te oparte są na Terrarii 1.1. Od tego czasu gra znacznie się rozwinęła, ale nawet współczesna Terraria może zmieścić się w 8 bajtach na lokalizację kafelków, czyli ~ 40 MB. Nadal jest to całkiem znośne).

Nigdy nie należy przechowywać tej reprezentacji jako tablic klas C #. Powinny to być tablice liczb całkowitych lub coś podobnego. AC # struct też by działało.

Teraz, gdy nadchodzi czas na narysowanie części mapy (zwróć uwagę na nacisk), Terraria musi przekształcić te płytki koncepcyjne w rzeczywiste płytki. Każda płytka musi faktycznie wybrać obraz pierwszego planu, obraz tła, opcjonalny obraz mebli i mieć obraz z drutu. W tym miejscu pojawia się XNA z różnymi arkuszami sprite i tym podobne.

Musisz przekonwertować widoczną część mapy pojęciowej na rzeczywiste płytki arkuszy sprite XNA. Nie powinieneś próbować konwertować całej rzeczy naraz . Każdy przechowywany kafelek powinien być po prostu indeksem z napisem „Jestem kafelkiem typu X”, gdzie X jest liczbą całkowitą. Używasz tego indeksu liczb całkowitych, aby pobrać duszka, którego używasz do jego wyświetlenia. I używasz arkuszy sprite XNA, aby uczynić to szybszym niż tylko rysowanie pojedynczych quadów.

Teraz widoczny obszar kafelków musi zostać podzielony na różne części, abyś nie budował ciągle arkuszy duszka przy każdym ruchu kamery. Więc możesz mieć 64x64 kawałki świata jako arkusze sprite. Niezależnie od tego, które fragmenty świata 64x64 są widoczne z aktualnej pozycji kamery gracza, są to losowane fragmenty. Wszelkie inne fragmenty nie mają nawet arkuszy sprite; jeśli fragment spadnie z ekranu, wyrzucasz ten arkusz (uwaga: tak naprawdę go nie usuwasz; trzymasz go w pobliżu i określasz ponownie jako nowy fragment, który może być później widoczny).

Chciałbym, aby cała mapa była przetwarzana przynajmniej w aplikacji serwera (i na kliencie, jeśli to możliwe).

Twój serwer nie musi wiedzieć ani dbać o wizualną reprezentację kafelków. Jedyne, na czym trzeba się przejmować, to reprezentacja konceptualna. Użytkownik dodaje tutaj kafelek, więc zmienia on indeks kafelków.

Nicol Bolas
źródło
4
+1 Doskonała odpowiedź. Trzeba głosować nad czymś więcej niż moim.
MichaelHouse
22

Podziel teren na regiony lub fragmenty. Następnie ładuj tylko te części, które są widoczne dla graczy, i te, które nie są. Możesz myśleć o tym jak o przenośniku taśmowym, w którym ładujesz kawałki na jednym końcu i rozładowujesz je na drugim, gdy gracz się porusza. Zawsze wyprzedzając gracza.

Możesz także użyć sztuczek takich jak instancja. Gdzie, jeśli wszystkie kafelki jednego typu mają tylko jedną instancję w pamięci i są rysowane wielokrotnie we wszystkich lokalizacjach, w których jest to potrzebne.

Możesz znaleźć więcej takich pomysłów tutaj:

Jak to zrobili: Miliony płytek w Terraria

„Strefowanie” obszarów na dużej mapie kafelków, a także lochy

MichaelHouse
źródło
Problem w tym, że takie podejście jest dobre dla aplikacji klienckiej, ale nie tak dobre dla serwera. Kiedy niektóre kafelki na świecie gasną i zaczyna się reakcja łańcuchowa (i to się często zdarza), cała mapa powinna być w tym celu w pamięci i jest to problem, gdy wyrzuca się z pamięci.
user1306322,
6
Co zawiera każdy kafelek? Ile pamięci zużywa się na kafelek? Nawet po 10 bajtów każdy ma tylko 190 megabajtów. Serwer nie powinien ładować tekstury ani żadnych innych niepotrzebnych informacji. Jeśli potrzebujesz symulacji dla 20 milionów kafelków, musisz usunąć jak najwięcej z tych kafelków, aby utworzyć zestaw symulacji, który zawiera tylko informacje wymagane do reakcji łańcuchowych.
MichaelHouse
5

Odpowiedź Byte56 jest dobra i zacząłem pisać to jako komentarz do tego, ale stało się to zbyt długo i zawiera kilka wskazówek, które mogą być bardziej pomocne w odpowiedzi.

Podejście to jest absolutnie tak samo dobre dla serwera, jak i dla klienta. W rzeczywistości jest to prawdopodobnie bardziej odpowiednie, biorąc pod uwagę, że serwer zostanie poproszony o zajęcie się znacznie większą liczbą obszarów niż klient. Serwer ma inne wyobrażenie o tym, co należy załadować niż klient (dba o wszystkie regiony, o które dbają wszyscy podłączeni klienci), ale jest równie zdolny do zarządzania działającym zestawem regionów.

Nawet jeśli potrafisz zmieścić w pamięci tak gigantyczną mapę (a najprawdopodobniej nie możesz), nie chcesz . Załadowanie danych, których nie trzeba od razu wykorzystywać, jest absolutnie zerowe, jest nieefektywne i powolne. Nawet jeśli umieścisz to w pamięci, najprawdopodobniej nie będziesz w stanie przetworzyć tego wszystkiego w rozsądnym czasie.

Podejrzewam, że sprzeciwiłeś się rozładowaniu niektórych regionów, ponieważ twoja aktualizacja świata jest iteracyjna po wszystkich kafelkach i ich przetwarzaniu? Jest to z pewnością ważne, ale nie wyklucza załadowania tylko niektórych części. Zamiast tego po załadowaniu regionu zastosuj wszelkie pominięte aktualizacje, aby zaktualizować go. Jest to również znacznie bardziej wydajne pod względem przetwarzania (koncentracja dużego wysiłku na małym obszarze pamięci). Jeśli wystąpią jakiekolwiek efekty przetwarzania, które mogą przekroczyć granice regionu (np. Przepływ lawy lub przepływ wody, który przeniesie się do innego regionu), to połącz te dwa regiony razem, aby po załadowaniu jednego z nich tak samo było z drugim. Idealnie byłoby, gdyby takie sytuacje zostały zminimalizowane, w przeciwnym razie szybko znajdziesz się ponownie w przypadku „wszystkie regiony muszą być ładowane przez cały czas”.

MrCranky
źródło
3

Wydajnym sposobem, który wykorzystałem w grze XNA było:

  • użyj arkuszy sprite, a nie obrazu dla każdego kafelka / obiektu, i po prostu załaduj to, czego potrzebujesz na tej mapie;
  • podziel mapę na mniejsze części, powiedzmy mapy fragmentów o wymiarach 500 x 200 kafelków, jeśli twoja mapa jest czterokrotnie większa, lub coś, co możesz zmienić na pół;
  • załaduj ten fragment do pamięci i pomaluj tylko widoczne części mapy (powiedzmy, widoczne płytki plus 1 płytka dla każdego kierunku, w którym porusza się gracz) w buforze poza ekranem;
  • wyczyść rzutnię i skopiuj piksele z bufora (nazywa się to podwójnym buforem i wygładza ruch);
  • kiedy char się poruszasz, śledź budowle mapy i ładuj następny fragment, gdy się do niego zbliży, i dołącz do bieżącego;
  • gdy gracz znajdzie się całkowicie na nowej mapie, możesz rozładować ostatnią.

Możesz także użyć w ten sposób pojedynczej dużej mapy, jeśli nie jest ona TAK duża i użyj przedstawionej logiki.

Oczywiście rozmiar twoich tekstur musi być zrównoważony, a rozdzielczość płaci w tym świetną cenę. Spróbuj pracować w niższej rozdzielczości i, jeśli potrzebujesz, zrób pakiet wysokiej rozdzielczości do załadowania, jeśli ustawienie konfiguracji jest ustawione na.

Za każdym razem będziesz musiał zapętlić tylko odpowiednie części tej mapy i renderować tylko to, co jest potrzebne. W ten sposób znacznie poprawisz wydajność.

Serwer może szybciej przetwarzać ogromną mapę, ponieważ nie musi renderować (jednej z najwolniejszych operacji) gry, więc logika może polegać na wykorzystaniu większych fragmentów lub nawet całej mapy, ale przetworzyć tylko logikę wokół graczy, tak jak w dwukrotności pola widzenia gracza wokół gracza.

Rada tutaj jest taka, że ​​pod żadnym pozorem nie jestem ekspertem od gier, a moja gra została stworzona dla końcowego projektu ukończenia studiów (który miałem najlepszy wynik), więc może nie mam racji w każdym punkcie, ale mam przeszukałem strony internetowe i strony takie jak http://www.gamasutra.com oraz witrynę twórców XNA creators.xna.com (w tej chwili http://create.msdn.com ) w celu zebrania wiedzy i umiejętności, a dla mnie dobrze .

Ricardo Souza
źródło
2

Zarówno

  • kup więcej pamięci
  • reprezentują struktury danych w bardziej zwarty sposób
  • nie przechowuj wszystkich danych jednocześnie.
Tomasz
źródło
Mam RAM 8 Gb na Win7 64bit, a aplikacja zawiesza się, gdy osiągnie 1,5 Gb. Więc naprawdę nie ma sensu kupować więcej pamięci RAM, chyba że czegoś mi brakuje.
user1306322,
1
@ user1306322: Jeśli masz 20 milionów kafelków, a zajmuje to około ~ 1,5 GB pamięci, oznacza to, że twoje kafelki mają około 80 bajtów na płytkę . Co dokładnie przechowujesz w tych rzeczach?
Nicol Bolas,
@NicolBolas, jak powiedziałem, kilka liczb wewnętrznych, bajtów i texture2d. Ponadto nie wszystkie zajmują 1,5 GB, aplikacja ulega awarii, gdy osiągnie ten koszt pamięci.
user1306322,
2
@ user1306322: To wciąż o wiele więcej niż trzeba. Jak duży jest texture2d? Czy każdy kafelek ma unikatowy lub czy mają wspólne tekstury? Również, gdzie masz to powiedział? Na pewno nie umieściłeś tego w swoim pytaniu, gdzie powinny znajdować się informacje. Proszę lepiej wyjaśnić swoje szczególne okoliczności.
Nicol Bolas,
@ user1306322: Szczerze mówiąc, nie bardzo rozumiem, dlaczego moja odpowiedź została odrzucona. Wymieniłem trzy środki zaradcze na sytuacje braku pamięci - oczywiście nie oznacza to, że wszystkie muszą koniecznie mieć zastosowanie. Ale nadal uważam, że mają rację. Prawdopodobnie powinienem napisać „rozszerz swoją pamięć” zamiast „kup”, aby uwzględnić przypadek maszyn wirtualnych. Czy jestem odrzucany, ponieważ moja odpowiedź była zwięzła, a nie pełna? Myślę, że te punkty są na tyle proste, że można je zrozumieć bez dalszych wyjaśnień ?!
Thomas
1

pytanie brzmi, jak sprawić, by gra przestała się zawieszać, gdy jest jeszcze wolna pamięć do użycia. Jeśli chodzi o dzielenie mapy na części, jest to dobre podejście, ale nie w moim przypadku.

W odpowiedzi na aktualizację nie można przydzielić tablic o Int32.MaxValuewiększym rozmiarze. Musisz podzielić to na kawałki. Możesz nawet umieścić go w klasie opakowania, która eksponuje elewację podobną do tablicy:

Jimmy
źródło