Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Wikis des
Svelto.ECS- Projekts von Sebastiano Mandalà.
Svelto.ECS ist das Ergebnis langjähriger Forschung und Anwendung von SOLID-Prinzipien bei der Entwicklung von Spielen auf Unity. Dies ist eine der vielen Implementierungen des für C # verfügbaren ECS-Musters mit verschiedenen einzigartigen Funktionen, die eingeführt wurden, um die Mängel des Musters selbst zu beheben.
Erster Blick
Der einfachste Weg, um die Grundfunktionen von Svelto.ECS zu sehen, ist das Herunterladen von
Vanilla Example . Wenn Sie die Benutzerfreundlichkeit sicherstellen möchten, zeige ich Ihnen ein Beispiel:
Leider ist es nicht möglich, die Theorie hinter diesem Code schnell zu verstehen, die einfach, aber gleichzeitig verwirrend aussehen kann. Um dies zu verstehen, müssen Sie Zeit damit verbringen, die „Textwand“ zu lesen und die obigen Beispiele auszuprobieren.
Einführung
In letzter Zeit habe ich mit mehreren mehr oder weniger erfahrenen Programmierern
viel über
Svelto.ECS gesprochen . Ich habe viele Rückmeldungen gesammelt und viele Notizen gemacht, die ich als Ausgangspunkt für meine nächsten Artikel verwenden werde, in denen ich mehr über Theorie und bewährte Praktiken sprechen werde. Ein kleiner Spoiler: Ich habe festgestellt, dass die größte Hürde bei
der Verwendung von Svelto.ECS darin besteht,
das Programmierparadigma zu ändern . Es ist erstaunlich, wie viel ich schreiben muss, um die neuen Konzepte von Svelto.ECS zu erklären, verglichen mit der geringen Menge an Code, die zur Entwicklung des Frameworks geschrieben wurde. Während das Framework selbst sehr einfach und leicht ist, verhindert der Übergang von OOP unter aktiver Verwendung der Vererbung oder der üblichen Unity-Komponenten zu dem von Svelto.ECS angebotenen „neuen“ modularen und lose gekoppelten Design, dass sich Benutzer an das Framework anpassen.
Svelto.ECS wird in
Freejam aktiv verwendet (Anmerkung des Übersetzers - Der Autor ist der technische Direktor dieser Firma). Da ich meinen Kollegen immer die grundlegenden Konzepte des Frameworks erklären kann, brauchen sie weniger Zeit, um die Arbeit damit zu verstehen. Obwohl Svelto.ECS so hart wie möglich ist, sind schlechte Gewohnheiten schwer zu überwinden, sodass Benutzer dazu neigen, eine gewisse Flexibilität zu missbrauchen, die es ihnen ermöglicht, das Framework an die „alten“ Paradigmen anzupassen, mit denen sie vertraut sind. Dies kann aufgrund von Missverständnissen oder Verzerrungen der der Rahmenlogik zugrunde liegenden Konzepte zu einer Katastrophe führen. Aus diesem Grund beabsichtige ich, so viele Artikel wie möglich zu schreiben, zumal ich sicher bin, dass das ECS-Paradigma derzeit die beste Lösung ist, um effektiven und unterstützten Code für große Projekte zu schreiben, die sich über mehrere Jahre hinweg mehrmals ändern und überarbeiten.
Robocraft und
Cardlife sind ein Beweis dafür.
Ich werde nicht viel über die Theorien sprechen, die diesem Artikel zugrunde liegen. Ich möchte Sie nur daran erinnern, warum ich mich geweigert habe, den
IoC-Container zu verwenden, und ausschließlich das ECS-Framework verwendet habe: Der IoC-Container ist ein sehr gefährliches Werkzeug, wenn er verwendet wird, ohne das Wesentliche der Steuerungsinversion zu verstehen. Wie Sie aus meinen vorherigen Artikeln sehen können, unterscheide ich zwischen der Inversion der Erstellungssteuerung (Inversion der Erstellungssteuerung) und der Inversion der Flusssteuerung (Inversion der Flusssteuerung). Das Umkehren der Flusskontrolle ist wie Hollywoods Prinzip: "Rufen Sie uns nicht an, wir rufen Sie an." Dies bedeutet, dass injizierte Abhängigkeiten niemals direkt über öffentliche Methoden verwendet werden sollten, da Sie dabei einfach den IoC-Container als Ersatz für jede andere Form der globalen Injektion verwenden, z. B. Singleton. Wenn der IoC-Container jedoch auf der Grundlage von Inversion of Management (IoC) verwendet wird, kommt es im Wesentlichen darauf an, das Muster „Vorlagenmethode“ wiederzuverwenden, um Manager einzuführen, die nur zum Registrieren von von ihnen verwalteten Objekten verwendet werden. Im realen Kontext von Flusskontrollinversionen sind Manager immer für die Verwaltung von Entitäten verantwortlich. Sieht das aus wie ein ECS-Muster? Natürlich. Basierend auf dieser Überlegung habe ich das ECS-Muster genommen und ein starres Framework darauf basierend entwickelt, und seine Verwendung ist gleichbedeutend mit der Anwendung des neuen Programmierparadigmas.
Composition Root und EnginesRoot
Die Hauptklasse ist die Kompositionswurzel der Anwendung. Die Wurzel der Komposition ist der Ort, an dem Abhängigkeiten erstellt und implementiert werden (darüber habe ich in meinen Artikeln viel gesprochen). Eine Kompositionswurzel gehört zu einem Kontext, aber ein Kontext kann mehr als eine Kompositionswurzel haben. Zum Beispiel ist die Fabrik die Wurzel der Komposition. Eine Anwendung kann mehr als einen Kontext haben, dies ist jedoch ein erweitertes Szenario. In diesem Beispiel wird dies nicht berücksichtigt.
Bevor wir uns mit dem Code befassen, sollten wir uns mit den ersten Regeln der Sprache Svelto.ECS vertraut machen. ECS ist die Abkürzung Entity Component System. Die ECS-Infrastruktur wurde in Artikeln von vielen Autoren gut analysiert, aber während die Grundkonzepte allgemein sind, variieren die Implementierungen stark. Erstens gibt es keinen Standardweg, um einige Probleme zu lösen, die bei der Verwendung von ECS-orientiertem Code auftreten. In Bezug auf dieses Thema mache ich den größten Teil meiner Bemühungen, aber ich werde später oder in den folgenden Artikeln darüber sprechen. Die Theorie basiert auf den Konzepten von Essenz, Komponenten (Entitäten) und Systemen. Obwohl ich verstehe, warum das Wort System in der Vergangenheit verwendet wurde, fand ich es von Anfang an nicht intuitiv genug für diesen Zweck. Daher habe ich die Engine als Synonym für das System verwendet, und Sie können je nach Ihren Vorlieben einen dieser Begriffe verwenden.
Die EnginesRoot-Klasse ist der Kern von Svelto.ECS. Mit seiner Hilfe können Sie Engines registrieren und die gesamte Essenz des Spiels entwerfen. Das dynamische Erstellen von Engines ist wenig sinnvoll. Daher sollten sie alle der EnginesRoot-Instanz aus demselben Stammverzeichnis der Komposition hinzugefügt werden, in der sie erstellt wurden. Aus ähnlichen Gründen sollte eine EnginesRoot-Instanz niemals bereitgestellt werden, und Engines sollten nach dem Hinzufügen nicht gelöscht werden.
Um Abhängigkeiten zu erstellen und zu implementieren, benötigen wir mindestens eine Wurzel der Komposition. Ja, in einer Anwendung gibt es möglicherweise mehr als einen EnginesRoot, aber wir werden dies im aktuellen Artikel nicht ansprechen, den ich so weit wie möglich zu vereinfachen versuche. So sieht die Kompositionswurzel bei der Erstellung von Engines und der Abhängigkeitsinjektion aus:
void SetupEnginesAndEntities() {
Dieser Code stammt aus dem Survival-Beispiel, das jetzt auskommentiert ist und fast allen Regeln bewährter Verfahren entspricht, die ich anwenden möchte, einschließlich der Verwendung plattformunabhängiger und getesteter Engine-Logik. Kommentare helfen Ihnen, die meisten von ihnen zu verstehen, aber ein Projekt dieser Größe kann schwierig zu verstehen sein, wenn Sie neu bei Svelto sind.
Entitäten
Der erste Schritt nach dem Erstellen des leeren Stamms der Komposition und einer Instanz der EnginesRoot-Klasse besteht darin, die Objekte zu identifizieren, mit denen Sie zuerst arbeiten möchten. Es ist logisch, mit Entity Player zu beginnen. Die Essenz von Svelto.ECS sollte nicht mit dem Unity Game Object (GameObject) verwechselt werden. Wenn Sie andere Artikel zu ECS lesen, können Sie feststellen, dass Entitäten in vielen von ihnen häufig als Indizes bezeichnet werden. Dies ist wahrscheinlich der schlechteste Weg, um das ECS-Konzept einzuführen. Obwohl dies für Svelto.ECS zutrifft, ist es darin versteckt. Ich möchte, dass der Svelto.ECS-Benutzer jede Entität in Bezug auf die Game Design Domain-Sprache darstellt, beschreibt und identifiziert. Die Entität im Code muss das Objekt sein, das im Designdokument des Spiels beschrieben ist. Jede andere Form der Entitätsdefinition führt zu einer weit hergeholten Methode, um Ihre alten Ansichten an die Svelto.ECS-Prinzipien anzupassen. Befolgen Sie diese Grundregel und Sie werden sich nicht irren. Die Entitätsklasse selbst ist im Code nicht vorhanden, Sie sollten sie jedoch nicht abstrakt definieren.
Motoren
Der nächste Schritt besteht darin, darüber nachzudenken, welches Verhalten die Entitäten zu fragen haben. Jedes Verhalten wird immer innerhalb der Engine modelliert. Sie können keinen anderen Klassen in der Svelto.ECS-Anwendung Logik hinzufügen. Wir können beginnen, indem wir den Charakter des Spielers verschieben und die
PlayerMovementEngine- Klasse definieren. Der Name des Motors sollte sehr eng fokussiert sein, denn je spezifischer er ist, desto wahrscheinlicher ist es, dass der Motor der Einzelverantwortungsregel folgt. Die korrekte Benennung von Klassen in Svelto.ECS ist von grundlegender Bedeutung. Und das Ziel ist nicht nur, Ihre Absichten klar zu zeigen, sondern Ihnen auch zu helfen, sie selbst zu „sehen“.
Aus dem gleichen Grund ist es wichtig, dass sich Ihre Engine in einem sehr speziellen Namespace befindet. Wenn Sie Namespaces gemäß der Ordnerstruktur definieren, passen Sie sie an die Konzepte von Svelto.ECS an. Die Verwendung bestimmter Namespaces hilft dabei, Entwurfsfehler zu erkennen, wenn Entitäten in inkompatiblen Namespaces verwendet werden. Beispielsweise wird nicht davon ausgegangen, dass ein feindliches Objekt im Namespace des Spielers verwendet wird, es sei denn, das Ziel besteht darin, die Regeln zu brechen, die mit der Modularität und der schwachen Kopplung von Objekten verbunden sind. Die Idee ist, dass Objekte eines bestimmten Namespace nur innerhalb dieses oder des übergeordneten Namespace verwendet werden können. Mit Svelto.ECS ist es viel schwieriger, Ihren Code in Spaghetti umzuwandeln, in die Abhängigkeiten rechts und links eingefügt werden. Diese Regel hilft Ihnen dabei, die Messlatte für die Codequalität noch höher zu legen, wenn Abhängigkeiten zwischen Klassen korrekt abstrahiert werden.
In Svelto.ECS bewegt sich die Abstraktion einige Zeilen vorwärts, aber ECS hilft im Wesentlichen dabei, Daten von der Logik zu abstrahieren, die die Daten verarbeiten soll. Entitäten werden durch ihre Daten bestimmt, nicht durch ihr Verhalten. In diesem Fall ist Engines ein Ort, an dem Sie das gemeinsame Verhalten identischer Entitäten platzieren können, sodass Engines immer mit einer Reihe von Entitäten arbeiten können.
Svelto.ECS und das ECS-Paradigma ermöglichen es dem Encoder, einen der heiligen Grale der reinen Programmierung zu erreichen, der die ideale Verkapselung der Logik darstellt. Motoren sollten keine öffentlichen Funktionen haben. Die einzigen öffentlichen Funktionen, die vorhanden sein müssen, sind diejenigen, die zur Implementierung der Framework-Schnittstellen erforderlich sind. Dies führt dazu, dass die Abhängigkeitsinjektion vergessen wird, und hilft, fehlerhaften Code zu vermeiden, der bei Verwendung der Abhängigkeitsinjektion ohne Steuerungsinversion auftritt. Motoren sollten NIEMALS in einen anderen Motor oder eine andere Klasse eingebettet werden. Wenn Sie glauben, die Engine implementieren zu wollen, machen Sie einfach einen grundlegenden Fehler im Code-Design.
Im Vergleich zu Unity MonoBehaviours weisen Engines bereits den ersten großen Vorteil auf, nämlich den Zugriff auf alle Zustände von Entitäten dieses Typs aus demselben Codebereich. Dies bedeutet, dass der Code den Status aller Objekte problemlos direkt von derselben Stelle aus verwenden kann, an der die Logik des gemeinsamen Objekts ausgeführt wird. Darüber hinaus können einzelne Engines dieselben Objekte verarbeiten, sodass die Engine den Status des Objekts ändern kann, während die andere Engine es lesen kann, wobei effektiv zwei Engines für die Kommunikation über dieselben Entitätsdaten verwendet werden. Ein Beispiel finden Sie in den
Engines PlayerGunShootingEngine und
PlayerGunShootingFxsEngine . In diesem Fall befinden sich zwei Engines im selben Namespace, sodass sie dieselben Entitätsdaten gemeinsam nutzen können.
PlayerGunShootingEngine ermittelt, ob ein Spieler (Feind) beschädigt wurde, und schreibt den
lastTargetPosition- Wert der
IGunAttributesComponent- Komponente (die eine
PlayerGunEntity- Komponente ist).
PlayerGunShootFxsEngine verarbeitet die grafischen Effekte der Waffe und liest die Position des vom Spieler ausgewählten Ziels. Dies ist ein Beispiel für die Interaktion zwischen Engines durch Datenabfrage. Später in diesem Artikel werde ich zeigen, wie ein Mechanismus durch
Pushen von Daten (Daten-Pushing) oder
Datenbindung (Datenbindung ) zwischen ihnen kommunizieren kann. Logischerweise sollten Motoren niemals den Status speichern.
Motoren müssen nicht wissen, wie sie mit anderen Motoren interagieren sollen. Externe Kommunikation erfolgt durch Abstraktion, und Svelto.ECS löst die Verbindung zwischen den Motoren auf drei verschiedene offizielle Arten, aber ich werde später darauf eingehen. Die besten Motoren sind solche, die keine externe Kommunikation erfordern. Diese Engines spiegeln ein gut gekapseltes Verhalten wider und arbeiten normalerweise durch eine logische Schleife. Schleifen werden immer mit Svelto.Task-Tasks in Svelto.ECS-Anwendungen modelliert. Da die Bewegung des Spielers bei jedem physischen Tick aktualisiert werden muss, ist es selbstverständlich, eine Aufgabe zu erstellen, die bei jedem physischen Tick ausgeführt wird. Mit Svelto.Tasks können Sie jeden
IEnumerator- Typ auf mehreren Schedulertypen
ausführen . In diesem Fall haben wir beschlossen, eine Aufgabe in
PhysicScheduler zu erstellen, mit der Sie die Position des Spielers aktualisieren können:
public PlayerMovementEngine(IRayCaster raycaster, ITime time) { _rayCaster = raycaster; _time = time; _taskRoutine = TaskRunner.Instance.AllocateNewTaskRoutine() .SetEnumerator(PhysicsTick()).SetScheduler(StandardSchedulers.physicScheduler); } protected override void Add(PlayerEntityView entityView) { _taskRoutine.Start(); } protected override void Remove(PlayerEntityView entityView) { _taskRoutine.Stop(); } IEnumerator PhysicsTick() {
Svelto.Tasks-Aufgaben können direkt oder über
ITaskRoutine- Objekte ausgeführt werden. Ich werde hier nicht viel über Svelto sprechen. Aufgaben, da ich andere Artikel dafür geschrieben habe. Der Grund, warum ich mich für die Task-Routine entschieden habe, anstatt die IEnumerator-Implementierung direkt zu starten, liegt im Ermessen. Ich wollte zeigen, dass Sie einen Zyklus starten können, wenn das Objekt eines Spielers zur Engine hinzugefügt wird, und es stoppen können, wenn es gelöscht wird.
Dazu müssen Sie jedoch wissen, wann ein Objekt hinzugefügt und gelöscht wird.Svelto.ECS führt das Hinzufügen und Entfernen von Rückrufen ein , um zu wissen, wann bestimmte Entitäten hinzugefügt oder entfernt werden. Dies ist etwas Einzigartiges in Svelto.ECS, aber dieser Ansatz sollte mit Bedacht verwendet werden. Ich habe oft gesehen, dass diese Rückrufe missbraucht werden, da sie in vielen Fällen ausreichen, um Entitäten abzufragen. Selbst eine Entitätsreferenz als Engine-Feld sollte eher als Ausnahme als als Regel betrachtet werden.Nur wenn diese Rückrufe verwendet werden sollen, sollte die Engine entweder von SingleEntityViewEngine oder von MultiEntitiesViewEngine <EntityView1, ..., EntityViewN> geerbt werden. Auch hier sollte die Verwendung dieser Daten selten sein, und sie beabsichtigen in keiner Weise zu melden, welche Objekte die Engine verarbeiten wird.Engines implementieren am häufigsten die IQueryingEntityViewEngine- Schnittstelle . Auf diese Weise können Sie auf Daten aus einer Entitätsdatenbank zugreifen und diese extrahieren. Denken Sie daran, dass Sie jederzeit ein Objekt innerhalb der Engine anfordern können. Sobald Sie jedoch eine Entität anfordern, die nicht mit dem Namespace kompatibel ist, in dem sich die Engine befindet, sollten Sie verstehen, dass Sie bereits etwas falsch machen. Engines sollten niemals davon ausgehen, dass auf Entitäten zugegriffen werden kann, und sollten an einer Reihe von Objekten arbeiten. Es sollte nicht davon ausgegangen werden, dass immer nur ein Spieler im Spiel ist, wie ich es im Codebeispiel tue. In EnemyMovementEngine Es gibt einen sehr allgemeinen Ansatz zum Anfordern von Objekten: public void Ready() { Tick().Run(); } IEnumerator Tick() { while (true) { var enemyTargetEntityViews = entityViewsDB.QueryEntityViews<EnemyTargetEntityView>(); if (enemyTargetEntityViews.Count > 0) { var targetEntityView = enemyTargetEntityViews[0]; var enemies = entityViewsDB.QueryEntityViews<EnemyEntityView>(); for (var i = 0; i < enemies.Count; i++) { var component = enemies[i].movementComponent; component.navMeshDestination = targetEntityView.targetPositionComponent.position; } } yield return null; } }
In diesem Fall startet der Haupt-Engine-Zyklus direkt auf dem vordefinierten Scheduler. Häkchen () .Run ()zeigt den kürzesten Weg, um IEnumerator mit Svelto.Tasks zu starten. IEnumerator wird weiterhin dem nächsten Frame nachgeben, bis mindestens ein Enemy-Ziel gefunden wurde. Da wir wissen, dass es immer nur ein Ziel geben wird (eine weitere schlechte Annahme), wähle ich das erste verfügbare. Während das Ziel von Enemy Target nur eines sein kann (obwohl es mehr geben könnte!), Gibt es viele Feinde, und der Motor kümmert sich dennoch um die Logik der Bewegung für alle. In diesem Fall habe ich betrogen, da ich tatsächlich das Unity Nav Mesh-System verwende. Ich muss also nur das Ziel auf NavMesh setzen. Ehrlich gesagt habe ich den Unity NavMesh-Code nie verwendet, daher bin ich mir nicht einmal sicher, wie er funktioniert. Dieser Code wurde nur von der ursprünglichen Survival-Demo geerbt.Beachten Sie, dass eine Komponente niemals direkt eine Navmesh Unity-Abhängigkeit bereitstellt. Die Entitätskomponente sollte, wie ich später erläutern werde, immer Werttypen verfügbar machen. In diesem Fall können Sie mit dieser Regel auch den Code unter Kontrolle halten, da der Werttyp des Felds navMeshDestination später ohne Verwendung von Unity Nav Mesh implementiert werden kann.Um den Absatz über Motoren zu vervollständigen, beachten Sie, dass es keinen zu kleinen Motor gibt. Haben Sie daher keine Angst davor, eine Engine zu schreiben, die mehrere Codezeilen enthält, da Sie keine Logik an einer anderen Stelle schreiben können und Ihre Engines der Regel der einheitlichen Verantwortung folgen müssen.Entitätsdarstellungen
Zuvor haben wir das Konzept der Engine und die abstrakte Definition der Essenz eingeführt. Lassen Sie uns nun definieren, was die Darstellung der Essenz ist. Ich muss zugeben, dass von den 5 Konzepten, auf denen Svelto.ECS basiert, Entity Views wahrscheinlich die verwirrendsten sind. Früher Node genannt (ein Name aus dem ECS Ash-Framework ), wurde mir klar, dass der Name „Node“ nichts bedeutet. EntityView kann auch irreführend sein, da Programmierer Ansichten normalerweise einem Konzept zuordnen, das aus einer Vorlage stammt. Model View Controller(Model View Controller) Svelto.ECS verwendet jedoch View, da EntityView EntityView ist, wie die Engine Entity sieht. Ich beschreibe es gerne so, weil es am natürlichsten erscheint, aber ich könnte es auch EntityMap nennen, weil EntityView die Komponenten der Entität anzeigt, auf die die Engine zugreifen soll. Dieses Schema der Svelto.ECS-Konzepte sollte ein wenig helfen:
Ich schlage vor, mit der Engine zu beginnen, und jetzt sind wir auf der rechten Seite dieses Schemas. Jede Engine verfügt über einen eigenen Satz von EntityViews. Die Engine kann EntityViews, die mit dem Namespace kompatibel sind, wiederverwenden, aber meistens definiert die Engine ihre EntityViews. Die Engine kümmert sich nicht darum, ob die Player-Entität wirklich definiert ist, sondern gibt an, dass sie PlayerEntityView benötigtfür die Arbeit. Das Schreiben des Codes hängt von den Anforderungen der Engine ab. Sie sollten keine Entität und ihr Feld erstellen, bevor Sie nicht verstanden haben, wie sie verwendet werden. In einem komplexeren Szenario könnte der Name EntityView noch spezifischer sein. Wenn wir beispielsweise komplexe Engines schreiben müssten , um die Player-Logik zu handhaben und Player-Grafiken (oder Animationen usw.) zu rendern, könnten wir PlayerPhysicEngine mit PlayerPhysicEntityView sowie PlayerGraphicEngine mit PlayerGraphicEntityView oder PlayerAnimationEngine mit PlayerAnimationEntityView verwenden . Es können spezifischere Namen verwendet werden, z. B. PlayerPhysicMovementEngine oder PlayerPhysicJumpEngine (usw.).Komponenten
Wir haben festgestellt, dass Engines das Verhalten für eine Reihe von Entitätsdaten modellieren, und wir verstehen, dass Engines Entitäten nicht direkt verwenden, sondern Entitätskomponenten durch Darstellungen von Entitäten verwenden. Wir haben festgestellt, dass EntityView eine Klasse ist, die NUR öffentliche Komponenten von Entitäten enthalten kann. Ich habe auch angedeutet, dass Entitätskomponenten immer Schnittstellen sind. Geben wir daher eine bessere Definition:Entitäten sind eine Sammlung von Daten, und Entitätskomponenten sind eine Möglichkeit, auf diese Daten zuzugreifen. Wenn Sie dies noch nicht bemerkt haben, ist das Definieren von Entitätskomponenten als Schnittstellen eine weitere ziemlich einzigartige Funktion von Svelto.ECS. In der Regel sind Komponenten in anderen Frameworks Objekte. Die Verwendung von Schnittstellen kann den Code erheblich reduzieren. Wenn Sie dem Prinzip folgen„ Prinzip der Schnittstellentrennung“ Nachdem Sie kleine Komponentenschnittstellen geschrieben haben, auch mit jeweils einer Eigenschaft, werden Sie feststellen, dass Sie begonnen haben, Komponentenschnittstellen innerhalb verschiedener Entitäten wiederzuverwenden. In unserem Beispiel wird ITransformComponent in vielen Entitätsdarstellungen wiederverwendet. Durch die Verwendung von Komponenten als Schnittstellen können sie auch dieselben Objekte implementieren. Dies vereinfacht in vielen Fällen die Beziehung zwischen Entitäten, die dieselbe Entität sehen, unter Verwendung unterschiedlicher Darstellungen von Entitäten (oder, falls möglich, derselben).Daher ist in Svelto.ECS die Entitätskomponente immer eine Schnittstelle, und diese Schnittstelle wird nur über das Feld EntityView in der Engine verwendet. Die Entitätskomponentenschnittstelle wird dann von der sogenannten implementiert«». , .Komponenten sollten immer aussagekräftige Typen speichern, und Felder sind immer Eigenschaften. Ausnahmen können nur gemacht werden, um Setter und Getter als Methoden zur Verwendung des Schlüsselworts ref zu schreiben, wenn eine Optimierung erforderlich ist. Dies bedeutet nicht, dass der Code datenorientiert ist, aber Sie können Code für Tests erstellen, da die Logik der Engine keine Verknüpfungen zu externen Abhängigkeiten verarbeiten sollte. Außerdem wird dadurch verhindert, dass Codierer das Framework betrügen und öffentliche Funktionen (einschließlich Logik!) Von zufälligen Objekten verwenden. Der einzige Grund, warum Sie das Bedürfnis verspüren konnten, Links innerhalb der Schnittstellen von Entitätskomponenten zu verwenden, bestand darin, sich mit Abhängigkeiten von Drittanbietern wie Unity-Objekten zu befassen. Das Survival-Beispiel zeigt jedoch, wie Sie damit umgehen können:Verlassen des Engine-Testcodes, ohne sich um Unity-Abhängigkeiten kümmern zu müssen.Hier kommen Entity Descriptors zur Rettung, um alles zusammenzusetzen. Wir wissen, dass Engines über Komponenten, die in Entitätsansichten gespeichert sind, auf Entitätsdaten zugreifen können. Wir wissen, dass Engines Klassen sind, EntityView Klassen, die nur Komponentenentitäten enthalten, und dass Komponenten Schnittstellen sind. Obwohl ich eine abstrakte Definition der Essenz gegeben habe, haben wir keine einzige Klasse gesehen, die tatsächlich die Essenz darstellt. Dies entspricht dem Konzept von Objekten, die Identifikatoren innerhalb des modernen ECS-Systems sind. Ohne die korrekte Definition von Entitäten werden die Codierer jedoch gezwungen, Entitäten mit Repräsentationen von Entitäten zu identifizieren, was katastrophal falsch wäre. Die Darstellung von Entitäten ist die Art und Weise, wie mehrere Engines dieselbe Entität sehen können.aber sie sind keine Entitäten. Die Entität selbst sollte immer als ein Datensatz betrachtet werden, der durch die Komponenten der Entität definiert wird, aber selbst dies ist eine schwache Definition. Eine EntityDescriptor-Instanz ermöglicht es dem Encoder, seine Entitäten korrekt zu bestimmen, unabhängig von den Engines, die sie verarbeiten. Daher benötigen wir im Fall von Entity PlayerPlayerEntityDescriptor . Diese Klasse wird zum Erstellen von Entitäten verwendet, und obwohl das, was sie wirklich tut, etwas völlig anderes ist, hilft die Tatsache, dass der Benutzer BuildEntity <PlayerEntityDescriptor> () schreiben kann, sehr einfach, Entitäten zum Erstellen und Kommunizieren von Absichten für andere zu visualisieren. Encoder.Was EntityDescriptor jedoch wirklich tut, ist eine EntityViews-Liste zu erstellen !!! In den frühen Phasen der Entwicklung des Frameworks erlaubte ich Codierern, diese EntityViews-Liste manuell zu erstellen, was zu sehr hässlichem Code führte, da nicht mehr visualisiert werden konnte, was tatsächlich geschah.So sieht PlayerEntityDescriptor aus : using Svelto.ECS.Example.Survive.Camera; using Svelto.ECS.Example.Survive.HUD; using Svelto.ECS.Example.Survive.Enemies; using Svelto.ECS.Example.Survive.Sound; namespace Svelto.ECS.Example.Survive.Player { public class PlayerEntityDescriptor : GenericEntityDescriptor<HUDDamageEntityView, PlayerEntityView, EnemyTargetEntityView, DamageSoundEntityView, HealthEntityView, CameraTargetEntityView> { } }
Entitätsdeskriptoren (und Implementierer) sind die einzigen Klassen, die Bezeichner aus mehreren Namespaces verwenden können. In diesem Fall definiert der PlayerEntityDescriptor eine Liste von EntityViews, die beim Erstellen der PlayerEntity instanziiert und in die Engine eingefügt werden sollen.EntityDescriptorHolder
EntityDescriptorHolder ist eine Erweiterung für Unity und sollte nur in bestimmten Fällen verwendet werden. Am häufigsten wird eine Art Polymorphismus erstellt, in dem Informationen zu Entitäten zum Erstellen eines Unity GameObject gespeichert werden. Somit kann derselbe Code verwendet werden, um mehrere Arten von Entitäten zu erstellen. Bei Robocraft verwenden wir beispielsweise eine einzelne Würfelfabrik, in der alle Würfel hergestellt werden, aus denen die Maschinen bestehen. Der Würfeltyp für die Montage wird im Fertighaus des Würfels selbst gespeichert. Dies ist gut, solange die Implementierer zwischen den Cubes oder in GameObject dieselben sind wie die von MonoBehaviour. Das direkte Erstellen von Entitäten ist vorzuziehen. Verwenden Sie EntityDescriptorHolders daher nur, wenn Sie die Prinzipien von Svelto.ECS richtig verstanden haben. Andernfalls besteht die Gefahr eines Missbrauchs. Diese Funktion aus dem Beispiel zeigt, wie die Klasse verwendet wird: void BuildEntitiesFromScene(UnityContext contextHolder) {
Beachten Sie, dass ich in diesem Beispiel eine weniger bevorzugte, nicht generische BuildEntity- Funktion verwende . Ich werde das erklären. In diesem Fall sind die Implementierer die MonoBehaviour-Klassen, die an das GameObject angehängt sind. Dies ist keine gute Praxis. Ich hätte diesen Code aus dem Beispiel entfernen sollen, bin aber gegangen, um Ihnen diesen Sonderfall zu zeigen. Implementierer sollten, wie wir später sehen werden, nur bei Bedarf MonoBehaviours-Klassen sein!Impulatoren
Bevor wir unsere Essenz erstellen, definieren wir das letzte Konzept in Svelto.ECS, nämlich den Impaler . Wie wir wissen, sind Entitätskomponenten immer Schnittstellen, und C # -Schnittstellen müssen implementiert werden. Ein Objekt, das diese Schnittstellen implementiert, wird als "Implementierer" bezeichnet. Implementierer haben mehrere wichtige Merkmale:- Die Möglichkeit, die Anzahl der zusammenzusetzenden Objekte von der Anzahl der Entitätskomponenten zu trennen, die zur Bestimmung der Entitätsdaten erforderlich sind.
- Die Möglichkeit, Daten zwischen verschiedenen Komponenten auszutauschen, da Komponenten Daten über Eigenschaften bereitstellen, können verschiedene Eigenschaften einer Komponente dasselbe Implementierungsfeld zurückgeben.
- Möglichkeit zum Erstellen einer Stub-Entität für Schnittstellenkomponenten. Dies ist wichtig, um den Motorcode testen zu lassen.
- Svelto.ECS (third party) . . Unity, , , Monobehaviour . , Unity, OnTriggerEnter / OnTriggerExit , Unity. , . :
public class EnemyTriggerImplementor : MonoBehaviour, IImplementor, IEnemyTriggerComponent, IEnemyTargetComponent { public event Action<int, int, bool> entityInRange; bool IEnemyTriggerComponent.targetInRange { set { _targetInRange = value; } } bool IEnemyTargetComponent.targetInRange { get { return _targetInRange; } } void OnTriggerEnter(Collider other) { if (entityInRange != null) entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), true); } void OnTriggerExit(Collider other) { if (entityInRange != null) entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), false); } bool _targetInRange; }
, , . , .Entitätserstellung
Angenommen, wir haben unsere Engines erstellt , sie zu EnginesRoot hinzugefügt und ihre Entitätsansichten erstellt , für die Komponenten als Schnittstellen erforderlich sind, die in den Implementierern implementiert werden . Es ist Zeit, unsere erste Essenz zu erschaffen. Eine Entität wird immer über eine Instanz der Entity Factory erstellt, die von EnginesRoot über die GenerateEntityFactory- Funktion erstellt wurde . Im Gegensatz zu einer EnginesRoot-Instanz kann eine IEntityFactory-Instanz bereitgestellt und übertragen werden. Objekte können innerhalb des Kompositionsstamms oder dynamisch innerhalb der Fabriken erstellt werden. In letzterem Fall müssen Sie eine IEntityFactory über einen Parameter übergeben.IEntityFactory bietet mehrere ähnliche Funktionen. In diesem Artikel werde ich die Erklärung Funktionen überspringen PreallocateEntitySlots und BuildMetaEntity , auf die am häufigsten verwendeten Funktionen zu konzentrieren BuildEntity und BuildEntityInGroup .Es ist am besten, immer BuildEntityInGroup zu verwenden , aber für das Survival-Beispiel ist dies nicht erforderlich. Sehen wir uns also an, wie die übliche BuildEntity im Beispiel verwendet wird: IEnumerator IntervaledTick() {
Denken Sie daran, alle Kommentare in diesem Beispiel zu lesen, damit Sie die Konzepte von Svelto.ECS besser verstehen. Aufgrund der Einfachheit des Beispiels verwende ich nicht die BuildEntityInGroup , die in komplexeren Projekten verwendet wird. In Robocraft verarbeitet jede Engine, die die Logik von Funktionswürfeln verarbeitet, die Logik ALLER Funktionswürfel dieses bestimmten Typs im Spiel. Es ist jedoch häufig erforderlich zu wissen, zu welchem Fahrzeug die Würfel gehören. Wenn Sie also für jede Maschine eine Gruppe verwenden, können Sie die Würfel desselben Typs in Maschinen aufteilen, wobei die Maschinen-ID die Gruppen-ID ist. Auf diese Weise können wir coole Dinge implementieren, z. B. das Ausführen einer Svelto.Tasks-Aufgabe auf einem Computer innerhalb derselben Engine, die mithilfe von Multithreading parallel arbeiten kann.Dieser Code zeigt ein wichtiges Problem, das ich in den folgenden Artikeln ausführlicher behandeln werde ... aus dem Kommentar (falls Sie ihn nicht gelesen haben):Erstellen Sie niemals MonoBehaviour-Imprementoren nur zur Datenspeicherung. Daten sollten unabhängig von der Datenquelle immer über die Service-Schicht abgerufen werden. Die Vorteile sind zahlreich, einschließlich der Tatsache, dass Sie zum Ändern der Datenquelle nur den Servicecode ändern müssen. In diesem einfachen Beispiel verwende ich nicht die Service-Schicht, aber im Allgemeinen ist die Idee klar. Beachten Sie auch, dass ich Daten für jeden Anwendungsstart außerhalb der Hauptschleife nur einmal hochlade. Sie können diesen Trick immer verwenden, wenn die benötigten Daten nie geändert werden.Anfangs habe ich Daten direkt von MonoBehaviour gelesen, wie es ein guter Lazy Encoder tun würde. Dadurch habe ich einen schreibgeschützten MonoBehaviore-Serializer-Implementierer erstellt. Dies ist akzeptabel, wenn wir die Datenquelle nicht abstrahieren möchten. Es ist jedoch viel besser, die Informationen in eine JSON-Datei zu serialisieren und auf Anfrage an den Dienst zu lesen, als diese Daten aus der Entitätskomponente zu lesen.Kommunikation bei Svelto.ECS
Ein Problem, dessen Lösung durch keine ECS-Implementierung standardisiert wurde, ist die Kommunikation zwischen Systemen. Dies ist ein weiterer Ort, an dem ich viel nachgedacht habe, und Svelto.ECS löst es auf zwei neue Arten. Die dritte Möglichkeit besteht darin, das Standardmuster "Beobachter / Beobachtet" zu verwenden, das in sehr spezifischen und spezifischen Fällen akzeptabel ist.DispatchOnSet / DispatchOnChange
Zuvor haben wir gesehen, wie Engines mithilfe von Data Polling Daten über Entity Components austauschen können. DispatchOnSet und DispatchOnChange sind die einzigen Referenzen (nicht signifikante Typen), die von den Eigenschaften von Entitätskomponenten zurückgegeben werden können, aber der Typ des generischen Parameters T muss ein aussagekräftiger Typ sein. Die Namen der Funktionen klingen wie ein Ereignis-Dispatcher, sollten jedoch als Methoden zum Pushen der Daten betrachtet werden, im Gegensatz zu Datenabfragen, die ein bisschen wie Datenbindung sind. Das ist alles, manchmal ist das Abrufen von Daten unpraktisch. Wir möchten nicht in jedem Frame eine Variable abfragen, wenn wir wissen, dass sich Daten selten ändern. DispatchOnSet und DispatchOnChangekann nicht gestartet werden, ohne die Daten zu ändern. Dies ermöglicht es uns, sie als Datenbindungsmechanismus anstelle eines regulären Ereignisses zu betrachten. Es gibt auch keine Startfunktion zum Aufrufen. Stattdessen muss der Wert der von diesen Klassen gehaltenen Daten festgelegt oder geändert werden. Im Survival - Code keine großen Beispielen, aber man kann sehen , wie ein boolean Feld targetHit von IGunHitTargetComponent . Der Unterschied zwischen DispatchOnSet und DispatchOnChange besteht darin, dass letzteres das Ereignis nur auslöst , wenn sich die Daten tatsächlich ändern, und erstere immer.Sequenzer
Ideale Engines sind vollständig gekapselt, und Sie können die Logik dieser Engine als Folge von Anweisungen mit Svelto.Tasks und IEnumerators schreiben. Dies ist jedoch nicht immer möglich, da Engines in einigen Fällen Ereignisse an andere Engines senden müssen. Dies erfolgt normalerweise über Entitätsdaten, insbesondere mit DispatchOnSet und DispatchOnChangeWie im Fall von Entitäten, die im Beispiel „beschädigt“ sind, wirkt jedoch eine Reihe unabhängiger und nicht verwandter Motoren darauf ein. In anderen Fällen möchten Sie, dass die Reihenfolge in der Reihenfolge, in der die Motoren aufgerufen wurden, streng ist, wie in dem Beispiel, in dem der Tod für letztere immer eintreten soll. In diesem Fall ist die Sequenz nicht nur sehr einfach zu bedienen, sondern auch sehr praktisch! Sequenz-Refactoring ist sehr einfach. Verwenden Sie daher IEnumerator Svelto-Aufgaben für "vertikale" Engines und Sequenzen für "horizontale" Logik zwischen Engines.Beobachter / Beobachtet
Ich habe die Möglichkeit verlassen, dieses Muster speziell für Fälle zu verwenden, in denen Legacy-Code oder Code, der Svelto.ECS nicht verwendet, mit den Svelto.ECS-Engines interagieren sollte. In anderen Fällen sollte es mit äußerster Vorsicht verwendet werden, da die Möglichkeit eines Missbrauchs des Musters besteht, da es den meisten Codierern, die neu in Svelto.ECS sind, bekannt ist und Sequenzer normalerweise die beste Wahl sind.