Organisieren Sie ein Entitätssystem mit externen Komponentenmanagern?


13

Ich entwerfe eine Game-Engine für ein Top-Down-2D-Shooter-Multiplayer-Spiel, das ich für andere Top-Down-Shooter-Spiele wiederverwenden möchte. Im Moment denke ich darüber nach, wie so etwas wie ein Entitätssystem in ihm gestaltet werden sollte. Zuerst habe ich darüber nachgedacht:

Ich habe eine Klasse namens EntityManager. Es sollte eine Methode namens Update und eine andere namens Draw implementieren. Der Grund, warum ich Logic und Rendering trenne, ist, dass ich die Draw-Methode weglassen kann, wenn ein eigenständiger Server ausgeführt wird.

EntityManager besitzt eine Liste von Objekten des Typs BaseEntity. Jede Entität besitzt eine Liste von Komponenten wie EntityModel (die darstellbare Darstellung einer Entität), EntityNetworkInterface und EntityPhysicalBody.

EntityManager verfügt auch über eine Liste von Komponentenmanagern wie EntityRenderManager, EntityNetworkManager und EntityPhysicsManager. Jeder Komponentenmanager speichert Verweise auf die Entitätskomponenten. Es gibt verschiedene Gründe, diesen Code aus der eigenen Klasse der Entität zu verschieben und ihn stattdessen kollektiv auszuführen. Zum Beispiel verwende ich eine externe Physikbibliothek, Box2D, für das Spiel. In Box2D fügen Sie zuerst die Körper und Formen zu einer Welt hinzu (in diesem Fall Eigentum des EntityPhysicsManager) und fügen Kollisionsrückrufe hinzu (die an das Entitätsobjekt selbst in meinem System gesendet würden). Dann führen Sie eine Funktion aus, die alles im System simuliert. Ich finde es schwierig, eine bessere Lösung dafür zu finden, als dies in einem externen Komponentenmanager wie diesem zu tun.

Die Erstellung von Entitäten erfolgt folgendermaßen: EntityManager implementiert die Methode RegisterEntity (entityClass, factory), die registriert, wie eine Entität dieser Klasse erstellt wird. Es implementiert auch die Methode CreateEntity (entityClass), die ein Objekt vom Typ BaseEntity zurückgibt.

Nun kommt mein Problem: Wie würde der Verweis auf eine Komponente bei den Komponentenmanagern registriert? Ich habe keine Ahnung, wie ich die Komponentenmanager aus einer Fabrik / Schließung referenzieren würde.


Ich weiß nicht, ob dies vielleicht ein hybrides System sein soll, aber es hört sich so an, als ob Ihre "Manager" das sind, was ich allgemein als "Systeme" bezeichnet habe. dh die Entität ist eine abstrakte ID; Eine Komponente ist ein Datenpool. und was Sie als "Manager" bezeichnen, ist das, was allgemein als "System" bezeichnet wird. Interpretiere ich den Wortschatz richtig?
BRPocock

Meine Frage hier könnte von Interesse sein: Spielkomponenten, Spielmanager und Objekteigenschaften
George Duckett

Schauen Sie unter gamadu.com/artemis nach, ob deren Methoden Ihre Frage beantworten.
Patrick Hughes

1
Es gibt keine Möglichkeit, ein Entitätssystem zu entwerfen, da über seine Definition wenig Einigkeit besteht. Was @BRPocock beschreibt und was Artemis verwendet, wird in diesem Blog ausführlicher beschrieben: t-machine.org/index.php/category/entity-systems zusammen mit einem Wiki: entity-systems.wikidot.com
user8363

Antworten:


6

Systeme sollten ein Schlüsselwertpaar von Entity to Component in einer Art Map, Dictionary Object oder Associative Array speichern (abhängig von der verwendeten Sprache). Außerdem würde ich mir beim Erstellen Ihres Entitätsobjekts keine Gedanken darüber machen, es in einem Manager zu speichern, es sei denn, Sie müssen es von einem der Systeme abmelden können. Eine Entität ist eine Zusammenstellung von Komponenten, sollte jedoch keine Komponentenaktualisierungen verarbeiten. Das sollte von den Systemen übernommen werden. Behandeln Sie Ihre Entität stattdessen als einen Schlüssel, der allen in den Systemen enthaltenen Komponenten zugeordnet ist, sowie als einen Kommunikationsknoten, über den diese Komponenten miteinander kommunizieren können.

