Komponenten von Spielobjekten (Entitäten) abrufen

7

Mit C # und XNA 4 habe ich mich nach dem Lesen von Beiträgen wie diesem und jenem für ein auf Entitäten basierendes Design für mein Spiel entschieden , aber ich habe jetzt Schwierigkeiten, herauszufinden, wie Komponenten abgerufen werden können, sobald sie vorhanden sind zu einer Entität hinzugefügt.

Einige Artikel, die ich gelesen habe, besagen, dass beim Erstellen einer Komponente diese mithilfe eines Nachrichtensystems bei dem System registriert wird, das darauf reagiert. Ein Nachteil, den ich hier sehe, ist, dass wenn ein System 2 Komponenten benötigt, um zu handeln (z. B. CollideComponent und PositionComponent), dies kompliziert wird (und ich kann mir nicht vorstellen, wie es funktionieren würde).

Die andere Möglichkeit, die ich derzeit verfolge, besteht darin, ein System fragen zu lassen, ob eine Entität eine bestimmte Komponente hat, wenn sie diese benötigt. Allerdings habe ich keine Codebeispiele gesehen, nur die Theorie im Netz, also habe ich versucht, sie durcheinander zu bringen. Ein Code hier könnte erklären, wie weit ich gekommen bin:

Zuerst a System:

public class RenderingSystem : IGameSystem
{
  public void PerformAction(IQueryable<IGameObject> gameObjects)
  {
    foreach (var gameObject in gameObjects)
    {
      RenderableGameComponent renderableGameComponent = gameObject.GetComponent<RenderableGameComponent>(ComponentType.Renderable);
      if (renderableGameComponent != null)
      {
        this.spriteBatch.Draw(renderableGameComponent.Texture, renderableGameComponent.DrawablePosition, Color.White);
      }
    }
  }
}

Nun das eigentliche GameObject:

public class GameObject : IGameObject
{
    private List<IGameComponent> gameComponents;
    private ComponentType componentTypes;

    public void AddComponent(IGameComponent component)
    {
        this.gameComponents.Add(component);
        this.componentTypes = this.componentTypes | component.ComponentType;
    }

    public T GetComponent<T>(ComponentType componentType)
    {
        return (T)this.gameComponents.FirstOrDefault(gc => gc.ComponentType == componentType);
    }
}

Und schließlich die Componentund ComponentTypeAufzählung:

public class RenderableGameComponent : IGameComponent
{
    public Texture2D Texture { get; set; }

    public Vector2 DrawablePosition { get; set; }

    public ComponentType ComponentType
    {
        get { return ComponentType.Renderable; }
    }
}

[Flags]
public enum ComponentType
{
    Renderable = 1,
    Updateable = 2,
    Position = 4,
}

Wie Sie vielleicht sehen können, versuche ich, bitweise Operationen für die ComponentTypeAufzählung zu verwenden, die jeder Componentvon ihnen GameObjecthat. Der Grund dafür ist, dass ich dachte, dies wäre ein viel saubererer Ansatz, wenn ich versuche, das Erforderliche Componentaus dem herauszuholen, GameObjectwenn ich von a gefragt werde System(siehe auch Link 1 oben).

Ich versuche, nicht an einem Typ vorbeizukommen und eine Iteration über alle Components von a durchzuführen GameObject. z.B:

foreach (var component in this.GameComponents)
{
  if (component.GetType() == requestedType) { return component; }
}

Dies liegt daran, dass wenn ein paar hundert Kugeln auf dem Bildschirm angezeigt werden und eine CollidableSystemerstellt wird, das Spiel die Überprüfung aller Typen massiv verlangsamt. Ich habe Erfahrung damit, als ich ein vorheriges Spiel erstellt habe, aber zugegebenermaßen wurde ein vererbungsbasiertes Designsystem verwendet.

Es macht mir Sorgen, dass es für jedes Systemerstellte Element GameObjectjedes Mal durchlaufen muss, wenn jedes Mal dieselbe Frage gestellt wird: "Haben Sie die Komponente, nach der ich suche?" und immer noch viel Casting und Null-Checks.

