Bei einer Karte mit 20 Millionen Kacheln geht dem Spiel der Speicher aus. Wie vermeide ich das?

11

Beim Laden besonders großer Karten wird bei der Lademethode eine Speicherausnahme ausgelöst, bei der eine neue Instanz der Kartenkachel erstellt wird. Ich möchte, dass die gesamte Karte zumindest auf der Server-App (und wenn möglich auf dem Client) verarbeitet wird. Wie soll ich dieses Problem lösen?

UPD: Die Frage hier ist, wie das Spiel nicht mehr abstürzt, wenn noch freier Speicherplatz zur Verfügung steht. Das Aufteilen der Karte in Blöcke ist ein guter Ansatz, aber nicht das, was ich in meinem Fall möchte.

UPD2: Zuerst habe ich jeder neuen Instanz der Kachelklasse eine Textur zugewiesen, und das hat so viel Speicherplatz in Anspruch genommen (auch Ladezeit). Jetzt braucht es ungefähr viermal weniger Platz. Vielen Dank an alle, jetzt kann ich riesige Karten erstellen, ohne daran zu denken, sie noch in Stücke zu zerlegen.

UPD3: Nachdem ich den Code neu geschrieben hatte, um festzustellen , ob Arrays von Kacheleigenschaften schneller funktionieren oder weniger Speicher verbrauchen (als die Eigenschaften in ihren jeweiligen Kacheln als Objekteigenschaften), stellte ich fest, dass ich nicht nur viel Zeit gebraucht habe, um dies zu versuchen brachte keine Leistungsverbesserung und machte das Debuggen des Spiels furchtbar schwierig.

user1306322
quelle
8
Laden Sie nur die Bereiche um die Spieler.
MichaelHouse
4
20 Millionen Kacheln mit 4 Bytes pro Kachel sind nur etwa 80 MB groß - es klingt also so, als wären Ihre Kacheln nicht so klein. Wir können Ihnen nicht auf magische Weise mehr Speicher geben, daher müssen wir herausfinden, wie Sie Ihre Daten verkleinern können. Zeigen Sie uns also, was sich in diesen Kacheln befindet.
Kylotan
Was macht die Lademethode? Postleitzahl, verwenden Sie die Content-Pipeline? Lädt es Texturen in den Speicher oder nur Metadaten? Welche Metadaten? XNA-Inhaltstypen verweisen im Allgemeinen auf nicht verwaltete DirectX-Inhalte, sodass die verwalteten Objekte sehr klein sind. Auf der nicht verwalteten Seite können jedoch viele Inhalte angezeigt werden, die nur angezeigt werden, wenn Sie auch einen DirectX-Profiler ausführen. stackoverflow.com/questions/3050188/…
Oskar Duveborn
2
Wenn das System eine Ausnahme wegen Speichermangels auslöst, ist trotz Ihrer Meinung nicht genügend freier Speicher verfügbar. Es wird keine geheime Codezeile geben, die wir Ihnen geben können, um zusätzlichen Speicher zu aktivieren. Der Inhalt jeder Kachel und die Art und Weise, wie Sie sie zuweisen, sind wichtig.
Kylotan
3
Was lässt Sie denken, dass Sie freien Speicher haben? Führen Sie einen 32-Bit-Prozess in einem 64-Bit-Betriebssystem aus?
Dalin Seivewright

Antworten:

58

Die App stürzt ab, wenn 1,5 GB erreicht sind.

Dies deutet stark darauf hin, dass Sie Ihre Kacheln nicht richtig darstellen, da dies bedeuten würde, dass jede Kachel eine Größe von ~ 80 Byte hat.

Was Sie verstehen müssen, ist, dass es eine Trennung zwischen dem Gameplay- Konzept einer Kachel und der visuellen Kachel geben muss , die der Benutzer sieht. Diese beiden Konzepte sind nicht dasselbe .

Nehmen wir zum Beispiel Terraria. Die kleinste Terraria-Welt nimmt 4200 x 1200 Kacheln auf, was 5 Millionen Kacheln entspricht. Wie viel Gedächtnis braucht es, um diese Welt darzustellen ?

Nun, jede Fliese hat eine Vordergrundebene, eine Hintergrundebene (die Hintergrundwände), eine "Drahtschicht", in die Drähte gehen, und eine "Möbelebene", in die Möbelstücke gehen. Wie viel Speicher nimmt jede Kachel ein? Auch hier sprechen wir nur konzeptionell, nicht visuell.

