Zu DOTS: Entitätskomponentensystem

Dies ist einer von mehreren Beiträgen über unseren neuen Data-Oriented Tech Stack (DOTS), in denen wir einige Einblicke darüber geben, wie und warum wir dorthin gekommen sind, wo wir heute sind, und wohin wir als nächstes gehen.
In meinem letzten Beitrag habe ich über HPC# und Burst als Low-Level-Grundlagentechnologien für die Zukunft von Unity gesprochen. Ich bezeichne diese Ebene unseres Stacks gerne als "Game Engine Engine". Jeder kann diesen Stack verwenden, um eine Spiel-Engine zu schreiben. Wir können. Wir werden. Das können Sie auch. Mögen Sie unsere nicht? Schreiben Sie Ihren eigenen Text oder ändern Sie unseren nach Ihren Wünschen.
Die nächste Schicht, die wir darauf aufbauen, ist ein neues Komponentensystem. Unity hat sich schon immer auf das Konzept der Komponenten konzentriert. Wenn man einem GameObject eine Rigidbody-Komponente hinzufügt, beginnt es zu fallen. Sie fügen eine Lichtkomponente zu einem GameObject hinzu und es beginnt Licht zu emittieren. Fügen Sie eine AudioEmitter-Komponente hinzu, und das GameObject wird anfangen, Sound zu produzieren.
Es ist ein sehr natürliches Konzept für Programmierer und Nicht-Programmierer gleichermaßen, und es ist einfach, intuitive Benutzeroberflächen dafür zu erstellen. Ich bin eigentlich ziemlich erstaunt, wie gut dieses Konzept gealtert ist. So gut, dass wir es behalten wollen.
Was nicht gut gealtert ist, ist die Art und Weise, wie wir unser Komponentensystem eingeführt haben. Es wurde mit einer objektorientierten Denkweise geschrieben. Komponenten und GameObjects sind "schwere C++"-Objekte. Das Erstellen/Löschen dieser Objekte erfordert eine Mutex-Sperre, um die globale Liste der id->Objektzeiger zu ändern. Alle GameObjects haben einen Namen. Jedes dieser Objekte erhält ein C#-Wrapper-Objekt, das auf das C++-Objekt verweist. Dieses C#-Objekt kann sich überall im Speicher befinden. Das C++-Objekt kann sich auch an einer beliebigen Stelle im Speicher befinden. Cache-Verfehlungen in Hülle und Fülle. Wir versuchen, die Symptome so gut wie möglich zu lindern, aber es gibt nicht viel, was man tun kann.
Mit einer datenorientierten Denkweise können wir viel mehr erreichen. Wir können die gleichen angenehmen Eigenschaften aus Benutzersicht beibehalten (fügen Sie eine Rigidbody-Komponente hinzu, und das Ding wird fallen), aber auch erstaunliche Leistung und Parallelität mit unserem neuen Komponentensystem erzielen.
Dieses neue Komponentensystem ist unser Entity Component System (ECS). Ganz grob gesagt, was man heute mit einem GameObject macht, macht man im neuen System mit einer Entity. Komponenten werden immer noch als Komponenten bezeichnet. Was ist also anders? Das Datenlayout.
Schauen wir uns einige gängige Datenzugriffsmuster an
Eine typische Komponente, die Sie auf herkömmliche Weise in Unity schreiben würden, könnte wie folgt aussehen:
Klasse Orbit : MonoBehaviour
{
public Transform _objectToOrbitAround;
void Update()
{
//bitte ignorieren Sie, dass die Mathematik kaputt ist, darum geht es hier nicht :)
var currentPos = GetComponent<Transform>().position;
var targetPos = _objectToOrbitAround.position;
GetComponent<RigidBody>().velocity += SomehowSteerTowards(currentPos,targetPos)
}
}
Dieses Muster taucht immer wieder auf. Eine Komponente muss eine oder mehrere andere Komponenten auf demselben GameObject finden und einige Werte darauf lesen/schreiben.
Daran sind viele Dinge falsch:
- Die Methode Update() wird für eine einzelne Orbit-Komponente aufgerufen. Der nächste Update()-Aufruf könnte für eine völlig andere Komponente erfolgen, was wahrscheinlich dazu führt, dass dieser Code aus dem Cache entfernt wird, wenn dieser Frame das nächste Mal für eine andere Orbit-Komponente ausgeführt werden muss.
- Update() muss GetComponent() verwenden, um den Rigidbody zu finden. (Es könnte stattdessen zwischengespeichert werden, aber dann muss man darauf achten, dass die Rigidbody-Komponente nicht zerstört wird).
- Die anderen Komponenten, mit denen wir arbeiten, befinden sich an ganz anderen Stellen im Speicher.
Das von ECS verwendete Datenlayout erkennt, dass dies ein sehr häufiges Muster ist, und optimiert das Speicherlayout, um solche Vorgänge schnell zu machen.
ECS gruppiert alle Entitäten, die genau denselben Satz von Komponenten haben, im Speicher. Er nennt eine solche Reihe einen Archetyp. Ein Beispiel für einen Archetyp ist: "Position & Geschwindigkeit & Rigidbody & Collider". ECS weist den Speicher in 16k-Blöcken zu. Jeder Chunk enthält nur die Komponentendaten für Entitäten eines einzigen Archetyps.
Anstatt dass die Update-Methode des Benutzers zur Laufzeit pro Orbit-Instanz nach anderen Komponenten sucht, muss man im ECS-Land statisch deklarieren: "Ich möchte einige Operationen auf allen Entitäten ausführen, die sowohl eine Velocity- als auch eine Rigidbody- und eine Orbit-Komponente haben. Um alle diese Entitäten zu finden, suchen wir einfach alle Archetypen, die einer bestimmten "Komponentensuchanfrage" entsprechen. Jeder Archetyp hat eine Liste von Chunks, in denen Entitäten dieses Archetyps gespeichert werden. Wir führen eine Schleife über all diese Chunks durch, und innerhalb jedes Chunks führen wir eine lineare Schleife mit eng gepacktem Speicher aus, um die Komponentendaten zu lesen und zu schreiben. Diese lineare Schleife, die auf jeder Entität denselben Code ausführt, ist auch eine wahrscheinliche Vektorisierungsmöglichkeit für Burst.
In vielen Fällen kann dieser Prozess trivialerweise in mehrere Jobs aufgeteilt werden, so dass der Code, der die ECS-Komponente betreibt, mit einer Kernauslastung von nahezu 100 % läuft.
ECS übernimmt all diese Arbeit für Sie, Sie müssen nur den Code angeben, den Sie auf jeder Entität ausführen möchten. (Sie können die Chunk-Iteration aber auch manuell durchführen).
Wenn Sie eine Komponente zu einer Entität hinzufügen oder aus ihr entfernen, wechselt sie den Archetyp. Wir verschieben es von seinem aktuellen Chunk zu einem Chunk des neuen Archetyps und tauschen die letzte Entität des vorherigen Chunks wieder aus, um das "Loch zu füllen".
In ECS deklarieren Sie auch statisch, was Sie mit den Komponentendaten machen wollen. ReadOnly oder ReadWrite. Durch das Versprechen (das Versprechen wird überprüft), nur von der Komponente Position zu lesen, kann ECS eine effizientere Planung seiner Aufträge erreichen. Andere Aufträge, die ebenfalls aus der Komponente Position lesen wollen, müssen nicht warten.
Dieses Datenlayout ermöglicht es uns auch, ein langjähriges Problem zu lösen, nämlich die Ladezeiten und die Serialisierungsleistung. Das Laden/Streamen von ECS-Daten für eine große Szene ist nicht viel mehr als das Laden von Rohbytes von der Festplatte und deren unveränderte Verwendung.
Das ist der Grund, warum die Megacity-Demo auf einem Telefon in wenigen Sekunden geladen ist.
Entitäten können zwar das tun, was Spielobjekte heute tun, aber sie können noch mehr, weil sie so leichtgewichtig sind. Was ist eigentlich eine Entität? In einem früheren Entwurf dieses Beitrags schrieb ich "wir speichern Entitäten in Chunks", und änderte es später in "wir speichern Komponentendaten für Entitäten in Chunks". Es ist eine wichtige Unterscheidung zu machen, um zu erkennen, dass ein Entity nur eine 32-Bit-Ganzzahl ist. Es gibt nichts zu speichern oder zuzuweisen, außer den Daten seiner Komponenten. Weil sie so billig sind, kann man sie für Szenarien verwenden, für die Spielobjekte nicht geeignet waren. Wie die Verwendung einer Entität für jedes einzelne Partikel in einem Partikelsystem.
Die nächste Schicht, die wir aufbauen müssen, ist sehr groß. Es ist die "Spiel-Engine"-Schicht, die aus Funktionen wie "Renderer", "Physik", "Netzwerk", "Eingabe", "Animation" usw. besteht. Das ist in etwa der Stand von heute. Wir haben mit der Arbeit an diesen Stücken begonnen, aber sie werden nicht über Nacht fertig sein.
Das mag sich wie ein Flop anhören. In gewisser Weise ist es das, aber in anderer Hinsicht auch nicht. Da ECS und alles, was darauf aufbaut, in C# geschrieben ist, kann es innerhalb des traditionellen Unity laufen. Da es innerhalb von Unity läuft, können Sie ECS-Komponenten schreiben, die Vor-ECS-Funktionen verwenden. Es gibt derzeit kein reines ECS-Netzzeichnungssystem. Sie können jedoch ein ECS MeshRenderSystem schreiben, das die Vor-ECS Graphics.DrawMeshIndirect API als Implementierung verwendet, während Sie auf eine reine ECS-Version warten. Dies ist genau die Technik, die in unserer Megacity-Demo verwendet wird. Laden/Streaming/Culling/LODding/Animation wird mit reinen ECS-Systemen durchgeführt, die endgültige Zeichnung jedoch nicht.
Sie können also mischen und kombinieren. Das Tolle daran ist, dass Sie bereits jetzt die Vorteile des Burst-Codegen und der ECS-Leistung für Ihren Spielcode nutzen können, anstatt darauf warten zu müssen, dass wir reine ECS-Versionen aller Subsysteme liefern. Das Schlimme daran ist, dass man in dieser Übergangsphase diese Reibung sieht und spürt, dass man "zwei verschiedene Welten benutzt, die zusammengeklebt sind".
Wir werden den gesamten Quellcode für unsere ECS HPC#-Subsysteme in Paketen ausliefern. Sie können jedes Subsystem untersuchen, debuggen und modifizieren und haben eine feinere Kontrolle darüber, wann Sie welches Subsystem aktualisieren wollen. Sie könnten zum Beispiel das Physik-Subsystem-Paket aktualisieren, ohne etwas anderes zu aktualisieren.
Die Spielobjekte werden nicht verschwinden. Seit über einem Jahrzehnt werden erfolgreich tolle Spiele auf diesem System entwickelt. Diese Stiftung wird nicht verschwinden.
Was sich ändern wird, ist, dass sich unsere Energie für Verbesserungen mit der Zeit von der Welt der Spielobjekte auf die ECS-Welt verlagern wird.
Ein häufig vorgebrachter und sehr berechtigter Einwand, wenn man sich mit ECS beschäftigt, ist, dass man sehr viel tippen muss. Eine Menge Standardcode, der zwischen Ihnen und dem, was Sie erreichen wollen, steht.
Es sind viele Verbesserungen in Sicht, die darauf abzielen, die meisten Standardformulierungen überflüssig zu machen und die Formulierung Ihrer Absichten zu vereinfachen. Wir haben noch nicht viele davon implementiert, da wir uns auf die grundlegende Leistung konzentriert haben, aber wir glauben, dass es keinen guten Grund für ECS-Spielcode gibt, viel Boilerplate-Code zu haben oder besonders viel mehr Arbeit zu machen als ein MonoBehaviour zu schreiben.
Das Projekt Tiny hat bereits einige dieser Verbesserungen implementiert (z. B. eine lambda-basierte Iterations-API). Wo wir gerade dabei sind...
Project Tiny wird auf demselben C# ECS basieren, über das wir in diesem Blogbeitrag gesprochen haben. Das Projekt Tiny wird für uns in mehrfacher Hinsicht ein großer ECS-Meilenstein sein:
- Es wird in einer vollständigen ECS-Umgebung laufen können. Ein neuer Spieler ohne Altlasten.
- Das bedeutet, dass es auch ein reines ECS ist und mit allen ECS-Subsystemen geliefert werden muss, die ein echtes (kleines) Spiel braucht.
- Wir werden die Editor-Unterstützung von Project Tiny für die Bearbeitung von Entitäten für alle ECS-Szenarien übernehmen, nicht nur für Tiny.
Wir haben offene Stellen für alle Teile des DOTS-Stacks, insbesondere in Burbank und Kopenhagen, siehe careers.unity.com.
Besuchen Sie auch das Unity Entity Component System und das C# Job System Forum, um Feedback zu geben und Informationen über experimentelle und Vorschau-Funktionen zu erhalten.