Nun ist meine Frage dies. Ich möchte wissen, ob es ein De-facto-Entwurfsmuster gibt, um Components von GameObjects zu erhalten, für die keine Nullprüfungen oder Castings erforderlich sind. Irgendwann wird ein SystemWille sagen "Gib mir diese Komponente, ich weiß, dass du sie hast" und keine Zeit damit verschwenden, diejenigen zu fragen, die GameObjectdie nicht haben Component. Sag, frag nicht.

David K.
quelle
1
Ich weiß nicht, ob es ein De-facto-Entwurfsmuster gibt, aber ein Beispiel ist das Artemis-Framework. ( Gamadu.com/artemis ) In diesem Framework kennt jedes System die Komponententypen, an denen es interessiert ist, und wird benachrichtigt, wenn eine Entität hinzugefügt wird oder aus der "Welt" entfernt, der es zugeordnet ist. Mit anderen Worten, jedes System verarbeitet nur die Entitäten, deren Komponenten den Kriterien entsprechen.
Ryan Maloney
Ich habe mit dem Artemis Framework herumgespielt, aber dies führt immer noch eine Typprüfung durch. Es verwendet effektiv ein Repository-Muster für jeden Komponententyp, prüft, um welchen Komponententyp es sich handelt, und fügt es dem richtigen Repository zum späteren Abrufen hinzu.
David K

Antworten:

3

Systeme führen eine Liste der Entitäten, an denen sie interessiert sind.

Systeme werden nur einmal erstellt , wenn das Spiel initialisiert wird. Wenn Sie Systeme mit Entitäten erstellen, die bereits im Spiel sind, machen Sie es falsch :).

Alle Systeme, die Ihr Spiel verwenden wird, müssen erstellt werden, bevor Entitäten erstellt werden. Wenn eine neue Entität erstellt wird, prüft jedes System, ob es an dieser Entität zur Verarbeitung interessiert ist. Hier kommt Ihre bitweise Prüfung sehr schnell ins Spiel. Wenn sich die Komponenten einer Entität ändern, sollte jedes System diese Entität erneut überprüfen, um festzustellen, ob sie noch an einer Verarbeitung interessiert ist. Ein Beispiel für diese Überprüfung finden Sie im Artemis Framework-Code mit der check(Entity e)Funktion .

Wenn nun jedes System verarbeitet, durchläuft es nur die Liste der Entitäten, die es gespeichert hat.

Das Abrufen von Komponenten erfolgt mit verschachtelten Hash-Maps. Die erste Ebene ist eine Karte von Karten, die durch die Komponenten-ID indiziert sind. Wahrscheinlich würde die lange Zeit, die für die bitweise Identifizierung verwendet wird, hier gut funktionieren. Die zweite Ebene ist eine Zuordnung von Komponenten, die durch die Entitäts-ID indiziert sind. Dies würde im Baumformat ungefähr so ​​aussehen:

-Renderable (1)
  |-- Entity 1 (1)
       |-- Instance of Renderable
  |-- Entity 2 (2)
       |-- Instance of Renderable
-Updateable (2)
  |-- Entity 1 (1)
       |-- Instance of Updateable
-Position (4)
  |-- Entity 1 (1)
       |-- Instance of Position
  |-- Entity 2 (2)
       |-- Instance of Position

Wenn Entität 1 eine Entität mit renderbaren, aktualisierbaren und Positionskomponenten ist und Entität 2 eine Entität mit renderbaren und Positionskomponenten ist.

Diese Hash-Maps haben konstante Zugriffszeiten und sind sehr einfach zu navigieren.

Wenn Sie also Entitäten verarbeiten, können Sie eine Komponente wie folgt leicht abrufen (ähnlich der Artemis-Methode ):

Component getComponent(Entity e, ComponentType type) {
       HashMap<Long, Component> components = componentsByType.get(type.getLongID());
       if(components != null) {
            return components.get(e.getID());
       }
       return null;
 }

Es gibt immer noch eine Nullprüfung, aber Sie können diese beseitigen, wenn Sie alle Ihre componentsByTypeKarten mit leeren Karten für jede Komponente füllen . So können Sie den Look verkürzen auf:

 Component getComponent(Entity e, ComponentType type) {
       return componentsByType.get(type.getLongID()).get(e.getID());
 }

