Hallo allerseits!
Der vierte Stream
âC ++ Developerâ startet hier, einer der aktivsten Kurse in unserem Land, gemessen an den tatsĂ€chlichen Treffen, bei denen nicht nur âKreuzfahrerâ mit
Dima Shebordaev ins GesprÀch
kommen :) Im Allgemeinen ist der Kurs bereits gewachsen Als eine der gröĂten in unserem Land ist es unverĂ€ndert geblieben, dass
Dima offene Lektionen durchfĂŒhrt und wir vor Beginn des Kurses interessante Materialien auswĂ€hlen.
Lass uns gehen!
Eintrag
Das Entity-Component-System (ECS, âEntity-Component-Systemâ) ist als architektonische Alternative, die das Prinzip der Komposition gegenĂŒber der Vererbung betont, auf dem Höhepunkt der PopularitĂ€t. In diesem Artikel werde ich nicht auf die Details des Konzepts eingehen, da zu diesem Thema bereits genĂŒgend Ressourcen vorhanden sind. Es gibt viele Möglichkeiten, ECS zu implementieren, und ich wĂ€hle meistens recht komplexe, die AnfĂ€nger verwirren und viel Zeit in Anspruch nehmen können.
In diesem Beitrag werde ich einen sehr einfachen Weg beschreiben, um ECS zu implementieren, dessen funktionale Version fast keinen Code erfordert, aber vollstÀndig dem Konzept folgt.

