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 Component
und ComponentType
Aufzä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 ComponentType
Aufzählung zu verwenden, die jeder Component
von ihnen GameObject
hat. Der Grund dafür ist, dass ich dachte, dies wäre ein viel saubererer Ansatz, wenn ich versuche, das Erforderliche Component
aus dem herauszuholen, GameObject
wenn ich von a gefragt werde System
(siehe auch Link 1 oben).
Ich versuche, nicht an einem Typ vorbeizukommen und eine Iteration über alle Component
s 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 CollidableSystem
erstellt 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 System
erstellte Element GameObject
jedes 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 Component
s von GameObject
s zu erhalten, für die keine Nullprüfungen oder Castings erforderlich sind. Irgendwann wird ein System
Wille sagen "Gib mir diese Komponente, ich weiß, dass du sie hast" und keine Zeit damit verschwenden, diejenigen zu fragen, die GameObject
die nicht haben Component
. Sag, frag nicht.
quelle
Antworten:
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:
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 ):
Es gibt immer noch eine Nullprüfung, aber Sie können diese beseitigen, wenn Sie alle Ihre
componentsByType
Karten mit leeren Karten für jede Komponente füllen . So können Sie den Look verkürzen auf: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.
quelle
EntityManager
kö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.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
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:
Angenommen, die "faulste" Implementierung, bei der Sie die Listen den Systemen zur Verfügung stellen.
quelle
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.
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.
quelle
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
quelle