Stroring von Komponenten in Arrays (Entity Component System)

8

Ich suche nach Möglichkeiten, mein ECS zu verbessern, und ich habe viele Leute gesehen, die vorgeschlagen haben, Komponenten in Arrays zu speichern. Dies scheint großartig zu sein, wenn man den schnellen Zugriff für eine Komponente unter Verwendung der Entitäts-ID und vor allem die schnelle Iteration über Elemente des Arrays (da Systeme dies ständig tun) und weniger Cache-Fehler berücksichtigt, als wenn ich Komponenten in der Entität speichere oder verwende:

std::map<int,std::shared_ptr<Component>> components // where int is a component id 

Aber ich habe einige Fragen zu diesem Ansatz.

1) Muss ich für jede Komponente ein Array erstellen? Etwas wie:

PositionComponent positionComponents[MAX_OBJECTS];
HealthComponent healthComponents[MAX_OBJECTS];
...

Oder gibt es eine bessere Möglichkeit, verschiedene Komponenten zusammenhängend im Speicher zu speichern?

2) Wie kann ich Komponenten mithilfe der Entitäts-ID abrufen, wenn ich dies tue?

Ich möchte eine Funktion wie diese implementieren:

template <class T>
T* getComponent(int entityId) const {
    ... // ??
}

so kann ich es dann so benutzen:

PositionComponent* pc = getComponent<PositionComponent>(entityId);
Elias Daler
quelle

Antworten:

12

Dies scheint großartig zu sein, wenn man den schnellen Zugriff für eine Komponente unter Verwendung der Entitäts-ID und vor allem die schnelle Iteration über Elemente des Arrays (da Systeme dies ständig tun) und weniger Cache-Fehler berücksichtigt, als wenn ich Komponenten in der Entität speichere oder verwende:

Haben Sie tatsächlich Cache-Fehler gemessen? Sind sie in irgendeiner Weise ein Problem für Sie? Würde es wahrscheinlich messbare Auswirkungen haben, wenn Sie sie für Ihre Komponentenzugriffe entfernen? Ist es für Sie wertvoll, Ihre Optimierungszeit damit zu verbringen, sich um sie zu kümmern, anstatt sich auf Parallelität oder eine bessere GPU-Nutzung zu konzentrieren? Haben Sie zumindest Messungen Ihres Codes vor einer solchen Änderung, damit Sie diese mit Messungen nach der Änderung vergleichen können?

Das einfache Einfügen von Dingen in ein Array garantiert auch nicht weniger Cache-Fehler. Dies ist der erste von mehreren Schritten zur Verbesserung der Cache-Nutzung für Komponenten. Dies kann auch ein falscher Schritt zur Verbesserung Ihrer Gesamtleistung sein.

1) Muss ich für jede Komponente ein Array erstellen? Etwas wie:

PositionComponent positionComponents[MAX_OBJECTS];
HealthComponent healthComponents[MAX_OBJECTS];

A std::vectoroder ähnliches würde genauso gut funktionieren. Beachten Sie, dass vectorSie Komponentenindizes anstelle von Zeigern verwenden müssen , da der Backing-Speicher neu zugewiesen und vorhandene vorhandene Iteratoren / Zeiger ungültig gemacht werden können.

Sie können auch problemlos ein Chunked-Speichermittel schreiben (wie ein std::deque, jedoch nicht für Warteschlangen optimiertes), mit dem die Struktur wachsen, eine schnelle Iteration beibehalten und stabile Zeiger haben kann.

2) Wie kann ich Komponenten mithilfe der Entitäts-ID abrufen, wenn ich dies tue?

template <class T>
T* getComponent(int entityId) const {
    ... // ??
}

Sie können eine zusätzliche boost::flat_mapoder std::unordered_mapeine benutzerdefinierte Hochleistungs-Hash-Tabelle speichern , die Entitäts-IDs Komponentenindizes für diesen Komponententyp zuordnet.