ECS
Apropos ECS: Menschen meinen oft verschiedene Dinge. Wenn ich ĂŒber ECS spreche, meine ich ein System, mit dem Sie EntitĂ€ten definieren können, die keine oder mehr reine Datenkomponenten enthalten. Diese Komponenten werden von reinen Logiksystemen selektiv verarbeitet. Beispielsweise sind Position, Geschwindigkeit, Trefferfeld und Zustand einer Komponente an die EntitĂ€t E gebunden. Sie speichern einfach Daten in sich. Beispielsweise kann eine Gesundheitskomponente zwei Ganzzahlen speichern: eine fĂŒr den aktuellen Zustand und eine fĂŒr das Maximum. Ein System kann ein System zur Wiederherstellung der Gesundheit sein, das alle Instanzen einer Gesundheitskomponente findet und alle 120 Frames um 1 erhöht.
Typische C ++ - Implementierung
Es gibt viele Bibliotheken, die ECS-Implementierungen anbieten. Normalerweise enthalten sie ein oder mehrere Elemente aus der Liste:
- Vererbung der Basiskomponente / des
GravitySystem : public ecs::System
der GravitySystem : public ecs::System
Klasse GravitySystem : public ecs::System
; - Aktive Nutzung von Vorlagen;
- Sowohl das als auch ein anderes in einem CRTP- Look;
- Die
EntityManager
Klasse, die die Erstellung / Speicherung von EntitÀten implizit steuert.
Einige schnelle Google-Beispiele:
Alle diese Methoden haben das Recht auf Leben, aber sie haben einige Nachteile. Die Art und Weise, wie sie die Daten auf undurchsichtige Weise verarbeiten, bedeutet, dass es schwierig sein wird zu verstehen, was im Inneren vor sich geht und ob die Leistung nachgelassen hat. Dies bedeutet auch, dass Sie die gesamte Abstraktionsebene studieren und sicherstellen mĂŒssen, dass sie gut zu vorhandenem Code passt. Vergessen Sie nicht versteckte Fehler, die wahrscheinlich viel in der Menge an Code versteckt sind, die Sie debuggen mĂŒssen.
Ein vorlagenbasierter Ansatz kann die Kompilierungszeit und die HĂ€ufigkeit, mit der Sie den Build neu erstellen mĂŒssen, erheblich beeinflussen. WĂ€hrend vererbungsbasierte Konzepte die Leistung beeintrĂ€chtigen können.
Der Hauptgrund, warum ich diese AnsĂ€tze fĂŒr ĂŒbertrieben halte, ist, dass das Problem, das sie lösen, zu einfach ist. Letztendlich sind dies nur zusĂ€tzliche Datenkomponenten, die der EntitĂ€t und ihrer selektiven Verarbeitung zugeordnet sind. Im Folgenden werde ich einen sehr einfachen Weg zeigen, wie dies implementiert werden kann.
Mein einfacher Ansatz
EssenzIn einigen AnsĂ€tzen ist die EntitĂ€tsklasse definiert, in anderen arbeiten sie mit EntitĂ€ten als ID / Handle. Bei einem Komponentenansatz ist eine EntitĂ€t nichts anderes als die ihr zugeordneten Komponenten, und dafĂŒr wird keine Klasse benötigt. Eine EntitĂ€t wird explizit basierend auf ihren zugehörigen Komponenten existieren. Definieren Sie dazu:
using EntityID = int64_t;
EntitĂ€tskomponentenKomponenten sind verschiedene Arten von Daten, die vorhandenen EntitĂ€ten zugeordnet sind. Wir können sagen, dass fĂŒr jede EntitĂ€t e null und leichter zugĂ€ngliche Komponententypen vorhanden sind. Im Wesentlichen handelt es sich hierbei um eine explodierte SchlĂŒssel-Wert-Beziehung, fĂŒr die es glĂŒcklicherweise Standardbibliothekstools in Form von Karten gibt.
Also definiere ich die Komponenten wie folgt:
struct Position { float x; float y; }; struct Velocity { float x; float y; }; struct Health { int max; int current; }; template <typename Type> using ComponentMap = std::unordered_map<EntityID, Type>; using Positions = ComponentMap<Position>; using Velocities = ComponentMap<Velocity>; using Healths = ComponentMap<Health>; struct Components { Positions positions; Velocities velocities; Healths healths; };
Dies reicht aus, um EntitÀten durch Komponenten anzuzeigen, wie von ECS erwartet. Um beispielsweise eine EntitÀt mit einer Position und Gesundheit, jedoch ohne Geschwindigkeit, zu erstellen, benötigen Sie:
Um eine EntitÀt mit einer bestimmten ID zu zerstören,
.erase()
wir sie einfach von jeder Karte.
SystemeDie letzte Komponente, die wir brauchen, sind Systeme. Dies ist die Logik, die mit Komponenten arbeitet, um ein bestimmtes Verhalten zu erreichen. Da ich Dinge gerne vereinfache, benutze ich normale Funktionen. Das oben erwÀhnte Gesundheitsregenerationssystem kann einfach die nÀchste Funktion sein.
void updateHealthRegeneration(int64_t currentFrame, Healths& healths) { if(currentFrame % 120 == 0) { for(auto& [id, health] : healths) { if(health.current < health.max) ++health.current; } } }
Wir können den Aufruf dieser Funktion an einer geeigneten Stelle in der Hauptschleife platzieren und ihn in den Speicher der IntegritĂ€tskomponente ĂŒbertragen. Da das IntegritĂ€tsrepository nur DatensĂ€tze fĂŒr EntitĂ€ten mit IntegritĂ€tsdaten enthĂ€lt, kann es diese isoliert verarbeiten. Dies bedeutet auch, dass die Funktion nur die erforderlichen Daten aufnimmt und die irrelevanten nicht berĂŒhrt.
Was aber, wenn das System mit mehr als einer Komponente arbeitet? Sagen wir ein physisches System, das seine Position basierend auf der Geschwindigkeit Ă€ndert. Dazu mĂŒssen wir alle SchlĂŒssel aller beteiligten Komponententypen schneiden und ĂŒber ihre Werte iterieren. Zu diesem Zeitpunkt reicht die Standardbibliothek nicht mehr aus, aber das Schreiben von Helfern ist nicht so schwierig. Zum Beispiel:
void updatePhysics(Positions& positions, const Velocities& velocities) {
Oder Sie können einen kompakteren Helfer schreiben, der einen effizienteren Zugriff ĂŒber Iteration anstelle der Suche ermöglicht.
void updatePhysics(Positions& positions, const Velocities& velocities) {
So haben wir uns mit der GrundfunktionalitÀt eines regulÀren ECS vertraut gemacht.
Die Vorteile
Dieser Ansatz ist sehr effektiv, da er von Grund auf neu erstellt wird, ohne die Abstraktion einzuschrĂ€nken. Sie mĂŒssen keine externen Bibliotheken integrieren oder die Codebasis an die vordefinierten Vorstellungen von EntitĂ€ten / Komponenten / Systemen anpassen.
Und da dieser Ansatz vollstĂ€ndig transparent ist, können Sie auf seiner Grundlage alle Dienstprogramme und Helfer erstellen. Diese Implementierung wĂ€chst mit den Anforderungen Ihres Projekts. FĂŒr einfache Prototypen oder Spiele fĂŒr Game Jam'ov verfĂŒgen Sie höchstwahrscheinlich ĂŒber genĂŒgend Funktionen, die oben beschrieben wurden.
Wenn Sie mit diesem gesamten ECS-Bereich noch nicht vertraut sind, hilft ein derart einfacher Ansatz, die Hauptideen zu verstehen.
EinschrÀnkungen
Wie bei jeder anderen Methode gibt es jedoch einige EinschrÀnkungen. Nach meiner Erfahrung ist es genau eine solche Implementierung mit
unordered_map
in einem nicht trivialen Spiel, die zu Leistungsproblemen fĂŒhrt.
Das Iterieren von SchlĂŒsselschnittpunkten auf mehreren Instanzen von
unordered_map
mit mehreren EntitÀten lÀsst sich nicht gut skalieren, da Sie tatsÀchlich
N*M
Lookups durchfĂŒhren, wobei N die Anzahl ĂŒberlappender Komponenten, M die Anzahl ĂŒbereinstimmender EntitĂ€ten und
unordered_map
Caching nicht sehr gut ist. Dieses Problem kann behoben werden, indem anstelle von
unordered_map
ein SchlĂŒsselwertspeicher verwendet wird, der besser fĂŒr die Iteration geeignet ist.
Eine weitere EinschrĂ€nkung ist das Boilerplating. Je nachdem, was Sie tun, kann das Identifizieren neuer Komponenten mĂŒhsam werden. Möglicherweise mĂŒssen Sie eine AnkĂŒndigung nicht nur in der Komponentenstruktur, sondern auch in der Spawn-Funktion, der Serialisierung, dem Debugging-Dienstprogramm usw. hinzufĂŒgen. Ich bin selbst darauf gestoĂen und habe das Problem durch Generieren von Code gelöst. Ich habe Komponenten in externen JSON-Dateien definiert und dann in der Erstellungsphase C ++ - Komponenten und Hilfsfunktionen generiert. Ich bin sicher, dass Sie andere Methoden finden können, die auf Vorlagen basieren, um alle Probleme mit Boilerplates zu beheben, auf die Sie stoĂen.
DAS ENDE
Wenn Sie Fragen und Kommentare haben, können Sie diese hier lassen oder zu
einer offenen Lektion mit
Dima gehen , ihm zuhören und bereits herumfragen.