Der große Teil der Entity-Component-System-Modelle besteht darin, dass Sie die Möglichkeit implementieren können, ganz einfach Nachrichten von einer Komponente an die übrigen Komponenten einer Entität zu übergeben. Auf diese Weise kann eine Komponente mit einer anderen Komponente kommunizieren, ohne zu wissen, wer diese Komponente ist oder wie mit der zu ändernden Komponente umgegangen wird. Stattdessen wird eine Nachricht übergeben und die Komponente kann sich selbst ändern (falls vorhanden).

Zum Beispiel würde ein Positionssystem nicht viel Code enthalten, sondern nur Entitätsobjekte verfolgen, die ihren Positionskomponenten zugeordnet sind. Wenn sich eine Position ändert, kann eine Nachricht an die betreffende Entität gesendet werden, die wiederum an alle Komponenten dieser Entität weitergeleitet wird. Eine Position ändert sich aus welchem ​​Grund auch immer? Das Positionssystem sendet der Entität eine Nachricht, die besagt, dass sich die Position geändert hat, und dass die Bildwiedergabekomponente dieser Entität diese Nachricht abruft und aktualisiert, wo sie sich als Nächstes selbst zeichnet.

Umgekehrt muss ein Physiksystem wissen, was alle seine Objekte tun. Es muss in der Lage sein, alle Weltobjekte zu sehen, um Kollisionen zu testen. Wenn eine Kollision auftritt, wird die Richtungskomponente der Entität aktualisiert, indem eine Art "Richtungsänderungsnachricht" an die Entität gesendet wird, anstatt direkt auf die Entitätskomponente zu verweisen. Dies entkoppelt den Manager von der Notwendigkeit, mithilfe einer Nachricht die Richtung ändern zu müssen, anstatt sich darauf zu verlassen, dass eine bestimmte Komponente vorhanden ist (die möglicherweise überhaupt nicht vorhanden ist). In diesem Fall stößt die Nachricht nur auf taube Ohren, anstatt auf einen Fehler auftreten, weil ein erwartetes Objekt nicht vorhanden war).

Sie werden einen enormen Vorteil davon bemerken, da Sie erwähnt haben, dass Sie eine Netzwerkschnittstelle haben. Eine Netzwerkkomponente würde alle eingehenden Nachrichten abhören, über die alle anderen Bescheid wissen sollten. Es liebt den Klatsch. Wenn das Netzwerksystem aktualisiert wird, senden die Netzwerkkomponenten diese Nachrichten an andere Netzwerksysteme auf anderen Clientcomputern, die sie dann erneut an alle anderen Komponenten senden, um die Playerpositionen usw. zu aktualisieren. Möglicherweise ist eine spezielle Logik erforderlich, damit nur bestimmte Entitäten dies können Nachrichten über das Netzwerk senden, aber das ist das Schöne am System. Sie können diese Logik einfach steuern lassen, indem Sie die richtigen Dinge dafür registrieren.

Zusamenfassend:

Entität ist eine Zusammensetzung von Komponenten, die Nachrichten empfangen können. Die Entität kann Nachrichten empfangen und diese an alle Komponenten delegieren, um sie zu aktualisieren. (Position geändert Nachricht, Geschwindigkeitsänderung Richtung, etc.) Es ist wie eine zentrale Mailbox, die alle Komponenten voneinander hören können, anstatt direkt miteinander zu sprechen.

Die Komponente ist ein kleiner Teil einer Entität, in dem ein bestimmter Status der Entität gespeichert ist. Diese können bestimmte Nachrichten analysieren und andere Nachrichten wegwerfen. Beispielsweise würde sich eine "Richtungskomponente" nur um "Richtungsänderungsnachrichten" kümmern, nicht aber um "Positionsänderungsnachrichten". Komponenten aktualisieren ihren eigenen Status basierend auf Nachrichten und aktualisieren dann den Status anderer Komponenten, indem sie Nachrichten von ihrem System senden.

Das System verwaltet alle Komponenten eines bestimmten Typs und ist dafür verantwortlich, die Komponenten in jedem Frame zu aktualisieren und Nachrichten von den von ihnen verwalteten Komponenten an die Entitäten zu senden, zu denen die Komponenten gehören

Systeme könnten in der Lage sein, alle ihre Komponenten parallel zu aktualisieren und alle Nachrichten zu speichern, wenn sie gehen. Wenn die Ausführung der Aktualisierungsmethoden aller Systeme abgeschlossen ist, bitten Sie jedes System, seine Nachrichten in einer bestimmten Reihenfolge zu senden. Kontrollen zuerst möglich, gefolgt von Physik, gefolgt von Richtung, Position, Rendering usw. Es ist wichtig, in welcher Reihenfolge sie gesendet werden, da ein Richtungswechsel in der Physik eine kontrollbasierte Richtungsänderung immer ausgleichen sollte.