Eine Vordergrundkachel könnte leicht in einem nicht signierten Kurzschluss gespeichert werden. Es gibt nicht mehr als 65536 Vordergrundkacheltypen, daher macht es keinen Sinn, mehr Speicher als diesen zu verwenden. Die Hintergrundkacheln können sich leicht in einem vorzeichenlosen Byte befinden, da es weniger als 256 verschiedene Arten von Hintergrundkacheln gibt. Die Drahtschicht ist rein binär: Entweder enthält eine Kachel einen Draht oder nicht. Das ist also ein Bit pro Kachel. Und die Möbelschicht könnte wieder ein vorzeichenloses Byte sein, abhängig davon, wie viele verschiedene Möbelstücke es gibt.

Gesamtspeichergröße pro Kachel: 2 Bytes + 1 Byte + 1 Bit + 1 Byte: 4 Bytes + 1 Bit. Somit beträgt die Gesamtgröße für eine kleine Terraria-Karte 20790000 Byte oder ~ 20 MB. (Hinweis: Diese Berechnungen basieren auf Terraria 1.1. Das Spiel hat sich seitdem stark erweitert, aber selbst moderne Terraria könnten innerhalb von 8 Bytes pro Kachelort oder ~ 40 MB passen. Immer noch ziemlich erträglich).

Sie sollten diese Darstellung niemals als Arrays von C # -Klassen speichern. Sie sollten Arrays von ganzen Zahlen oder ähnlichem sein. AC # struct würde auch funktionieren.

Wenn es nun an der Zeit ist, einen Teil einer Karte zu zeichnen (beachten Sie die Betonung), muss Terraria diese konzeptionellen Kacheln in tatsächliche Kacheln umwandeln . Jede Kachel muss tatsächlich ein Vordergrundbild, ein Hintergrundbild, ein optionales Möbelbild und ein Drahtbild haben. Hier kommt XNA mit seinen verschiedenen Sprite-Blättern und dergleichen ins Spiel.

Sie müssen lediglich den sichtbaren Teil Ihrer konzeptionellen Karte in tatsächliche XNA-Sprite-Blattkacheln konvertieren. Sie sollten nicht versuchen, das Ganze auf einmal zu konvertieren . Jede Kachel, die Sie speichern, sollte nur ein Index sein, der besagt, dass "Ich bin Kacheltyp X", wobei X eine Ganzzahl ist. Sie verwenden diesen ganzzahligen Index, um das Sprite abzurufen, mit dem Sie es anzeigen. Und Sie verwenden die Sprite-Blätter von XNA, um dies schneller zu machen als nur einzelne Quads zu zeichnen.

Jetzt muss der sichtbare Bereich der Kacheln in verschiedene Teile aufgeteilt werden, damit Sie nicht ständig Sprite-Blätter erstellen, wenn sich die Kamera bewegt. Sie könnten also 64x64 Teile der Welt als Sprite-Blätter haben. Welche 64x64-Chunks der Welt von der aktuellen Kameraposition des Players aus sichtbar sind, sind die Chunks, die Sie zeichnen. Alle anderen Brocken haben nicht einmal Sprite-Blätter. Wenn ein Block vom Bildschirm fällt, werfen Sie das Blatt weg (Hinweis: Sie löschen es nicht wirklich; Sie behalten es bei sich und spezifizieren es erneut für einen neuen Block, der später sichtbar werden kann).

Ich möchte, dass die gesamte Karte zumindest auf der Server-App (und wenn möglich auf dem Client) verarbeitet wird.

Ihr Server muss die visuelle Darstellung von Kacheln nicht kennen oder sich nicht darum kümmern. Es muss sich nur um die konzeptionelle Darstellung kümmern. Der Benutzer fügt hier eine Kachel hinzu, sodass dieser Kachelindex geändert wird.

Nicol Bolas
quelle
4
+1 Ausgezeichnete Antwort. Muss über mehr als meine abgestimmt werden.
MichaelHouse
22

Teilen Sie das Gelände in Regionen oder Abschnitte auf. Laden Sie dann nur die Blöcke, die für die Spieler sichtbar sind, und entladen Sie diejenigen, die nicht sichtbar sind. Sie können sich das wie ein Förderband vorstellen, bei dem Sie an einem Ende Brocken laden und am anderen Ende entladen, während sich der Spieler weiterbewegt. Immer vor dem Spieler bleiben.

Sie können auch Tricks wie Instanzen verwenden. Wenn alle Kacheln eines Typs nur eine Instanz im Speicher haben und an allen Stellen, an denen sie benötigt werden, mehrmals gezeichnet werden.

Weitere Ideen wie diese finden Sie hier:

Wie haben sie das gemacht: Millionen Fliesen in Terraria

Zonierung von Bereichen auf einer großen Kachelkarte sowie von Dungeons

MichaelHouse
quelle
Das Problem ist, dass ein solcher Ansatz gut für die Client-App ist, aber nicht so gut für den Server. Wenn einige Kacheln auf der Welt verschwinden und eine Kettenreaktion beginnt (und das passiert häufig), sollte sich die gesamte Karte dafür im Speicher befinden, und es ist ein Problem, wenn der Speicher nicht mehr ausreicht.
user1306322
6
Was enthält jede Kachel? Wie viel Speicher wird pro Kachel verwendet? Selbst bei jeweils 10 Bytes sind Sie nur bei 190 Megabyte. Der Server sollte nicht die Textur oder andere Informationen laden, die nicht benötigt werden. Wenn Sie eine Simulation für 20 Millionen Kacheln benötigen, müssen Sie diese Kacheln so weit wie möglich entfernen, um einen Simulationssatz zu erstellen, der nur die für Ihre Kettenreaktionen erforderlichen Informationen enthält.
MichaelHouse
5

Die Antwort von Byte56 ist gut, und ich habe angefangen, dies als Kommentar zu schreiben, aber es wurde zu lang und enthält einige Tipps, die bei einer Antwort hilfreicher sein könnten.

Der Ansatz ist für Server genauso gut wie für einen Client. In der Tat ist es wahrscheinlich mehr gegeben falls , dass der Server über weit mehr Bereiche wird gebeten , als ein Kunde kümmern wird. Der Server hat eine andere Vorstellung davon, was geladen werden muss als ein Client (er kümmert sich um alle Regionen, die allen verbundenen Clients wichtig sind), ist jedoch gleichermaßen in der Lage, eine funktionierende Gruppe von Regionen zu verwalten.

Selbst wenn Sie eine solch gigantische Karte in den Speicher einfügen könnten (und dies höchstwahrscheinlich nicht können), möchten Sie dies nicht . Das Laden von Daten, die Sie nicht sofort verwenden müssen, ist absolut null. Es ist ineffizient und langsam. Selbst wenn Sie es in den Speicher einfügen könnten, wären Sie höchstwahrscheinlich nicht in der Lage, alles in einer vernünftigen Zeitspanne zu verarbeiten.

Ich vermute, Ihr Einwand gegen das Entladen bestimmter Regionen liegt darin, dass Ihr Welt-Update alle Kacheln durchläuft und verarbeitet. Das ist sicherlich gültig, schließt aber nicht aus, dass nur bestimmte Teile geladen werden. Wenden Sie stattdessen beim Laden einer Region alle fehlenden Aktualisierungen an, um sie auf den neuesten Stand zu bringen. Dies ist auch in Bezug auf die Verarbeitung weitaus effizienter (Konzentration eines großen Aufwands auf einen kleinen Speicherbereich). Wenn es Verarbeitungseffekte gibt, die Regionsgrenzen überschreiten könnten (z. B. ein Lavastrom oder ein Wasserfluss, der in eine andere Region übertragen wird), binden Sie diese beiden Regionen so zusammen, dass beim Laden auch die andere Region geladen wird. Im Idealfall werden diese Situationen minimiert, andernfalls befinden Sie sich schnell wieder im Fall "Alle Regionen müssen jederzeit geladen sein".

MrCranky
quelle
3

Ein effizienter Weg, den ich in einem XNA-Spiel verwendet habe, war:

  • Verwenden Sie Spritesheets, kein Bild für jede Kachel / jedes Objekt, und laden Sie einfach das, was Sie benötigen, auf diese Karte.
  • Teilen Sie die Karte in kleinere Teile auf, z. B. Blockkarten mit 500 x 200 Kacheln, wenn Ihre Karte viermal so groß ist oder etwas, das Sie bearbeiten können.
  • Laden Sie diesen Block in den Speicher und malen Sie nur die sichtbaren Teile der Karte (z. B. die sichtbaren Kacheln plus 1 Kachel für jede Richtung, in die sich der Spieler bewegt) in einen Offscreen-Puffer.
  • Löschen Sie das Ansichtsfenster und kopieren Sie die Pixel aus dem Puffer (dies wird als Doppelpuffer bezeichnet und glättet die Bewegung).
  • Wenn Sie sich bewegen, verfolgen Sie die Bouds der Karte und laden Sie den nächsten Block, wenn er sich ihm nähert, und hängen Sie ihn an den aktuellen an.
  • Sobald sich der Spieler vollständig in dieser neuen Karte befindet, können Sie die letzte Karte entladen.