Es ist immer noch möglich, dass dies null zurückgibt, wenn die Entität diesen Komponententyp nicht hat. Es liegt an Ihnen, ob Sie dies überprüfen möchten, aber wenn das System die Entität basierend auf ihren Komponenten hinzugefügt hat, sollte diese Komponente vorhanden sein. Es liegt also an Ihnen, ob Sie dieses Risiko eingehen möchten.

MichaelHouse
quelle
"Systeme führen eine Liste der Entitäten, an denen sie interessiert sind." Manchmal musst du es dir nur buchstabieren :) Danke, ich habe diesen Punkt verpasst! Wäre es jedoch für einen EntityManager (oder eine Engine) sinnvoll, eine Liste dieser zu führen, und die Systeme selbst haben nur einen Verweis auf die Liste. Auf diese Weise müssten sie sich keine Gedanken über die Registrierung / Entfernung neuer Entitäten in ihrer Liste machen und könnten diese Aufgabe an die allgemeinere EntityManager-Klasse delegieren. Diese Klasse würde dann Entitäten für alle registrierten Systeme verwalten.
David K
Das EntityManagerkönnte diese Listen pflegen, aber es kann für das System einfacher sein, dies zu tun. Da niemand anderes als das System Zugriff auf diese Liste benötigt. Systeme werden nicht verarbeitet, wenn neue Entitäten hinzugefügt werden, sodass Sie sich keine Sorgen machen müssen, dass Systeme ausgelastet sind. Wenn ein Thread eine neue Entität hinzufügt, während die Systeme die Entität verarbeiten, sollte diese zu einer Add-Liste hinzugefügt werden, die im nächsten Frame integriert wird. Sie möchten nicht auf Parallelitätsprobleme mit Ihrer Entitätsliste stoßen.
MichaelHouse
2

Wenn ich überlegte, wie ich Komponenten, die zu einer bestimmten Entität gehören, optimal erhalten könnte, stoße ich auf das folgende Hauptproblem:

Geschwindigkeit: konstante Zeit gegen Suche.

Nun, dies ist Spielprogrammierung. Wenn Ihr Spiel nicht schnell funktioniert, müssen Sie das irgendwie angehen.

Mein erster Versuch bestand darin, Komponenten in einem Wörterbuch zu speichern. Das Wörterbuch ordnete die Komponenten nach Typ-ID, die Typ-ID war eine 64-Bit-Ganzzahl ohne Vorzeichen und wurde für jede Komponente durch Berechnung des Hash ihrer Klassennamenzeichenfolge erhalten. Das Problem ist, dass die getComponent-Methode der Entity-Klasse eine Suche beinhaltete, wahrscheinlich eine binäre Suche, aber im Vergleich zur konstanten Zeit immer noch nicht optimal ist. Dann konstante Zeit Zugang zu den Komponenten zu erreichen, ich habe als Richard Lord schlug in seinem Blog PostIn Bezug auf Entitätssysteme habe ich Knoten in Systemen erstellt, die den Zugriff auf Komponenten durch statische Typisierung und keine Suche ermöglichen. Mit dieser Methode habe ich die getComponent-Methode der Entität nur aufgerufen, wenn ich jede Entität zu Systemen hinzugefügt habe, um zu überprüfen, ob die erforderliche Komponente vorhanden war, und wenn dies der Fall war, habe ich den Knoten erstellt. Der Knoten ist eine Struktur / Klasse, die aus Zeigern auf Komponenten besteht. Ich habe dies nur für kritische Systeme wie Kollisionserkennung oder -rendering getan, nicht beispielsweise für AI, da AI möglicherweise mehrere Komponentensätze akzeptiert und je nach Vorhandensein oder Status der Komponenten unterschiedliche Codepfade verwendet. Daher hat AI immer noch häufig getComponent verwendet.

Später fand ich dies und das . Gute Optimierungen, denn wenn Sie schnell und in konstanter Zeit auf Komponenten zugreifen können, benötigen Sie überhaupt keine Knoten. Mit den entsprechenden Geschwindigkeits- und Speichervorteilen.

