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).