Sie möchten wahrscheinlich nur einen Index oder einen temporären Zeiger zuordnen (einen, den Sie versprechen, niemals beizubehalten), da dies dem Komponenteninhaber ermöglicht, seine Komponentensammlung zu defragmentieren. Dass beides dem Inhaber möglicherweise ermöglichen kann, Speicherblöcke freizugeben, und vor allem (für datenorientierte Anforderungen), ist entscheidend, um die Iteration über Komponenten tatsächlich schneller zu machen. Es wird wahrscheinlich viel schneller sein, Ihren vorhandenen Ansatz zu verwenden, als über ein riesiges und dünn besiedeltes Array zu iterieren. Denken Sie daran, dass Sie bei einem spärlichen Array (auch wenn es zu 99% belegt ist) eine zusätzliche if (isInUse)Überprüfung in Ihrer Schleife über die Komponenten hinzufügen müssen. Dies ist eine zusätzliche Lese- und Verzweigungsanweisung, die sich spürbar negativ auf Ihre gesamte Iterationsgeschwindigkeit auswirken kann.

Siehe die vier Artikel von BitSquid zum Thema . Diese Leute konzentrieren sich in der Regel auf C-ähnlichen Code und meiden moderne C ++ - Ansätze mit identischer Leistung, aber einfacherer Verwendung. Die zugrunde liegenden Ansätze, die sie verwenden, sind jedoch in C, C ++, C #, Rust oder ähnlichen Sprachen anwendbar.

Sean Middleditch
quelle
1
Um ehrlich zu sein, habe ich im Moment mehr Probleme mit dem Design und vielen virtuellen Casts (ich speichere Komponenten in Entity als std :: vector <Component *>) als Cache-Fehler. Ich möchte nur "reines" ECS erstellen, bei dem die Entität nur eine ID ist. Und wie würde diese ungeordnete Karte funktionieren? Ich verstehe es nicht ganz. Können Sie bitte ein genaueres Beispiel nennen? Und danke für diesen Link, werde es auf jeden Fall überprüfen.
Elias Daler
@EliasDaler: Die verlinkten Artikel erklären die Verwendung einer Hash-Tabelle wie unordered_map, nur sie verwenden ihre eigene. Die Konzepte sind die gleichen.
Sean Middleditch
Leider beantworten ihre Artikel meine Frage nicht. Ihr Komponentendesign unterscheidet sich sehr von meinem. Trotzdem, danke, werde ich diese Antwort markieren, da Ihre Erklärung für mich jetzt Sinn macht.
Elias Daler
1
@ EliasDaler: Vielleicht verstehe ich die Frage dann nicht. In diesem Artikel wird erläutert, wie Sie mithilfe einer Hash-Tabelle Entitäts-IDs Komponentenindizes zuordnen. Dieses Bit sollte auch dann gleich funktionieren, wenn Ihre Komponenten unterschiedlich sind. Wenn ich dich richtig verstehe.
Sean Middleditch
1
@Nikos: Das ist sicher der Low-Tech-Weg. Und für viele Benutzer völlig ausreichend. Es gibt komplexere Implementierungen dieser allgemeinen Idee, die möglich sind (basierend auf "Slot Maps" oder was die Bitsquid-Artikel "gepackte Arrays" nennen oder was EnTT "Sparse Sets" nennt), aber nicht überentwickeln, wenn Sie keine haben zu. :) Vielleicht möchten Sie auch einige andere Open-Source-C ++ - Hash-Tabellen (wie die in Abseil) ausprobieren, unordered_mapdie ... Probleme haben (es wurde für eine Reihe von Invarianten entwickelt, die hier nicht zutreffen, aber dafür einen erheblichen Perf-Overhead verursachen Fall).
Sean Middleditch
3

1) Muss ich für jede Komponente ein Array erstellen?

Ja, aber ein Vektor wäre bequemer und ebenso leistungsfähig.

2) Wie kann ich Komponenten mithilfe der Entitäts-ID abrufen, wenn ich dies tue?

Sie sollten die Entitäts-ID auf den Komponenten selbst speichern. Halten Sie Ihren Vektor nach dieser ID sortiert und verwenden Sie dann vector :: lower_bound , um die Komponenten abzurufen. In meinen eigenen Tests ist diese Suche bis zu einem bestimmten Punkt tatsächlich schneller als eine Karte.

Nox
quelle