Eigentlich speichere ich Komponenten in einem Vektor. Jede Komponente hat einen eindeutigen Index, keine Typ-ID wie zuvor. Die Zuweisung von Indexwerten für jeden Typ erfolgt nacheinander. Ich werde nicht näher darauf eingehen, wie ich dies erreicht habe. Im Moment können Sie eine Aufzählung durchführen (ähnlich wie Sie es bereits tun).

Ich habe zwei Versionen der getComponent-Funktion, eine Prüfung auf nicht vorhandene Komponenten (die von Systemen verwendet werden, die in einer Vielzahl von Entitäten agieren können, auch wenn nicht alle denselben Komponentensatz haben, wie z. B. AI), die andere, die schnelle, erlauben eine Speicherverletzung, wenn Sie versuchen, auf eine nicht vorhandene Komponente zuzugreifen. Systeme, die auf diese Weise auf Komponenten zugreifen, verfügen über eine Schnittstelle, über die sie benachrichtigt werden, wenn eine Entität ihren Komponentensatz ändert (Entfernen oder Hinzufügen von Komponenten). Anschließend wird die Entität erneut überprüft. Wenn das System nicht mehr über die erforderlichen Komponenten verfügt, wird die Entität aus dem System entfernt System, aber diese Systeme suchen während der Spielschleife nie nach Komponenten, da sie dadurch langsam laufen würden.

Mit meinem Vorschlag, einen Vektor und kein Wörterbuch zu verwenden, können Sie in Fällen ausgeführt werden, in denen eine Entität nur die Positionskomponente mit einem Indexwert von 0 und die CharacterStatus-Komponente mit einem Indexwert von 64 (tatsächlich aber weniger) hat Verwenden Sie eine hohe Zahl, um diese Situation zu veranschaulichen. Da wir einen Vektor zum Speichern von nach Index geordneten Komponenten verwenden, bedeutet dies, dass Sie mit einem Vektor mit 65 Elementen enden und die Elemente 1 bis 63 Nullzeiger sind. Um diese Auswirkungen zu minimieren, werden Sie versuchen, weniger verwendeten Komponenten hohe Indexwerte zuzuweisen. Ich habe einige Berechnungen (nur unter Berücksichtigung der Größe des internen Vektors der Entitäten) der Speicherkosten der schlimmsten Fälle durchgeführt (dies ist der Fall, wenn alle Entitäten die letzte Komponente enthalten).

Sie müssen das Wörterbuch entfernen, da für den Zugriff auf seine Elemente eine Suche erforderlich ist. Sie müssen das Abrufen von Komponenten zeitlich konstant machen oder Knoten verwenden, wie Richard Lord in seinem Beitrag zeigt.

Hinweis: Ich verwende C ++, nicht C #, aber alles, was ich gesagt habe, ist sprachunabhängig. Zum Beispiel habe ich bei meinem ersten Versuch std :: map als Wörterbuch verwendet. Aber du kommst auf die Idee. In C # können Sie List from System.Collections.Generic anstelle von std :: vector verwenden. Dadurch können Sie in konstanter Zeit auf Komponentenreferenzen zugreifen.

Schließlich: Der erste Link aus dem Wiki für Entitätssysteme schlägt vor, Komponenten in Vektoren zu speichern, eine für jeden Komponententyp. Ich denke, dass dies nicht freundlich zu C # ist, weil dies

List<ComponentClassName>

würde eine sequentielle Liste von Verweisen auf ComponentClassName bedeuten und was auch immer Sie tun, Sie erhalten eine Referenz, dann tun Sie das in C # nicht sinnvoll, etwas ähnliches wie das, was ich tue, ist besser für C # (jede Entität hat einen Vektor von Zeigern, Gut Verweise in Ihrem Fall auf Komponenten, statische Abgüsse sind noch erforderlich). In C ++ können Sie std :: vector ausführen, und das wäre anders als std :: vector (mit dem * am Ende), sodass die im Wiki vorgeschlagene Methode in C ++ möglicherweise noch einen Sinn hat. Vielleicht benötigt sie sogar noch weniger Speicher als was Ich mache.

