Ich implementiere eine Entitätssystemvariante mit:
Eine Entitätsklasse , die kaum mehr als eine ID ist, die Komponenten miteinander verbindet
Eine Reihe von Komponentenklassen , die keine "Komponentenlogik", sondern nur Daten enthalten
Eine Reihe von Systemklassen (auch bekannt als "Subsysteme", "Manager"). Diese erledigen die gesamte Verarbeitung der Entitätslogik. In den meisten einfachen Fällen iterieren die Systeme nur durch eine Liste von Entitäten, an denen sie interessiert sind, und führen für jede eine Aktion aus
Ein MessageChannel-Klassenobjekt , das von allen Spielsystemen gemeinsam genutzt wird. Jedes System kann bestimmte Arten von Nachrichten abonnieren, die abgehört werden sollen, und über den Kanal Nachrichten an andere Systeme senden
Die ursprüngliche Variante der Behandlung von Systemnachrichten war ungefähr so:
- Führen Sie nacheinander ein Update für jedes Spielsystem aus
Wenn ein System eine Komponente bearbeitet und diese Aktion für andere Systeme von Interesse sein könnte, sendet das System eine entsprechende Nachricht (z. B. einen Systemaufruf)
messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))
wann immer ein Objekt bewegt wird)
Jedes System, das die bestimmte Nachricht abonniert hat, erhält die aufgerufene Nachrichtenbehandlungsmethode
Wenn ein System ein Ereignis verarbeitet und die Ereignisverarbeitungslogik das Senden einer anderen Nachricht erfordert, wird die Nachricht sofort gesendet und eine andere Kette von Nachrichtenverarbeitungsmethoden wird aufgerufen
Diese Variante war in Ordnung, bis ich anfing, das Kollisionserkennungssystem zu optimieren (es wurde sehr langsam, als die Anzahl der Entitäten zunahm). Zuerst würde es einfach jedes Entitätspaar unter Verwendung eines einfachen Brute-Force-Algorithmus iterieren. Dann habe ich einen "räumlichen Index" hinzugefügt, der ein Gitter von Zellen enthält, in dem Entitäten gespeichert sind, die sich im Bereich einer bestimmten Zelle befinden, sodass nur Entitäten in benachbarten Zellen überprüft werden können.
Jedes Mal, wenn sich eine Entität bewegt, prüft das Kollisionssystem, ob die Entität mit etwas an der neuen Position kollidiert. Ist dies der Fall, wird eine Kollision erkannt. Und wenn beide kollidierenden Entitäten "physische Objekte" sind (beide haben die RigidBody-Komponente und sollen sich gegenseitig wegdrücken, um nicht den gleichen Raum einzunehmen), fordert ein spezielles System zur Trennung starrer Körper das Bewegungssystem auf, die Entitäten zu einigen zu bewegen spezifische Positionen, die sie trennen würden. Dies wiederum veranlasst das Bewegungssystem, Nachrichten zu senden, die über geänderte Entitätspositionen informieren. Das Kollisionserkennungssystem soll reagieren, da es seinen räumlichen Index aktualisieren muss.
In einigen Fällen tritt ein Problem auf, weil der Inhalt der Zelle (eine generische Liste von Entitätsobjekten in C #) geändert wird, während sie durchlaufen werden, wodurch der Iterator eine Ausnahme auslöst.
Also ... wie kann ich verhindern, dass das Kollisionssystem unterbrochen wird, während es nach Kollisionen sucht?
Natürlich könnte ich eine "clevere" / "knifflige" Logik hinzufügen, die sicherstellt, dass der Zelleninhalt korrekt durchlaufen wird, aber ich denke, das Problem liegt nicht im Kollisionssystem selbst (ich hatte auch ähnliche Probleme in anderen Systemen), sondern in der Art und Weise Nachrichten werden auf dem Weg von System zu System behandelt. Was ich brauche, ist eine Möglichkeit, um sicherzustellen, dass eine bestimmte Ereignisbehandlungsmethode ihren Job ohne Unterbrechungen erledigt.
Was ich ausprobiert habe:
- Warteschlangen für eingehende Nachrichten . Jedes Mal, wenn ein System eine Nachricht rundsendet, wird die Nachricht zu den Nachrichtenwarteschlangen von Systemen hinzugefügt, die daran interessiert sind. Diese Nachrichten werden verarbeitet, wenn ein Systemupdate für jeden Frame aufgerufen wird. Das Problem : Wenn ein System A eine Nachricht zur Warteschlange von System B hinzufügt, funktioniert es gut, wenn System B später als System A (im selben Spiel-Frame) aktualisiert werden soll. Andernfalls verarbeitet die Nachricht den nächsten Spielrahmen (für einige Systeme nicht wünschenswert).
- Warteschlangen für ausgehende Nachrichten . Während ein System ein Ereignis verarbeitet, werden alle von ihm gesendeten Nachrichten zur Warteschlange für ausgehende Nachrichten hinzugefügt. Die Nachrichten müssen nicht auf die Verarbeitung eines Systemupdates warten: Sie werden "sofort" behandelt, nachdem der erste Nachrichten-Handler seine Arbeit beendet hat. Wenn die Verarbeitung der Nachrichten dazu führt, dass andere Nachrichten gesendet werden, werden auch diese einer ausgehenden Warteschlange hinzugefügt, sodass alle Nachrichten im selben Frame verarbeitet werden. Das Problem: Wenn ein Entity-Lifetime-System (ich habe das Entity-Lifetime-Management mit einem System implementiert) eine Entität erstellt, werden einige Systeme A und B darüber benachrichtigt. Während System A die Nachricht verarbeitet, verursacht es eine Kette von Nachrichten, die schließlich zur Zerstörung der erstellten Entität führen (z. B. wurde eine Aufzählungsentität genau dort erstellt, wo sie mit einem Hindernis kollidiert, wodurch die Aufzählungspunkte selbst zerstört werden). Während die Nachrichtenkette aufgelöst wird, ruft System B die Entitätserstellungsnachricht nicht ab. Wenn also System B auch an der Entitätszerstörungsnachricht interessiert ist, erhält es diese und erst, nachdem die "Kette" aufgelöst wurde, erhält es die ursprüngliche Entitätserzeugungsnachricht. Dadurch wird die Zerstörungsnachricht ignoriert und die Erstellungsnachricht "akzeptiert".
BEARBEITEN - ANTWORTEN AUF FRAGEN, KOMMENTARE:
- Wer ändert den Inhalt der Zelle, während das Kollisionssystem darüber iteriert?
Während das Kollisionssystem Kollisionsprüfungen für eine Entität und deren Nachbarn durchführt, wird möglicherweise eine Kollision erkannt und das Entitätssystem sendet eine Nachricht, auf die andere Systeme sofort reagieren. Die Reaktion auf die Nachricht kann dazu führen, dass andere Nachrichten erstellt und sofort verarbeitet werden. So könnte ein anderes System eine Meldung erstellen, die das Kollisionssystem dann sofort verarbeiten müsste (z. B. wenn eine Entität verschoben wurde und das Kollisionssystem ihren räumlichen Index aktualisieren muss), obwohl die früheren Kollisionsprüfungen noch nicht abgeschlossen waren.
- Können Sie nicht mit einer globalen Warteschlange für ausgehende Nachrichten arbeiten?
Ich habe kürzlich eine einzelne globale Warteschlange ausprobiert. Es verursacht neue Probleme. Problem: Ich verschiebe ein Panzerelement in ein Wandelement (der Panzer wird mit der Tastatur gesteuert). Dann entscheide ich mich, die Richtung des Panzers zu ändern. Um den Tank und die Wand von jedem Rahmen zu trennen, bewegt das CollidingRigidBodySeparationSystem den Tank so weit wie möglich von der Wand weg. Die Trennrichtung sollte der Bewegungsrichtung des Panzers entgegengesetzt sein (wenn das Spiel beginnt, sollte der Panzer so aussehen, als würde er sich niemals in die Wand bewegen). Die Richtung wird jedoch entgegengesetzt zur NEUEN Richtung, wodurch der Tank auf eine andere Seite der Wand bewegt wird als ursprünglich. Warum das Problem auftritt: So werden Nachrichten jetzt behandelt (vereinfachter Code):
public void Update(int deltaTime)
{
m_messageQueue.Enqueue(new TimePassedMessage(deltaTime));
while (m_messageQueue.Count > 0)
{
Message message = m_messageQueue.Dequeue();
this.Broadcast(message);
}
}
private void Broadcast(Message message)
{
if (m_messageListenersByMessageType.ContainsKey(message.GetType()))
{
// NOTE: all IMessageListener objects here are systems.
List<IMessageListener> messageListeners = m_messageListenersByMessageType[message.GetType()];
foreach (IMessageListener listener in messageListeners)
{
listener.ReceiveMessage(message);
}
}
}
Der Code fließt wie folgt (nehmen wir an, es ist nicht das erste Spielfeld):
- Die Systeme beginnen mit der Verarbeitung von TimePassedMessage
- InputHandingSystem wandelt Tastendrücke in Entitätsaktionen um (in diesem Fall wird aus einem Pfeil nach links eine MoveWest-Aktion). Die Entitätsaktion wird in der ActionExecutor-Komponente gespeichert
- ActionExecutionSystem fügt als Reaktion auf die Entitätsaktion eine MovementDirectionChangeRequestedMessage am Ende der Nachrichtenwarteschlange hinzu
- MovementSystem verschiebt die Objektposition basierend auf den Velocity-Komponentendaten und fügt die PositionChangedMessage-Nachricht am Ende der Warteschlange hinzu. Die Bewegung erfolgt mit der Bewegungsrichtung / Geschwindigkeit des vorherigen Frames (sagen wir Norden)
- Systeme beenden die Verarbeitung von TimePassedMessage
- Die Systeme beginnen mit der Verarbeitung von MovementDirectionChangeRequestedMessage
- MovementSystem ändert die Geschwindigkeit / Bewegungsrichtung des Objekts wie gewünscht
- Systeme beenden die Verarbeitung von MovementDirectionChangeRequestedMessage
- Die Systeme beginnen mit der Verarbeitung von PositionChangedMessage
- CollisionDetectionSystem erkennt, dass eine Entität, die sich bewegt hat, in eine andere Entität geraten ist (Tank ist in eine Wand gefahren). Es wird eine CollisionOccuredMessage zur Warteschlange hinzugefügt
- Systeme stoppen die Verarbeitung von PositionChangedMessage
- Die Systeme beginnen mit der Verarbeitung von CollisionOccuredMessage
- CollidingRigidBodySeparationSystem reagiert auf Kollisionen durch Trennung von Tank und Wand. Da die Wand statisch ist, wird nur der Tank bewegt. Die Bewegungsrichtung der Panzer wird als Indikator dafür verwendet, woher der Panzer kam. Es ist in entgegengesetzter Richtung versetzt
BUG: Als der Panzer diesen Rahmen bewegte, bewegte er sich mit der Bewegungsrichtung des vorherigen Rahmens, aber als er getrennt wurde, wurde die Bewegungsrichtung von DIESEM Rahmen verwendet, obwohl sie bereits anders war. So sollte es nicht funktionieren!
Um diesen Fehler zu vermeiden, muss die alte Bewegungsrichtung irgendwo gespeichert werden. Ich könnte es zu einer Komponente hinzufügen, nur um diesen speziellen Fehler zu beheben, aber deutet dieser Fall nicht auf eine grundlegend falsche Art des Umgangs mit Nachrichten hin? Warum sollte das Trennsystem darauf achten, welche Bewegungsrichtung es verwendet? Wie kann ich dieses Problem elegant lösen?
- Vielleicht möchten Sie gamadu.com/artemis lesen, um zu sehen, was sie mit Aspects gemacht haben. Auf welcher Seite treten einige der Probleme auf, die Sie sehen.
Eigentlich kenne ich Artemis schon eine ganze Weile. Untersuchte den Quellcode, las die Foren usw. Aber ich habe gesehen, dass "Aspekte" nur an wenigen Stellen erwähnt wurden und sie bedeuten, soweit ich das verstehe, im Grunde "Systeme". Aber ich kann nicht sehen, wie Artemis einige meiner Probleme angeht. Es werden nicht einmal Nachrichten verwendet.
- Siehe auch: "Entitätskommunikation: Nachrichtenwarteschlange vs Publish / Subscribe vs Signal / Slots"
Ich habe bereits alle Fragen zu gamedev.stackexchange in Bezug auf Entity-Systeme gelesen. Dieser scheint die Probleme, mit denen ich konfrontiert bin, nicht zu diskutieren. Vermisse ich etwas?
- Behandeln Sie die beiden Fälle unterschiedlich. Die Aktualisierung des Rasters muss nicht auf den Bewegungsnachrichten basieren, da diese Teil des Kollisionssystems sind
Ich bin mir nicht sicher was du meinst. Ältere Implementierungen von CollisionDetectionSystem prüften nur auf Kollisionen bei einem Update (wenn eine TimePassedMessage verarbeitet wurde), aber ich musste die Überprüfungen aufgrund der Leistung so gering wie möglich halten. Deshalb habe ich auf Kollisionsprüfung umgestellt, wenn sich eine Entität bewegt (die meisten Entitäten in meinem Spiel sind statisch).
quelle
Antworten:
Sie haben wahrscheinlich von dem Anti-Muster des God / Blob-Objekts gehört. Nun, Ihr Problem ist eine God / Blob-Schleife. Das Basteln an Ihrem Nachrichtenübermittlungssystem ist bestenfalls eine Band-Aid-Lösung und im schlimmsten Fall eine reine Zeitverschwendung. Tatsächlich hat Ihr Problem überhaupt nichts spezielles mit der Spieleentwicklung zu tun. Ich habe mich dabei erwischt, wie ich versucht habe, eine Sammlung zu ändern, während ich sie mehrmals durchlaufen habe, und die Lösung ist immer dieselbe: Unterteilen, Unterteilen, Unterteilen.
So wie ich den Wortlaut Ihrer Frage verstehe, sieht Ihre Methode zur Aktualisierung Ihres Kollisionssystems derzeit im Großen und Ganzen wie folgt aus.
So einfach geschrieben können Sie sehen, dass Ihre Schleife drei Verantwortlichkeiten hat, wenn sie nur eine haben sollte. Um Ihr Problem zu lösen, teilen Sie Ihre aktuelle Schleife in drei separate Schleifen auf, die drei verschiedene algorithmische Durchläufe darstellen .
Indem Sie Ihre ursprüngliche Schleife in drei Teilschleifen unterteilen, versuchen Sie nie mehr, die Sammlung zu ändern, über die Sie gerade iterieren. Beachten Sie auch, dass Sie nicht mehr als in Ihrer ursprünglichen Schleife arbeiten und dass Sie möglicherweise einige Cache-Gewinne erzielen, wenn Sie dieselben Vorgänge mehrere Male hintereinander ausführen.
Ein weiterer Vorteil ist, dass Sie jetzt Parallelität in Ihren Code einführen können. Ihr Ansatz mit kombinierten Schleifen ist von Natur aus seriell (was im Grunde genommen die Ausnahme für gleichzeitige Änderungen ist!), Da jede Schleifeniteration möglicherweise sowohl Lese- als auch Schreibvorgänge in Ihre Kollisionswelt ausführt. Die drei oben dargestellten Teilschleifen sind jedoch alle entweder Lese- oder Schreibschleifen, jedoch nicht beide. Zumindest der erste Durchgang, bei dem alle möglichen Kollisionen überprüft werden, ist peinlich parallel geworden, und je nachdem, wie Sie Ihren Code schreiben, können auch der zweite und der dritte Durchgang durchgeführt werden.
quelle
Wie kann die Nachrichtenbehandlung in einem komponentenbasierten Entitätssystem ordnungsgemäß implementiert werden?
Ich würde sagen, dass Sie zwei Arten von Nachrichten möchten: Synchron und Asynchron. Synchrone Nachrichten werden sofort verarbeitet, während asynchrone Nachrichten nicht im selben Stack-Frame verarbeitet werden (sondern möglicherweise im selben Game-Frame). Die Entscheidung, was ist, wird normalerweise auf der Basis "pro Nachrichtenklasse" getroffen, z. B. "alle EnemyDied- Nachrichten sind asynchron".
Einige Ereignisse werden mit einer dieser Methoden sehr viel einfacher gehandhabt . Meiner Erfahrung nach ist ein ObjectGetsDeletedNow - Ereignis viel weniger sexy und Rückrufe sind viel schwieriger zu implementieren als ObjectWillBeDeletedAtEndOfFrame. Andererseits wird jeder "Veto" -ähnliche Message-Handler (Code, der bestimmte Aktionen abbrechen oder ändern kann, während sie ausgeführt werden, wie ein Shield-Effekt das DamageEvent modifiziert ) in asynchronen Umgebungen nicht einfach, sondern ein Kinderspiel synchrone Anrufe.
In einigen Fällen kann die asynchrone Verarbeitung effizienter sein (z. B. können Sie einige Ereignishandler überspringen, wenn das Objekt ohnehin später gelöscht wird). Manchmal ist die Synchronisierung effizienter, insbesondere wenn das Berechnen des Parameters für ein Ereignis kostspielig ist und Sie lieber Callback-Funktionen zum Abrufen bestimmter Parameter anstelle bereits berechneter Werte übergeben möchten (falls sich ohnehin niemand für diesen bestimmten Parameter interessiert).
Sie haben bereits ein weiteres allgemeines Problem bei Nur-Synchron-Nachrichtensystemen erwähnt: Nach meiner Erfahrung mit synchronen Nachrichtensystemen ist eine der häufigsten Ursachen für Fehler und Trauer im Allgemeinen das Ändern von Listen beim Durchlaufen dieser Listen.
Denken Sie darüber nach: Es liegt in der Natur des synchronen (sofortige Behandlung aller Nachwirkungen einer Aktion) und des Nachrichtensystems (Entkoppeln des Empfängers vom Absender, sodass der Absender nicht weiß, wer auf Aktionen reagiert), dass Sie nicht in der Lage sind, auf einfache Weise zu reagieren finde solche Schleifen. Was ich sage ist: Seien Sie darauf vorbereitet, mit dieser Art von sich selbst modifizierender Iteration viel umzugehen. Seine Art von "by Design". ;-)
Wie kann ich verhindern, dass das Kollisionssystem während der Kollisionsprüfung unterbrochen wird?
Für Ihr spezielles Problem mit der Kollisionserkennung ist es möglicherweise ausreichend, Kollisionsereignisse asynchron zu machen, sodass sie in die Warteschlange gestellt werden, bis der Kollisionsmanager abgeschlossen ist und anschließend als ein Stapel ausgeführt wird (oder zu einem späteren Zeitpunkt im Frame). Dies ist Ihre Lösung "Incoming Queue".
Das Problem: Wenn ein System A eine Nachricht zur Warteschlange von System B hinzufügt, funktioniert es gut, wenn System B später als System A (im selben Spiel-Frame) aktualisiert werden soll. Andernfalls verarbeitet die Nachricht den nächsten Spielrahmen (für einige Systeme nicht wünschenswert).
Einfach:
while (! queue.empty ()) {queue.pop (). handle (); }
Führen Sie die Warteschlange einfach immer wieder aus, bis keine Nachricht mehr vorhanden ist. (Wenn Sie jetzt "Endlosschleife" schreien, denken Sie daran, dass Sie höchstwahrscheinlich dieses Problem als "Nachrichten-Spamming" haben würden, wenn es auf den nächsten Frame verzögert würde. Sie können () für eine vernünftige Anzahl von Iterationen angeben, um Endlosschleifen zu erkennen. wenn du Lust hast;))
quelle
Wenn Sie tatsächlich versuchen, die datenorientierte Gestaltung von ECS zu nutzen, sollten Sie sich überlegen, wie Sie am besten mit DOD vorgehen können.
Schauen Sie sich den BitSquid-Blog an , insbesondere den Teil über Ereignisse. Ein System, das sich gut in ECS einfügt, wird vorgestellt. Puffern Sie alle Ereignisse in eine schöne, saubere Warteschlange vom Typ "pro Nachricht", so wie Systeme in einem ECS pro Komponente sind. Später aktualisierte Systeme können die Warteschlange für einen bestimmten Nachrichtentyp effizient durchlaufen, um sie zu verarbeiten. Oder ignoriere sie einfach. Welcher auch immer.
Beispielsweise würde das CollisionSystem einen Puffer voller Kollisionsereignisse generieren. Jedes andere System, das nach einer Kollision ausgeführt wird, kann dann die Liste durchlaufen und diese nach Bedarf verarbeiten.
Es behält die datenorientierte Parallelität des ECS-Entwurfs bei, ohne die Komplexität der Nachrichtenregistrierung oder dergleichen. Nur Systeme, die sich tatsächlich für einen bestimmten Ereignistyp interessieren, durchlaufen die Warteschlange für diesen Typ und führen eine direkte Iteration in einem Durchgang durch die Nachrichtenwarteschlange aus, um den bestmöglichen Wirkungsgrad zu erzielen.
Wenn Sie die Komponenten in jedem System konsistent geordnet lassen (z. B. alle Komponenten nach Entitäts-ID oder ähnlichem ordnen), erhalten Sie sogar den Vorteil, dass Nachrichten in der effizientesten Reihenfolge generiert werden, um sie zu durchlaufen und die entsprechenden Komponenten im zu suchen Verarbeitungssystem. Wenn Sie also die Entitäten 1, 2 und 3 haben, werden die Nachrichten in dieser Reihenfolge generiert, und die während der Verarbeitung der Nachricht durchgeführten Komponentensuchen werden in einer streng aufsteigenden Adressreihenfolge (die am schnellsten ist) ausgeführt.
quelle