Ich mache einen Ego-Shooter und kenne viele verschiedene Containertypen, aber ich würde gerne den Container finden, der am effizientesten für die Speicherung dynamischer Objekte ist, die häufig im Spiel hinzugefügt und gelöscht werden. EX-Geschosse.
Ich denke in diesem Fall wäre es eine Liste, so dass der Speicher nicht zusammenhängend ist und es keine Größenänderung gibt, die stattfindet. Aber dann denke ich auch darüber nach, eine Karte oder ein Set zu verwenden. Wenn jemand hilfreiche Informationen hat, würde ich mich freuen.
Ich schreibe das übrigens in c ++.
Außerdem habe ich eine Lösung gefunden, von der ich denke, dass sie funktionieren wird.
Zu Beginn werde ich einen Vektor mit einer großen Größe zuweisen. Sagen wir 1000 Objekte. Ich werde den zuletzt hinzugefügten Index in diesem Vektor verfolgen, damit ich weiß, wo sich das Ende der Objekte befindet. Dann erstelle ich auch eine Warteschlange, die die Indizes aller Objekte enthält, die aus dem Vektor "gelöscht" wurden. (Es wird kein tatsächliches Löschen durchgeführt, ich werde nur wissen, dass dieser Slot frei ist). Wenn also die Warteschlange leer ist, füge ich dem zuletzt hinzugefügten Index im Vektor +1 hinzu, ansonsten füge ich dem Index des Vektors hinzu, der sich am Anfang der Warteschlange befand.
quelle
Antworten:
Die Antwort ist immer, ein Array oder einen std :: vector zu verwenden. Typen wie eine verknüpfte Liste oder eine std :: map sind in Spielen normalerweise absolut horrend , und dies schließt definitiv Fälle wie Sammlungen von Spielobjekten ein.
Sie sollten die Objekte selbst (keine Zeiger auf sie) im Array / Vektor speichern.
Sie möchten zusammenhängendes Gedächtnis. Du willst es wirklich wirklich. Das Durchlaufen von Daten im nicht zusammenhängenden Speicher führt im Allgemeinen zu vielen Cache-Fehlern und verhindert, dass der Compiler und die CPU effektive Cache-Vorablesezugriffe durchführen können. Dies allein kann die Leistung beeinträchtigen.
Sie möchten auch Speicherzuordnungen und Freigabezuordnungen vermeiden. Sie sind sehr langsam, auch mit einem schnellen Speicherzuweiser. Ich habe gesehen, dass Spiele eine 10-fache FPS-Erhöhung erhalten, wenn nur ein paar hundert Speicherzuordnungen pro Frame entfernt wurden. Scheint nicht so schlimm zu sein, aber es kann sein.
Schließlich können die meisten Datenstrukturen, die Sie für die Verwaltung von Spielobjekten benötigen, auf einem Array oder einem Vektor wesentlich effizienter implementiert werden als auf einem Baum oder einer Liste.
Zum Entfernen von Spielobjekten können Sie beispielsweise Swap-and-Pop verwenden. Einfach zu implementieren mit etwas wie:
Sie können Objekte auch einfach als gelöscht markieren und ihren Index in eine freie Liste setzen, wenn Sie das nächste Mal ein neues Objekt erstellen müssen, aber es ist besser, das Swap-and-Pop-Verfahren auszuführen. Damit können Sie eine einfache for-Schleife über alle Live-Objekte ausführen, ohne von der Schleife selbst abzweigen zu müssen. Für die Integration der Geschossphysik und dergleichen kann dies eine signifikante Leistungssteigerung sein.
Noch wichtiger ist, dass Sie mithilfe der Slot-Map-Struktur Objekte mit einem einfachen Paar von Tabellensuchen von einem stabilen Unique finden können.
Ihre Spielobjekte haben einen Index in ihrem Hauptarray. Sie können mit nur diesem Index sehr effizient nachgeschlagen werden (viel schneller als eine Karte oder sogar eine Hash-Tabelle). Der Index ist jedoch nicht stabil, da beim Entfernen von Objekten Swap and Pop ausgeführt wird.
Eine Slot-Map erfordert zwei Indirektionsebenen, aber beide sind einfache Array-Lookups mit konstanten Indizes. Sie sind schnell . Wirklich schnell.
Die Grundidee ist, dass Sie drei Arrays haben: Ihre Hauptobjektliste, Ihre Indirektionsliste und eine freie Liste für die Indirektionsliste. Ihre Hauptobjektliste enthält Ihre tatsächlichen Objekte, wobei jedes Objekt seine eigene eindeutige ID kennt. Die eindeutige ID besteht aus einem Index und einem Versions-Tag. Die Indirektionsliste ist einfach ein Array von Indizes zur Hauptobjektliste. Die freie Liste ist ein Stapel von Indizes in der Indirektionsliste.
Wenn Sie ein Objekt in der Hauptliste erstellen, finden Sie einen nicht verwendeten Eintrag in der Indirektionsliste (unter Verwendung der freien Liste). Der Eintrag in der Indirektionsliste zeigt auf einen nicht verwendeten Eintrag in der Hauptliste. Sie initialisieren Ihr Objekt an diesem Speicherort und setzen seine eindeutige ID auf den Index des ausgewählten Indirektionslisteneintrags und das vorhandene Versions-Tag im Hauptlistenelement plus eins.
Wenn Sie ein Objekt zerstören, tauschen Sie es wie gewohnt aus und erhöhen gleichzeitig die Versionsnummer. Anschließend fügen Sie der freien Liste auch den Indirektionslistenindex (Teil der eindeutigen ID des Objekts) hinzu. Wenn Sie ein Objekt als Teil des Swap-and-Pop verschieben, aktualisieren Sie auch seinen Eintrag in der Indirektionsliste an seinen neuen Speicherort.
Beispiel Pseudocode:
In der Indirektionsebene können Sie einen stabilen Bezeichner (den Index in der Indirektionsebene, in der Einträge nicht verschoben werden) für eine Ressource festlegen, die während der Komprimierung verschoben werden kann (die Hauptobjektliste).
Mit dem Versions-Tag können Sie eine ID für ein Objekt speichern, das möglicherweise gelöscht wird. Zum Beispiel haben Sie die ID (10,1). Das Objekt mit Index 10 wird gelöscht (z. B. Ihre Kugel trifft auf ein Objekt und wird zerstört). Die Versionsnummer des Objekts an diesem Speicherort in der Hauptobjektliste ist erhöht und gibt (10,2). Wenn Sie versuchen, von einer veralteten ID erneut nach (10,1) zu suchen, gibt die Suche dieses Objekt über den Index 10 zurück, kann jedoch feststellen, dass sich die Versionsnummer geändert hat, sodass die ID nicht mehr gültig ist.
Dies ist die absolut schnellste Datenstruktur, die Sie mit einer stabilen ID haben können, die es Objekten ermöglicht, sich im Speicher zu bewegen, was für die Datenlokalität und die Cache-Kohärenz wichtig ist. Dies ist schneller als jede mögliche Implementierung einer Hash-Tabelle. Mindestens eine Hash-Tabelle muss einen Hash berechnen (mehr Anweisungen als eine Tabellensuche) und dann der Hash-Kette folgen (entweder eine verknüpfte Liste im schrecklichen Fall von std :: unordered_map oder eine offen adressierte Liste in jede nicht-dumme Implementierung einer Hash-Tabelle) und muss dann einen Wertvergleich für jeden Schlüssel durchführen (nicht teurer, aber möglicherweise billiger als die Versions-Tag-Prüfung). Eine sehr gute Hash-Tabelle (die in keiner Implementierung der STL enthalten ist, da die STL eine Hash-Tabelle vorschreibt, die für andere Anwendungsfälle optimiert, als Sie für eine Spielobjektliste benötigen) kann eine Indirektion ersparen.
Es gibt verschiedene Verbesserungen, die Sie am Basisalgorithmus vornehmen können. Verwenden Sie beispielsweise so etwas wie std :: deque für die Hauptobjektliste. Eine zusätzliche Indirektionsebene, mit der Objekte in eine vollständige Liste eingefügt werden können, ohne dass temporäre Zeiger, die Sie von der Slotmap erhalten haben, ungültig werden.
Sie können auch vermeiden, den Index innerhalb des Objekts zu speichern, da der Index aus der Speicheradresse des Objekts (this - objects) berechnet werden kann und noch besser nur zum Entfernen des Objekts benötigt wird. In diesem Fall haben Sie bereits die ID des Objekts (und damit auch Index) als Parameter.
Entschuldigung für die Zuschreibung; Ich glaube nicht, dass es die klarste Beschreibung ist, die es geben könnte. Es ist spät und es ist schwierig zu erklären, ohne mehr Zeit als ich für Codebeispiele aufzuwenden.
quelle
Array
mit fester Größe (linearer Speicher) mit interner freier Liste (O (1) alloc / free, stable indicies)
mit schwachen Referenzschlüsseln (die Wiederverwendung des Schlitzes macht den Schlüssel ungültig
)
Behandelt alles von Kugeln über Monster bis hin zu Texturen und Partikeln. Dies ist die beste Datenstruktur für Videospiele. Ich denke, es kam von Bungie (damals in den Marathon- / Mythos-Tagen), ich habe es bei Blizzard erfahren und ich denke, es war in einem Spiel, das Juwelen programmiert. Es ist wahrscheinlich überall in der Spieleindustrie zu diesem Zeitpunkt.
F: "Warum kein dynamisches Array verwenden?" A: Dynamische Arrays verursachen Abstürze. Einfaches Beispiel:
Sie können sich Fälle mit mehr Komplikationen vorstellen (wie tiefe Callstacks). Dies gilt für alle Array-ähnlichen Container. Wenn wir Spiele machen, haben wir genug Verständnis für unser Problem, um Größen und Budgets gegen Leistung einzutauschen.
Und ich kann es nicht genug sagen: Wirklich, das ist das Beste, was es je gab. (Wenn Sie nicht einverstanden sind, posten Sie Ihre bessere Lösung! Vorsichtsmaßnahme - muss die oben in diesem Beitrag aufgelisteten Probleme angehen: linearer Speicher / Iteration, O (1) Allokation / Frei, stabile Indizes, schwache Referenzen, null Overhead-Derefs oder habe einen tollen grund warum du keinen brauchst;)
quelle
DataArray
scheint auch ein Array dynamisch in ctor zuzuweisen. So könnte es eine andere Bedeutung mit meinem Verständnis haben.Darauf gibt es keine richtige Antwort. Es hängt alles von der Implementierung Ihrer Algorithmen ab. Gehen Sie einfach mit einem, den Sie für das Beste halten. Versuchen Sie nicht, in diesem frühen Stadium zu optimieren.
Wenn Sie häufig Objekte löschen und neu erstellen, sollten Sie sich ansehen, wie Objektpools implementiert werden.
Edit: Warum komplizieren Dinge mit Slots und was nicht. Warum nicht einfach einen Stapel verwenden und den letzten Gegenstand herausnehmen und wiederverwenden? Wenn Sie also einen hinzufügen, werden Sie ++ ausführen, wenn Sie einen einfügen, den Sie ausführen, um den Überblick über Ihren Endindex zu behalten.
quelle
Das hängt von deinem Spiel ab. Die Container unterscheiden sich darin, wie schnell der Zugriff auf ein bestimmtes Element erfolgt, wie schnell ein Element entfernt und wie schnell ein Element hinzugefügt wird.
Normalerweise möchten Sie eine Liste verwenden, wenn Sie möchten, dass Ihre Objektliste nicht chronologisch sortiert ist und Sie daher neue Objekte einfügen müssen, anstatt sie anzuhängen, andernfalls eine Deque. Eine Deque hat die Flexibilität gegenüber einem Vektor erhöht, hat aber nicht wirklich einen großen Nachteil.
Wenn Sie wirklich viele Entitäten haben, sollten Sie sich Space Partitioning ansehen.
quelle