Welche Designs gibt es für ein komponentenbasiertes Entitätssystem, das benutzerfreundlich und dennoch flexibel ist?

23

Ich habe mich schon seit einiger Zeit für das komponentenbasierte Entity-System interessiert und unzählige Artikel darüber gelesen (Die Insomiac-Spiele , der hübsche Standard Evolve Your Hierarchy , die T-Machine , Chronoclast ... um nur einige zu nennen).

Sie scheinen alle eine Struktur von außen zu haben, die etwa so aussieht:

Entity e = Entity.Create();
e.AddComponent(RenderComponent, ...);
//do lots of stuff
e.GetComponent<PositionComponent>(...).SetPos(4, 5, 6);

Und wenn Sie die Idee der gemeinsamen Nutzung von Daten einbringen (dies ist das beste Design, das ich bisher gesehen habe, um Daten nicht überall zu duplizieren).

e.GetProperty<string>("Name").Value = "blah";

Ja, das ist sehr effizient. Es ist jedoch nicht gerade am einfachsten zu lesen oder zu schreiben. es fühlt sich sehr klobig an und arbeitet gegen dich.

Ich persönlich würde gerne etwas machen wie:

e.SetPosition(4, 5, 6);
e.Name = "Blah";

Natürlich ist der einzige Weg, zu dieser Art von Design zu gelangen, zurück in der Entität-> NPC-> Feind-> Fliegender Feind-> Fliegender Feind mit einer Art Hierarchie, die dieses Design zu vermeiden versucht.

Hat jemand ein Design für diese Art von Komponentensystem gesehen, das immer noch flexibel ist und dennoch ein hohes Maß an Benutzerfreundlichkeit bietet? Und was das angeht, schafft man es, die (wahrscheinlich schwierigste) Datenspeicherung auf eine gute Art und Weise zu umgehen?

Welche Designs gibt es für ein komponentenbasiertes Entitätssystem, das benutzerfreundlich und dennoch flexibel ist?

Die kommunistische Ente
quelle

Antworten:

13

Eines der Dinge, die Unity tut, ist das Bereitstellen einiger Hilfszugriffsfunktionen für das übergeordnete Spielobjekt, um einen benutzerfreundlicheren Zugriff auf allgemeine Komponenten zu ermöglichen.

Beispielsweise könnte Ihre Position in einer Transformationskomponente gespeichert sein. Mit Ihrem Beispiel müssten Sie so etwas schreiben

e.GetComponent<Transform>().position = new Vector3( whatever );

Aber in Unity wird das vereinfacht

e.transform.position = ....;

Wobei transformes sich buchstäblich nur um eine einfache Hilfsmethode in der Basisklasse GameObject( Entityin Ihrem Fall Klasse) handelt

Transform transform
{ 
    get { return this.GetComponent<Transform>(); }
}

Unity führt auch einige andere Aktionen aus, beispielsweise das Festlegen einer "Name" -Eigenschaft für das Spielobjekt selbst anstelle der untergeordneten Komponenten.

Persönlich mag ich die Idee, dass Sie Daten nach Namen teilen, nicht. Zugriff auf Eigenschaften von Namen statt durch eine Variable ist und den Benutzer auch müssen wissen , welche Art ist es scheint Fehler nur wirklich anfällig für mich. Was Unity macht, ist, dass sie ähnliche Methoden anwenden wie dieGameObject transform Eigenschaft in den ComponentKlassen, um auf Komponenten zuzugreifen. Jede beliebige Komponente, die Sie schreiben, kann dies einfach tun:

var myPos = this.transform.position;

Um auf die Position zuzugreifen. Wo dastransform Eigenschaft so etwas tut

Transform transform
{
    get { return this.gameObject.GetComponent<Transform>(); }
}

Sicher, es ist etwas ausführlicher als nur zu sagen e.position = whatever, aber man gewöhnt sich daran und es sieht nicht so böse aus wie die generischen Eigenschaften. Und ja, Sie müssten dies auf Umwegen für Ihre Client-Komponenten tun, aber die Idee ist, dass alle Ihre gängigen "Engine" -Komponenten (Renderer, Collider, Audioquellen, Transformationen usw.) über die einfachen Zugriffsmethoden verfügen.