Sie können auf diese Weise auch eine einzelne große Karte verwenden, wenn diese nicht so groß ist, und die dargestellte Logik verwenden.

Natürlich muss die Größe Ihrer Texturen ausgewogen sein und die Auflösung zahlt sich hierfür aus. Versuchen Sie, mit einer niedrigeren Auflösung zu arbeiten, und erstellen Sie bei Bedarf ein hochauflösendes Paket zum Laden, wenn eine Konfigurationseinstellung festgelegt ist.

Sie müssen jedes Mal nur die relevanten Teile dieser Karte schleifen und nur das rendern, was benötigt wird. Auf diese Weise verbessern Sie die Leistung erheblich.

Ein Server kann eine riesige Karte schneller verarbeiten, da er das Spiel nicht rendern muss (eine der langsamsten Operationen). Die Logik dafür kann also die Verwendung größerer Teile oder sogar der gesamten Karte sein, sondern nur die Logik um Spieler herum, wie in einem 2-fachen des Spieler-Ansichtsfensters des Bereichs um den Spieler herum.

Ein Ratschlag hier ist, dass ich keineswegs ein Experte für Spieleentwickler bin und mein Spiel für das Abschlussprojekt meines Abschlusses gemacht wurde (das ich mit der besten Punktzahl hatte), also bin ich vielleicht nicht in jedem Punkt so richtig, aber ich habe recherchierte im Internet und auf Websites wie http://www.gamasutra.com und auf der XNA-Erstellerseite creators.xna.com (zu diesem Zeitpunkt http://create.msdn.com ), um Wissen und Fähigkeiten zu sammeln, und es funktionierte gut für mich .

Ricardo Souza
quelle
2

Entweder

  • kaufe mehr Speicher
  • Stellen Sie Ihre Datenstrukturen kompakter dar
  • Halten Sie nicht alle Daten gleichzeitig im Speicher.
Thomas
quelle
Ich habe 8 GB RAM auf Win7 64-Bit und die App stürzt ab, wenn sie 1,5 GB erreicht. Es macht also wirklich keinen Sinn, mehr Widder zu kaufen, es sei denn, ich vermisse etwas.
user1306322
1
@ user1306322: Wenn Sie über 20 Millionen Kacheln verfügen und ca. 1,5 GB Speicherplatz beanspruchen, bedeutet dies, dass Ihre Kacheln ungefähr 80 Byte pro Kachel umfassen . Was genau speichern Sie in diesen Dingen?
Nicol Bolas
@NicolBolas wie gesagt, ein paar Ints, Bytes und eine Textur2d. Außerdem benötigen nicht alle 1,5 GB, die App stürzt ab, wenn diese Speicherkosten erreicht sind.
user1306322
2
@ user1306322: Das ist immer noch weit mehr als es sein muss. Wie groß ist ein texture2d? Hat jede Kachel eine eigene oder teilen sie Texturen? Und wo hast du das gesagt? Sie haben es sicherlich nicht in Ihre Frage aufgenommen, wo die Informationen sein sollten. Erklären Sie bitte Ihre besonderen Umstände besser.
Nicol Bolas
@ user1306322: Ehrlich gesagt verstehe ich nicht ganz, warum meine Antwort abgelehnt wurde. Ich habe drei Abhilfemaßnahmen für Situationen ohne Speicherplatz aufgezählt - das bedeutet natürlich nicht, dass alle unbedingt angewendet werden müssen. Aber ich denke immer noch, dass sie richtig sind. Ich hätte wahrscheinlich "Erweitern Sie Ihren Speicher" anstelle von "Kaufen" schreiben sollen, um den Fall von virtuellen Maschinen einzuschließen. Werde ich abgelehnt, weil meine Antwort prägnant statt ausführlich war? Ich denke, diese Punkte sind einfach genug, um ohne weitere Erklärung verstanden zu werden?!
Thomas
1

Die Frage hier ist, wie das Spiel nicht mehr abstürzt, wenn noch freier Speicherplatz zur Verfügung steht. Das Aufteilen der Karte in Blöcke ist ein guter Ansatz, aber nicht das, was ich in meinem Fall möchte.

Als Antwort auf Ihr Update können Sie keine Arrays mit einer größeren Int32.MaxValueGröße zuweisen . Sie müssen es in Stücke teilen. Sie können es sogar in eine Wrapper-Klasse einkapseln, die eine Array-ähnliche Fassade freigibt:

Jimmy
quelle