Möglicherweise bevorzugen Sie immer noch die im Wiki vorgeschlagene Methode, da Sie angegeben haben, dass Sie sich Sorgen machen, ständig Casts zu machen. Mit einem Vektor (Liste) pro Komponententyp und der Entität, die nur eine Ganzzahl ist, benötigen Sie überhaupt keine Umwandlungen. Etwas wie:

Vector3 displacement = position_list[entity_index] - previous_position_list[entity_index];

Angenommen, die "faulste" Implementierung, bei der Sie die Listen den Systemen zur Verfügung stellen.

Hatoru Hansou
quelle
Vielen Dank für den Link zu Richard Lords Beitrag. Zusammen mit Ihrer Antwort hat dies meinem Verständnis ernsthaft geholfen. Ich weiß jetzt, dass mir eine EntityManager- (oder Engine-) Klasse fehlte und ich versuchte, alles (Falsche) innerhalb der Spielschleife zwischen den Systemen und Entitäten direkt zu tun.
David K
Ist nicht, dass du etwas falsch gemacht hast? Es gibt viele Strategien, um etwas zum Laufen zu bringen. Ich möchte, dass Sie von Anfang an über diese "Optimierungen" nachdenken, da sie möglicherweise einige kleine Änderungen an Ihren Schnittstellen erfordern. Denken Sie jetzt daran, kann später Zeit sparen. Schneller Zugriff auf Komponenten ist meiner Meinung nach der Schlüssel.
Hatoru Hansou
Entschuldigung für den / sehr / späten Kommentar. Gab es einen Grund, warum Sie std :: unnordered_map nicht verwenden konnten?
Veritas
std :: unordered_map ist schneller als std :: map und erspart Ihnen gleichzeitig all diese verschwendeten leeren Zeiger. Sie sind immer noch langsamer als der Vektor, denke ich. Sie müssen sich also entscheiden, was wichtiger ist, ein bisschen mehr Geschwindigkeit oder Speicherplatz sparen. Beachten Sie, dass ich zu einem anderen Ansatz gewechselt bin. Jetzt sind meine Entitäten nur noch eine ID, und Komponenten sind Singletons, die ihre Daten zurückgeben können, wenn Sie ihnen eine Entitäts-ID übergeben (nicht so effizient wie Lookups) oder Sie alle Entitäten durchlaufen lassen, die dieser Komponente zugeordnet sind (was Systeme tun) meistens und sehr effizient).
Hatoru Hansou
0

Ich hatte das gleiche Problem: Ich hatte ein Wörterbuch , jeder Schlüssel ist ein ComponentType und der Wert ist eine Liste von GameObjects mit Komponenten dieses Typs.

Dictionary<ComponentType, List<GameObject>>

Auf diese Weise bleibt der größte Teil Ihres Software-Designs unverändert. Das System fordert das Wörterbuch nur auf, was es benötigt, keine unnötigen Iterationen und keine Nullprüfungen.

Die andere Art, wie ich Ihre Frage beantworten wollte, erklärt Ryan Maloney in seinem Kommentar.

Oliver
quelle
0

Nur eine Ergänzung zu dem, was Ryan Maloney erzählt hat. Ich habe am Port des erstaunlichen Artemis Entity-Systems gearbeitet (Java-Version: http://gamadu.com/artemis/ und unsere C # -Version http://thelinuxlich.github.com/artemis_CSharp/ ).

Systeme sind "irgendwie" mit Komponenten verbunden. Wenn Sie ein System erstellen, geben Sie die Arten von Komponenten an, die es verarbeitet, und zur Laufzeit werden nur die Entitäten mit diesen Komponententypen zur Verarbeitung an das System gesendet. Wenn Sie eine Komponente einer Entität entfernen oder hinzufügen (wir nennen, dass die Entität aktualisiert wurde), werden die interessierten Systeme benachrichtigt (ihre Methode onChange wird aufgerufen).

Ich empfehle Ihnen wirklich, sich den Quellcode https://github.com/thelinuxlich/artemis_CSharp anzusehen

Die meisten Probleme, über die Sie sprechen, wurden dort "gelöst" = P.

Es gibt auch ein einfaches Demo-Spiel, das es hier verwendet: https://github.com/thelinuxlich/starwarrior_CSharp

Tpastor
quelle