Wie lösen wir große Anforderungen an den Videospeicher in einem 2D-Spiel?
Wir entwickeln ein 2D-Spiel (Factorio) in Allegro C / C ++ und stehen vor dem Problem, dass die Anforderungen an den Videospeicher mit zunehmendem Spielinhalt steigen.
Wir sammeln derzeit alle Informationen zu Bildern, die zuerst verwendet werden sollen, beschneiden alle diese Bilder so weit wie möglich und ordnen sie so eng wie möglich in großen Atlanten an. Diese Atlanten werden im Videospeicher gespeichert, dessen Größe von den Systembeschränkungen abhängt. Derzeit sind es in der Regel 2 Bilder mit einer Größe von bis zu 8192 x 8192, sodass 256 MB bis 512 MB Videospeicher erforderlich sind.
Dieses System funktioniert ziemlich gut für uns, da wir mit einigen benutzerdefinierten Optimierungen und der Aufteilung des Render- und Update-Threads in der Lage sind, Zehntausende von Bildern auf dem Bildschirm mit 60 fps zu zeichnen. Wir haben viele Objekte auf dem Bildschirm, und das Ermöglichen einer großen Verkleinerung ist eine wichtige Voraussetzung. Da wir noch mehr hinzufügen möchten, wird es einige Probleme mit den Anforderungen an den Videospeicher geben, so dass dieses System möglicherweise nicht aushält.
Eines der Dinge, die wir versuchen wollten, ist ein Atlas mit den häufigsten Bildern und der zweite als Cache. Die Bilder würden bei Bedarf aus der Speicher-Bitmap dorthin verschoben. Bei diesem Ansatz gibt es zwei Probleme:
- Das Zeichnen von Speicher-Bitmap zu Video-Bitmap ist in Allegro schmerzhaft langsam.
- Es ist nicht möglich, mit einer anderen Video-Bitmap als dem Haupt-Thread in Allegro zu arbeiten, daher ist dies praktisch unbrauchbar.
Hier sind einige zusätzliche Anforderungen, die wir haben:
- Das Spiel muss deterministisch sein, damit die Leistungsprobleme / Ladezeiten den Spielstatus niemals ändern können.
- Das Spiel ist in Echtzeit und bald auch im Mehrspielermodus. Wir müssen um jeden Preis das kleinste Ruckeln vermeiden.
- Der Großteil des Spiels ist eine durchgehende offene Welt.
Der Test bestand aus dem Zeichnen von 10 000 Sprites in einem Stapel für Größen von 1x1 bis 300x300, mehrmals für jede Konfiguration. Ich habe die Tests mit der Nvidia Geforce GTX 760 durchgeführt.
- Das Zeichnen von Video-Bitmaps zu Video-Bitmaps nahm 0,1us pro Sprite in Anspruch, wenn sich die Quell-Bitmap nicht zwischen einzelnen Bitmaps änderte (die Atlas-Variante). Die Größe spielte keine Rolle
- Das Zeichnen von Video-Bitmaps in Video-Bitmaps nahm, während die Quell-Bitmaps zwischen Zeichnungen umgeschaltet wurden (Nicht-Atlas-Variante), 0,56 us pro Sprite in Anspruch. Auch die Größe spielte keine Rolle.
- Das Zeichnen von Speicherbitmaps auf Videobitmaps war wirklich verdächtig. Größen von 1x1 bis 200x200 brauchten 0,3us pro Bitmap, also nicht so schrecklich langsam. Bei größeren Formaten begann sich die Zeit sehr dramatisch zu erhöhen, und zwar bei 9us für 201x201 auf 3116us für 291x291.
Durch die Verwendung von Atlas wird die Leistung um einen Faktor größer als 5 erhöht. Wenn ich 10 ms für das Rendern hatte, bin ich mit einem Atlas auf 100.000 Sprites pro Frame und ohne diesen auf 20.000 Sprites beschränkt. Das wäre problematisch.
Ich habe auch versucht, eine Möglichkeit zum Testen der Bitmap-Komprimierung und des 1bpp-Bitmap-Formats für Schatten zu finden, aber ich konnte in Allegro keine Möglichkeit finden, dies zu tun.
quelle
Antworten:
Wir haben einen ähnlichen Fall mit unserem RTS (KaM Remake). Alle Einheiten und Häuser sind Sprites. Wir haben 18.000 Sprites für Einheiten und Häuser und Gelände sowie weitere ca. 6.000 für Teamfarben (angewendet als Masken). Lang gestreckt haben wir auch ungefähr 30 000 Zeichen, die in Schriften verwendet werden.
Daher gibt es einige Optimierungen für die von Ihnen verwendeten RGBA32-Atlanten:
Teilen Sie Ihren Sprites-Pool zuerst in viele kleinere Atlanten auf und verwenden Sie sie nach Bedarf, wie in anderen Antworten beschrieben. Dies ermöglicht es auch, unterschiedliche Optimierungstechniken für jeden Atlas einzeln anzuwenden . Ich vermute, Sie werden ein bisschen weniger verschwendeten Arbeitsspeicher haben, weil beim Packen in so große Texturen normalerweise unbenutzte Bereiche unten sind.
Versuchen Sie es mit palettierten Texturen . Wenn Sie Shader verwenden, können Sie die Palette im Shader-Code "anwenden".
Sie könnten eine Option hinzufügen, um RGB5_A1 anstelle von RGBA8 zu verwenden (wenn zum Beispiel Schachbrettschatten für Ihr Spiel in Ordnung sind). Vermeiden Sie nach Möglichkeit 8-Bit-Alpha und verwenden Sie RGB5_A1 oder gleichwertige Formate mit geringerer Genauigkeit (wie RGBA4). Sie beanspruchen die Hälfte des Platzes.
Stellen Sie sicher, dass Sie Sprites eng in Atlanten packen (siehe Algorithmen zum Packen von Behältern), drehen Sie Sprites bei Bedarf und prüfen Sie, ob Sie transparente Ecken für Rhombus-Sprites überlappen können.
Sie können versuchen, Hardware-Komprimierungsformate (DXT, S3TC usw.) zu verwenden - sie können die RAM-Nutzung drastisch reduzieren, aber auf Komprimierungsartefakte prüfen - bei einigen Bildern kann der Unterschied unbemerkt bleiben (Sie können dies selektiv verwenden, wie im ersten Aufzählungspunkt beschrieben). aber auf einigen - sehr aussprechen. Unterschiedliche Komprimierungsformate verursachen unterschiedliche Artefakte. Wählen Sie daher möglicherweise eines aus, das für Ihren Kunststil am besten geeignet ist.
Sehen Sie sich an , wie Sie große Sprites (natürlich nicht manuell, sondern in Ihrem Texturatlas-Packer) in statische Hintergrund-Sprites und kleinere Sprites für animierte Teile aufteilen.
quelle
Zunächst müssen Sie mehr, kleinere Texturatlanten verwenden. Je weniger Texturen Sie haben, desto schwieriger und starrer wird die Speicherverwaltung. Ich würde eine Atlasgröße von 1024 vorschlagen, in welchem Fall Sie 128 Texturen anstelle von 2 haben würden, oder 2048, in welchem Fall Sie 32 Texturen haben würden, die Sie nach Bedarf laden und entladen könnten.
Die meisten Spiele führen diese Ressourcenverwaltung durch, indem sie Ebenengrenzen haben, während auf einem Ladebildschirm alle Ressourcen, die im nächsten Level nicht mehr benötigt werden, entladen und die benötigten Ressourcen geladen werden.
Eine weitere Option ist das Laden auf Abruf, das erforderlich wird, wenn Ebenengrenzen unerwünscht sind oder sogar eine einzelne Ebene zu groß ist, um in den Speicher zu passen. In diesem Fall versucht das Spiel vorherzusagen, was der Spieler in Zukunft sehen wird, und lädt dies im Hintergrund. (Zum Beispiel: Dinge, die derzeit 2 Bildschirme vom Player entfernt sind.) Gleichzeitig werden Dinge entladen, die längere Zeit nicht mehr verwendet wurden.
Es gibt jedoch ein Problem: Was passiert, wenn etwas Unerwartetes passiert ist, das das Spiel nicht vorhersehen konnte?
quelle
Wow, das ist eine Menge Animations-Sprites, die vermutlich aus 3D-Modellen generiert wurden.
Du solltest dieses Spiel wirklich nicht in rohem 2D machen. Wenn Sie die Perspektive festgelegt haben, passiert etwas Lustiges. Sie können vorgerenderte Sprites und Hintergründe nahtlos mit live gerenderten 3D-Modellen mischen, die von einigen Spielen häufig verwendet wurden. Wenn Sie so feine Animationen wünschen, scheint dies die natürlichste Methode zu sein. Holen Sie sich eine 3D-Engine, konfigurieren Sie sie für die Verwendung der isometrischen Perspektive und rendern Sie die Objekte, für die Sie weiterhin Sprites verwenden, als einfache flache Oberflächen mit einem Bild darauf. Und Sie können die Texturkomprimierung mit einer 3D-Engine verwenden, das allein ist ein großer Fortschritt.
Ich denke nicht, dass das Laden und Entladen viel für Sie bedeutet, da Sie so ziemlich alles gleichzeitig auf dem Bildschirm haben können.
quelle
Suchen Sie zunächst das effizienteste Texturformat, mit dem Sie zufrieden sind, unabhängig davon, ob es sich um RGBA4444 oder DXT-Komprimierung usw. handelt. Wenn Sie mit den in einem DXT-Alpha-komprimierten Bild erzeugten Artefakten nicht zufrieden sind, ist dies möglich Um die Bilder mithilfe der DXT1-Komprimierung für die Farbe in Kombination mit einer 4 oder 8-Bit-Graustufen-Maskentextur für das Alpha nicht transparent zu machen? Ich stelle mir vor, Sie würden für die GUI auf RGBA8888 bleiben.
Ich befürworte die Aufteilung in kleinere Texturen mit dem von Ihnen gewählten Format. Bestimmen Sie die Elemente, die immer auf dem Bildschirm angezeigt und daher immer geladen werden. Dies können Gelände- und GUI-Atlanten sein. Ich würde dann die verbleibenden Elemente, die üblicherweise zusammen gerendert werden, so weit wie möglich aufteilen. Ich kann mir nicht vorstellen, dass Sie zu viel Leistung verlieren, selbst wenn Sie bis zu 50-100 Draw Calls auf dem PC ausführen, aber korrigieren Sie mich, wenn ich falsch liege.
Der nächste Schritt besteht darin, die Mipmap-Versionen dieser Texturen zu generieren, wie oben ausgeführt. Ich würde sie nicht in einer einzelnen Datei sondern separat speichern. Sie würden also 1024x1024, 512x512, 256x256 usw. Versionen jeder Datei erhalten, und ich würde dies tun, bis ich die niedrigste Detailstufe erreiche, die ich jemals angezeigt haben möchte.
Nachdem Sie nun über die separaten Texturen verfügen, können Sie ein LOD-System (Level of Detail) erstellen, das Texturen für die aktuelle Zoomstufe lädt und Texturen entlädt, wenn sie nicht verwendet werden. Eine Textur wird nicht verwendet, wenn das Element, das gerendert wird, nicht auf dem Bildschirm angezeigt wird oder für die aktuelle Zoomstufe nicht erforderlich ist. Versuchen Sie, die Texturen in einem von den Aktualisierungs- / Renderthreads getrennten Thread in den Video-RAM zu laden. Sie können die niedrigste LOD-Textur anzeigen, bis die gewünschte geladen ist. Dies kann manchmal zu einem sichtbaren Wechsel zwischen einer Textur mit niedrigen und hohen Details führen. Ich stelle mir jedoch vor, dass dies nur dann der Fall ist, wenn Sie beim Bewegen über die Karte extrem schnell zoomen. Sie könnten das System intelligent machen, indem Sie versuchen, vorab zu laden, wo sich die Person Ihrer Meinung nach bewegen oder zoomen wird, und so viel wie möglich innerhalb der aktuellen Speicherbeschränkungen laden.
So etwas würde ich testen, um zu sehen, ob es hilft. Ich stelle mir vor, dass Sie für extreme Zoomstufen zwangsläufig ein LOD-System benötigen.
quelle
Ich glaube, der beste Ansatz ist es, die Textur in viele Dateien zu teilen und sie bei Bedarf zu laden. Wahrscheinlich besteht Ihr Problem darin, dass Sie versuchen, größere Texturen zu laden, die Sie für eine vollständige 3D-Szene benötigen, und Sie verwenden hierfür Allegro.
Für die große Verkleinerung, die Sie anwenden möchten, müssen Sie Mipmaps verwenden. Mipmaps sind Versionen Ihrer Texturen mit niedrigerer Auflösung, die verwendet werden, wenn die Objekte weit genug von der Kamera entfernt sind. Dies bedeutet, dass Sie Ihre 8192x8192 als 4096x4096 und dann eine andere mit 2048x2048 usw. speichern können. Je kleiner Sie das Sprite auf dem Bildschirm sehen, desto niedriger sind die Auflösungen. Sie können beide als separate Texturen speichern oder beim Laden die Größe ändern (das Generieren von Mipmaps zur Laufzeit verlängert jedoch die Ladezeiten für Ihr Spiel).
Ein ordnungsgemäßes Verwaltungssystem würde die erforderlichen Dateien bei Bedarf laden und die Ressourcen freigeben, wenn sie von niemandem verwendet werden, sowie andere Dinge. Ressourcenverwaltung ist ein wichtiges Thema in der Spieleentwicklung, und Sie beschränken Ihre Verwaltung auf eine einfache Koordinatenzuordnung auf eine einzige Textur, die so gut wie gar keine Verwaltung hat.
quelle
Ich empfehle, mehr Atlas-Dateien zu erstellen, die mit zlib komprimiert und aus der Komprimierung für jeden Atlas gestreamt werden können. Wenn Sie mehr Atlas-Dateien und kleinere Dateien haben, können Sie die Menge der aktiven Bilddaten im Videospeicher begrenzen. Implementieren Sie außerdem den Dreifachpuffermechanismus, sodass Sie jeden Zeichnungsrahmen früher vorbereiten und möglicherweise schneller fertiggestellt werden, sodass die Ruckler nicht auf dem Bildschirm angezeigt werden.
quelle