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:
std::swap(objects[index], objects.back());
objects.pop_back();
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:
Object:
int index
int version
other data
SlotMap:
Object objects[]
int slots[]
int freelist[]
int count
Get(id):
index = indirection[id.index]
if objects[index].version = id.version:
return &objects[index]
else:
return null
CreateObject():
index = freelist.pop()
objects[count].index = id
objects[count].version += 1
indirection[index] = count
Object* object = &objects[count].object
object.initialize()
count += 1
return object
Remove(id):
index = indirection[id.index]
if objects[index].version = id.version:
objects[index].version += 1
objects[count - 1].version += 1
swap(objects[index].data, objects[count - 1].data)
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.