Tetrade
quelle
Ich kann sehen, warum das System mit gemeinsam genutzten Daten nach Namen ein Problem sein kann, aber wie kann ich das Problem vermeiden, dass mehrere Komponenten Daten benötigen (dh in welchen Komponenten speichere ich etwas)? Gerade in der Entwurfsphase sehr vorsichtig zu sein?
Die kommunistische Ente
Könnte ich es nicht auch im Sinne von "Der Benutzer muss auch wissen, welcher Typ es ist" einfach in property.GetType () in der get () -Methode umwandeln?
Die kommunistische Ente
1
Welche Art von globalen Daten (im Entitätsbereich) übertragen Sie in diese gemeinsam genutzten Datenrepositorys? Jede Art von Spielobjektdaten sollte über Ihre "Engine" -Klassen (Transformation, Collider, Renderer usw.) leicht zugänglich sein. Wenn Sie Änderungen an der Spiellogik vornehmen, die auf diesen "freigegebenen Daten" basieren, ist es viel sicherer, wenn eine Klasse diese besitzt und tatsächlich sicherstellt, dass sich die Komponente auf dem Spielobjekt befindet, als nur blind nachzufragen, ob eine benannte Variable vorhanden ist. Also ja, das sollten Sie in der Entwurfsphase erledigen. Sie können jederzeit eine Komponente erstellen, in der Daten nur dann gespeichert werden, wenn Sie sie wirklich benötigen.
Tetrad
@Tetrad - Können Sie kurz erläutern, wie Ihre vorgeschlagene GetComponent <Transform> -Methode funktionieren würde? Würde es alle GameObject-Komponenten durchlaufen und die erste Komponente des Typs T zurückgeben? Oder ist da noch was los?
Michael
@Michael das würde funktionieren. Sie könnten es einfach zur Verantwortung des Anrufers machen, dies als Leistungsverbesserung lokal zwischenzuspeichern.
Tetrad
3

Ich würde vorschlagen, eine Art Interface-Klasse für Ihre Entity-Objekte wäre schön. Es könnte die Fehlerbehandlung mit der Überprüfung durchführen, um sicherzustellen, dass eine Entität auch eine Komponente des entsprechenden Werts an einem Ort enthält, sodass Sie dies nicht überall tun müssen, wo Sie auf die Werte zugreifen. Alternativ beschäftige ich mich bei den meisten Entwürfen, die ich jemals mit einem komponentenbasierten System durchgeführt habe, direkt mit den Komponenten, fordere beispielsweise deren Positionskomponente an und greife dann direkt auf die Eigenschaften dieser Komponente zu bzw. aktualisiere sie.

Auf hoher Ebene sehr einfach, nimmt die Klasse die betreffende Entität auf und bietet eine benutzerfreundliche Schnittstelle zu den zugrunde liegenden Komponententeilen wie folgt:

public class EntityInterface
{
    private Entity entity { get; set };
    public EntityInterface(Entity e)
    {
        entity = e;
    }

    public Vector3 Position
    {
        get
        {
            return entity.GetProperty<Vector3>("Position");
        }
        set
        {
            entity.GetProperty<Vector3>("Position") = value;
        }
    }
}
James
quelle
Wenn ich davon ausgehe, dass ich nur für NoPositionComponentException oder so etwas vorgehen muss, was ist der Vorteil davon, wenn ich das alles in die Entity-Klasse setze?
Die kommunistische Ente
Da ich nicht sicher bin, was Ihre Entitätsklasse tatsächlich enthält, kann ich nicht sagen, ob es einen Vorteil gibt oder nicht. Ich persönlich befürworte Komponentensysteme, bei denen es keine Basiseinheit gibt, nur um die Frage zu vermeiden, was eigentlich dazugehört. Allerdings würde ich eine solche Interface-Klasse als ein gutes Argument dafür ansehen, dass sie existiert.
James
Ich kann sehen, wie das einfacher ist (ich muss die Position nicht über GetProperty abfragen), aber ich sehe den Vorteil dieser Methode nicht, anstatt sie in der Entity-Klasse selbst zu platzieren.
Die kommunistische Ente
2

In Python können Sie den Teil 'setPosition' abfangen, e.SetPosition(4,5,6)indem Sie eine __getattr__Funktion für Entity deklarieren . Diese Funktion kann die Komponenten durchlaufen und die entsprechende Methode oder Eigenschaft finden und diese zurückgeben, sodass der Funktionsaufruf oder die Funktionszuweisung an die richtige Stelle gelangt. Vielleicht hat C # ein ähnliches System, aber es ist möglicherweise nicht möglich, weil es statisch typisiert ist - es kann vermutlich nicht kompiliert werden, e.setPositionwenn e nicht irgendwo setPosition in der Schnittstelle hat.

Möglicherweise können Sie auch alle Entitäten veranlassen, die relevanten Schnittstellen für alle Komponenten zu implementieren, die Sie ihnen hinzufügen, und zwar mit Stub-Methoden, die eine Ausnahme auslösen. Wenn Sie dann addComponent aufrufen, leiten Sie einfach die Funktionen der Entität für die Schnittstelle dieser Komponente zur Komponente um. Ein bisschen fummelig.

Am einfachsten ist es jedoch, den Operator [] in Ihrer Entity-Klasse zu überladen, um die angehängten Komponenten auf das Vorhandensein einer gültigen Eigenschaft zu durchsuchen und diese zurückzugeben e["Name"] = "blah". Jede Komponente muss ihr eigenes GetPropertyByName-Objekt implementieren, und die Entität ruft jedes einzeln auf, bis sie die Komponente findet, die für die betreffende Eigenschaft verantwortlich ist.