Hoffe das hilft. Es ist eine Hölle von Design Patterns, aber es ist lächerlich mächtig, wenn es richtig gemacht wird.


0

Ich verwende ein ähnliches System in meiner Engine und so, wie ich es gemacht habe, enthält jede Entität eine Liste von Komponenten. Über den EntityManager kann ich alle Entitäten abfragen und feststellen, welche eine bestimmte Komponente enthalten. Beispiel:

class Component
{
    private uint ID;
    // etc...
}

class Entity
{
    List<Component> Components;
    // etc...
    public bool Contains(Type type)
    {
        foreach(Component comp in Components)
        {
            if(typeof(comp) == type)
                return true;
        }
        return false;
    }
}

class EntityManager
{
    List<Entity> Entities;
    // etc...
    public List<Entity> GetEntitiesOfType(Type type)
    {
        List<Entity> results = new List<Entity>();
        foreach(Entity entity in Entities)
        {
            if(entity.Contains(type))
                results.Add(entity);
        }
        return results;
    }
}

Offensichtlich ist dies nicht der genaue Code (Sie benötigen tatsächlich eine Vorlagenfunktion, um nach verschiedenen Komponententypen zu suchen, anstatt sie zu verwenden typeof), aber das Konzept ist da. Dann können Sie diese Ergebnisse verwenden, die gesuchte Komponente referenzieren und sie bei Ihrer Fabrik registrieren. Dies verhindert eine direkte Kopplung zwischen Komponenten und ihren Managern.


3
Vorsichtsmaßnahme: An dem Punkt, an dem Ihre Entität Daten enthält, handelt es sich um ein Objekt, nicht um eine Entität. In dieser Struktur geht der größte Teil der Vorteile von ECS für die Parallelisierung verloren. "Reine" E / C / S-Systeme sind relational, nicht objektorientiert ... Nicht, dass es für einige Fälle "schlecht" sein muss, aber es ist sicherlich "das relationale Modell brechen"
BRPocock

2
Ich bin mir nicht sicher, ob ich dich verstehe. Mein Verständnis (und bitte korrigieren Sie mich, wenn ich falsch liege) ist, dass das Entity-Component-System eine Entity-Klasse hat, die Komponenten enthält und möglicherweise eine ID, einen Namen oder eine Kennung hat. Ich denke, wir könnten ein Missverständnis in dem haben, was ich unter "Typ" der Entität verstehe. Wenn ich Entität "Typ" sage, beziehe ich mich auf die Komponententypen. Das heißt, eine Entität ist ein "Sprite" -Typ, wenn sie eine Sprite-Komponente enthält.
Mike Cluck

1
In einem reinen Entitäts- / Komponentensystem ist eine Entität normalerweise atomar: zB typedef long long int Entity; Eine Komponente ist ein Datensatz (möglicherweise eine Objektklasse oder nur eine struct), der einen Verweis auf die Entität enthält, der sie zugeordnet ist. und ein System wäre eine Methode oder eine Sammlung von Methoden. Das ECS-Modell ist nicht sehr kompatibel mit dem OOP-Modell, obwohl eine Komponente ein (größtenteils) Nur-Daten-Objekt und ein System ein Nur-Code-Singleton-Objekt sein kann, dessen Zustand sich in Komponenten befindet ... obwohl es sich um "hybride" Systeme handelt häufiger als "reine", verlieren sie viele der angeborenen Vorteile.
BRPocock

2
@BRPocock für "reine" Entitätssysteme. Ich denke, eine Entität als Objekt ist vollkommen in Ordnung, es muss keine einfache ID sein. Eine Sache ist die serialisierte Darstellung, eine andere das In-Memory-Layout eines Objekts / eines Konzepts / einer Entität. Solange Sie die Datenabhängigkeit aufrechterhalten können, sollten Sie nicht an nicht-idiomatischen Code gebunden sein, nur weil dies der "reine" Weg ist.
Raine

1
@BRPocock Dies ist eine faire Warnung, aber für "t-machine" -ähnliche Entity-Systeme. Ich verstehe warum, aber dies sind nicht die einzigen Möglichkeiten, komponentenbasierte Entitäten zu modellieren. Schauspieler sind eine interessante Alternative. Ich schätze sie eher, besonders für rein logische Einheiten.
Raine

0

1) Ihrer Factory-Methode sollte ein Verweis auf den EntityManager übergeben werden, der sie aufgerufen hat (ich verwende C # als Beispiel):

delegate BaseEntity EntityFactory(EntityManager manager);

2) Lassen Sie CreateEntity neben der Klasse / dem Typ der Entität auch eine ID erhalten (z. B. eine Zeichenfolge, eine Ganzzahl, die Sie selbst bestimmen), und registrieren Sie die erstellte Entität automatisch in einem Dictionary, indem Sie diese ID als Schlüssel verwenden:

class EntityManager
{
    // Rest of class omitted

    BaseEntity CreateEntity(string id, Type entityClass)
    {
        BaseEntity entity = factories[entityClass](this);
        registry.Add(id, entity);
        return entity;
    }

    Dictionary<Id, BaseEntity> registry;
}

3) Fügen Sie EntityManager einen Getter hinzu, um eine Entität nach ID abzurufen:

class EntityManager
{
    // Rest of class omitted

    BaseEntity GetEntity(string id)
    {
        return registry[id];
    }
}

Und das ist alles, was Sie brauchen, um innerhalb Ihrer Factory-Methode auf einen ComponentManager zu verweisen. Zum Beispiel:

BaseEntity CreateSomeSortOfEntity(EntityManager manager)
{
    // Create and configure entity
    BaseEntity entity = new BaseEntity();
    RenderComponent renderComponent = new RenderComponent();
    entity.AddComponent(renderComponent);

    // Get a reference to the render manager and register component
    RenderEntityManager renderer = manager.GetEntity("RenderEntityManager") as RenderEntityManager;
    if(renderer != null)
        renderer.Register(renderComponent)

    return entity;
}

Neben Id können Sie auch eine Art Type-Eigenschaft verwenden (eine benutzerdefinierte Aufzählung oder einfach das Typensystem der Sprache verwenden) und einen Getter erstellen, der alle BaseEntities eines bestimmten Typs zurückgibt.


1
Nicht um pedantisch zu sein, aber in einem reinen (relationalen) Entitätssystem haben Entitäten keinen Typ, außer den, der ihnen aufgrund ihrer Komponenten vermittelt wird ...
BRPocock

@BRPocock: Könnten Sie ein Beispiel erstellen, das der reinen Tugend folgt?
Zolomon

1
@Raine Vielleicht habe ich keine Erfahrung aus erster Hand damit, aber das habe ich gelesen. Außerdem können Sie Optimierungen implementieren, um den Zeitaufwand für das Nachschlagen von Komponenten nach ID zu verringern. Was die Cache-Kohärenz betrifft, ist dies meiner Meinung nach sinnvoll, da Sie Daten desselben Typs zusammenhängend im Arbeitsspeicher speichern, insbesondere wenn Ihre Komponenten kompakt sind oder einfache Eigenschaften aufweisen. Ich habe gelesen, dass ein einzelner Cache-Fehler auf der PS3 so kostspielig sein kann wie tausend CPU-Anweisungen, und dieser Ansatz, Daten ähnlichen Typs zusammenhängend zu speichern, ist eine sehr verbreitete Optimierungstechnik in der modernen Spieleentwicklung.
David Gouveia

2
In ref: „reiner“ Einheit System: das Entity - ID ist in der Regel etwas wie: typedef unsigned long long int EntityID;; Das Ideal ist, dass jedes System auf einer separaten CPU oder einem separaten Host ausgeführt werden kann und nur Komponenten abgerufen werden müssen, die für dieses System relevant bzw. in diesem System aktiv sind. Bei einem Entity-Objekt muss möglicherweise auf jedem Host dasselbe Entity-Objekt instanziiert werden, was die Skalierung erschwert. Bei einem reinen Entity-Component-System-Modell wird die Verarbeitung auf Knoten (Prozesse, CPUs oder Hosts) nach System und nicht in der Regel nach Entität aufgeteilt.
BRPocock

1
@DavidGouveia erwähnte "Optimierungen ... Nach Entitäten nach ID suchen". Tatsächlich neigen die (wenigen) Systeme, die ich auf diese Weise implementiert habe, dazu, dies nicht zu tun. Wählen Sie Komponenten häufiger nach einem bestimmten Muster aus, um anzuzeigen, dass sie für ein bestimmtes System von Interesse sind, und verwenden Sie Entitäten (IDs) nur für komponentenübergreifende Joins.
BRPocock
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.