Kylotan
quelle
Ich hatte überlegt, Indexer zu verwenden. Das ist wirklich nett. Ich werde etwas warten, um zu sehen, was sonst noch erscheint, aber ich strebe eine Mischung aus diesem, dem üblichen GetComponent () und einigen Eigenschaften wie @James an, die für allgemeine Komponenten vorgeschlagen wurden.
Die kommunistische Ente
Ich kann vermissen, was hier gesagt wird, aber dies scheint eher ein Ersatz für e.GetProperty <Typ> ("NameOfProperty"). Was auch immer mit e ["NameOfProperty"]. Was auch immer?
James
@James Es ist ein Wrapper dafür (ähnlich wie Ihr Design) .. außer es ist dynamischer - es macht es automatisch und sauberer als die GetPropertyMethode .. Einziger Nachteil ist die String-Manipulation und der Vergleich. Aber das ist ein strittiger Punkt.
Die kommunistische Ente
Ah, mein Missverständnis dort. Ich nahm an, dass die GetProperty Teil der Entität war (und das, was oben beschrieben wurde) und nicht die C # -Methode.
James
2

Um die Antwort von Kylotan zu erweitern, können Sie bei Verwendung von C # 4.0 statisch eine Variable eingeben, die dynamisch sein soll .

Wenn Sie von System.Dynamic.DynamicObject erben, können Sie TryGetMember und TrySetMember (unter den vielen virtuellen Methoden) überschreiben, um Komponentennamen abzufangen und die angeforderte Komponente zurückzugeben.

dynamic entity = EntityRegistry.Entities[1000];
entity.MyComponent.MyValue = 100;

Ich habe über die Jahre ein bisschen über Entity-Systeme geschrieben, aber ich gehe davon aus, dass ich nicht mit den Giganten mithalten kann. Einige ES-Notizen (etwas veraltet und spiegeln nicht unbedingt mein aktuelles Verständnis von Entitätssystemen wider, aber es ist eine Lektüre wert, imho).

Raine
quelle
Vielen Dank, wird helfen :) Würde die dynamische Methode nicht die gleiche Wirkung haben wie das Casting in property.GetType ()?
Die kommunistische Ente
Irgendwann wird es eine Umwandlung geben, da TryGetMember einen out- objectParameter hat - aber all dies wird zur Laufzeit aufgelöst. Ich denke, es ist eine verherrlichte Möglichkeit, eine flüssige Schnittstelle über zB entity.TryGetComponent (compName, out component) zu halten. Ich bin nicht sicher, ob ich die Frage verstanden habe :)
Raine
Die Frage ist, dass ich überall dynamische Typen verwenden oder (AFAICS) die Eigenschaft (property.GetType ()) zurückgeben könnte.
Die kommunistische Ente
1

Ich sehe hier viel Interesse an ES auf C #. Schauen Sie sich meinen Port der exzellenten ES-Implementierung von Artemis an:

https://github.com/thelinuxlich/artemis_CSharp

Ein Beispielspiel, mit dem Sie beginnen können, wie es funktioniert (mit XNA 4): https://github.com/thelinuxlich/starwarrior_CSharp

Vorschläge sind willkommen!

thelinuxlich
quelle
Sieht auf jeden Fall gut aus. Ich persönlich bin kein Fan der Idee von so vielen EntityProcessingSystems - wie funktioniert das im Code in Bezug auf die Nutzung?
Die kommunistische Ente
Jedes System ist ein Aspekt, der seine abhängigen Komponenten verarbeitet. Sehen Sie sich das an, um auf die Idee zu kommen: github.com/thelinuxlich/starwarrior_CSharp/tree/master/…
thelinuxlich
0

Ich habe mich für das exzellente Reflexionssystem von C # entschieden. Ich habe es aus dem allgemeinen Komponentensystem und einer Mischung der Antworten hier abgeleitet.

Komponenten werden sehr einfach hinzugefügt:

Entity e = new Entity(); //No templates/archetypes yet.
e.AddComponent(new HealthComponent(100));

Und kann entweder nach Name, Typ oder (falls üblich, nach Zustand und Position) nach Eigenschaften abgefragt werden:

e["Health"] //This, unfortunately, is a bit slower since it requires a dict search
e.GetComponent<HealthComponent>()
e.Health

Und entfernt:

e.RemoveComponent<HealthComponent>();

Auf Daten wird häufig über Eigenschaften zugegriffen:

e.MaxHP

Welches ist komponentenseitig gespeichert; weniger Overhead, wenn es keine HP hat:

public int? MaxHP
{
get
{

    return this.Health.MaxHP; //Error handling returns null if health isn't here. Excluded for clarity
}
set
{
this.Health.MaxHP = value;
}

Die große Menge des Tippens wird von Intellisense in VS unterstützt, aber zum größten Teil gibt es wenig Tippen - das, wonach ich gesucht habe.

Hinter den Kulissen speichere ich eine Dictionary<Type, Component>und habe Componentsdie ToStringMethode zur Namensüberprüfung außer Kraft gesetzt . Mein ursprünglicher Entwurf verwendete hauptsächlich Zeichenketten für Namen und Dinge, aber es wurde zu einem Schwein, mit dem man arbeiten konnte (oh schau, ich muss auch überall die fehlenden leicht zugänglichen Eigenschaften besetzen).

Die kommunistische Ente
quelle