Verwenden einer Entitätssystemarchitektur mit aufgabenbasierter Parallelität


9

Hintergrund

Ich habe in meiner Freizeit daran gearbeitet, eine Multithread-Spiel-Engine zu erstellen, und ich versuche derzeit, den besten Weg zu finden, um ein Entitätssystem in das zu integrieren, was ich bereits erstellt habe. Bisher habe ich diesen Artikel von Intel als Ausgangspunkt für meinen Motor verwendet. Bisher habe ich die normale Spielschleife mithilfe von Aufgaben implementiert und gehe nun dazu über, einige der Systeme und / oder Entitätssysteme zu integrieren. Ich habe in der Vergangenheit etwas Ähnliches wie Artemis verwendet , aber die Parallelität stört mich.

Der Artikel von Intel scheint zu befürworten, mehrere Kopien von Entitätsdaten zu haben und Änderungen an jeder Entität vorzunehmen, die am Ende eines vollständigen Updates intern verteilt werden. Dies bedeutet, dass das Rendern immer einen Frame hinter sich hat, aber dies scheint angesichts der Leistungsvorteile, die erzielt werden sollten, ein akzeptabler Kompromiss zu sein. Wenn es jedoch um ein Entitätssystem wie Artemis geht, bedeutet das Duplizieren jeder Entität für jedes System, dass jede Komponente auch dupliziert werden muss. Dies ist machbar, aber für mich scheint es, als würde es viel Speicher verbrauchen. Die Teile des Intel-Dokuments, die dies diskutieren, sind hauptsächlich 2.2 und 3.2.2. Ich habe einige Suchen durchgeführt, um zu sehen, ob ich gute Referenzen für die Integration der Architekturen finden konnte, die ich anstrebe, aber ich konnte noch nichts Nützliches finden.

Hinweis: Ich verwende C ++ 11 für dieses Projekt, aber ich stelle mir vor, dass das meiste, was ich frage, ziemlich sprachunabhängig sein sollte.

Mögliche Lösung

Verfügen Sie über einen globalen EntityManager, der zum Erstellen und Verwalten von Entities und EntityAttributes verwendet wird. Erlauben Sie Lesezugriff nur während der Aktualisierungsphase und speichern Sie alle Änderungen in einer Warteschlange pro Thread. Sobald alle Aufgaben abgeschlossen sind, werden die Warteschlangen kombiniert und die jeweiligen Änderungen übernommen. Dies hätte möglicherweise Probleme mit mehreren Schreibvorgängen in dieselben Felder, aber ich bin sicher, dass es ein Prioritätssystem oder einen Zeitstempel geben könnte, um dies zu klären. Dies scheint mir ein guter Ansatz zu sein, da Systeme in der Phase der Änderungsverteilung ganz natürlich über Änderungen an Entitäten informiert werden können.

Frage

Ich suche nach Feedback zu meiner Lösung, um zu sehen, ob sie überhaupt Sinn macht. Ich werde nicht lügen und behaupten, ein Experte für Multithreading zu sein, und ich mache dies größtenteils zum Üben. Ich kann einige komplizierte Probleme erkennen, die sich aus meiner Lösung ergeben, wenn mehrere Systeme mehrere Werte lesen / schreiben. Die von mir erwähnte Änderungswarteschlange kann auch schwer so zu formatieren sein, dass mögliche Änderungen leicht kommuniziert werden können, wenn ich nicht mit POD arbeite.

Jedes Feedback / Rat wäre sehr dankbar! Vielen Dank!

Links

Antworten:


12

Fork-Join

Sie benötigen keine separaten Kopien von Komponenten. Verwenden Sie einfach ein Fork-Join-Modell, das in diesem Artikel von Intel (äußerst schlecht) erwähnt wird.

In einem ECS haben Sie effektiv eine Schleife wie:

while in game:
  for each system:
    for each component in system:
      update component

Ändern Sie dies in etwas wie:

while in game:
  for each system:
    divide components into groups
    for each group:
      start thread (
        for each component in group:
          update component
      )
    wait for all threads to finish

Der schwierige Teil ist das Bit "Komponenten in Gruppen unterteilen". Für Grafiken sind fast keine gemeinsamen Daten erforderlich, daher ist dies einfach (teilen Sie renderbare Objekte gleichmäßig durch die Anzahl der verfügbaren Arbeitsthreads). Für Physik und KI möchten Sie logische "Inseln" von Objekten finden, die nicht interagieren, und diese zusammenfügen. Je weniger Interaktion zwischen Komponenten, desto besser.

Für Interaktionen, die vorhanden sein müssen, funktionieren verzögerte Nachrichten am besten. Wenn Objekt A Objekt B anweisen muss, Schaden zu nehmen, kann A eine Nachricht einfach in einen Pool pro Thread einreihen. Wenn die Threads verbunden werden, werden alle Pools zu einem einzigen Pool verkettet. Obwohl dies nicht direkt mit dem Threading zusammenhängt, lesen Sie die Veranstaltungsreihe der BitSquid-Entwickler (lesen Sie den gesamten Blog; ich bin nicht mit allem einverstanden, aber es ist eine fantastische Ressource).

Beachten Sie, dass "Fork-Join" weder die Verwendung fork()(wodurch Prozesse und keine Threads erstellt werden) bedeutet, noch bedeutet dies, dass Sie den Threads tatsächlich beitreten müssen. Es bedeutet nur, dass Sie eine einzelne Aufgabe übernehmen, sie in kleinere Teile zerlegen, die von Ihrem Pool von Arbeitsthreads bearbeitet werden sollen, und dann warten, bis alle Pakete verarbeitet sind.

Proxies

Dieser Ansatz kann allein oder in Kombination mit der Gabelverbindungsmethode verwendet werden, um die Notwendigkeit einer strengen Trennung weniger wichtig zu machen.

Sie können mit interagierenden Threads freundlicher umgehen, indem Sie einen einfachen zweischichtigen Ansatz verwenden. Haben Sie "autorisierende" Entitäten und "Proxy" -Entitäten. Autorisierende Entitäten können nur von einem einzelnen Thread aus geändert werden, der der eindeutige Eigentümer der autorisierenden Entität ist. Proxies-Entitäten können nicht geändert, sondern nur gelesen werden. Übertragen Sie an einem Synchronisierungspunkt in der Spielschleife alle Änderungen von autorisierenden Entitäten an die entsprechenden Proxys.

Ersetzen Sie "Entitäten" durch "Komponenten". Das Wesentliche ist, dass Sie höchstens zwei Kopien eines Objekts benötigen und es klare "Synchronisierungs" -Punkte in Ihrer Spielschleife gibt, wenn Sie in den meisten vernünftigen Game-Engine-Designs mit Gewinde von einem zum anderen kopieren können.

Sie können Proxys erweitern, um weiterhin die Verwendung (einer Teilmenge von) Methoden / Nachrichten zu ermöglichen, indem Sie all diese Dinge einfach in eine Warteschlange weiterleiten, die im nächsten Frame an das autorisierende Objekt übermittelt wird.

Beachten Sie, dass der Proxy-Ansatz ein fantastisches Design auf einer höheren Ebene ist, da er die Netzwerkunterstützung sehr einfach macht.


Ich hatte einige Dinge über die Gabelverbindung gelesen, die Sie zuvor erwähnt haben, und ich hatte den Eindruck, dass es zwar möglich ist, Parallelität zu nutzen, es jedoch Situationen gibt, in denen einige Arbeitsthreads möglicherweise darauf warten, dass eine Gruppe beendet wird. Im Idealfall versuche ich, diese Situation zu vermeiden. Die Proxy-Idee ist interessant und ähnelt etwas dem, woran ich gearbeitet habe. Eine Entität hat EntityAttributes und dies sind Wrapper für die tatsächlich von der Entität gespeicherten Werte. Werte können also jederzeit von ihnen gelesen, aber nur zu bestimmten Zeiten festgelegt werden und einen Proxy-Wert im Attribut enthalten, richtig?
Ross Hays

1
Es besteht eine gute Chance, dass Sie beim Versuch, Wartezeiten zu vermeiden, so viel Zeit damit verbringen, das Abhängigkeitsdiagramm zu analysieren, dass Sie insgesamt Zeit verlieren.
Patrick Hughes

@roflha: Ja, Sie könnten die Proxys auf die EntityAttribute-Ebene setzen. Oder erstellen Sie eine separate Entität mit einem zweiten Satz von Attributen. Oder lassen Sie das Konzept der Attribute einfach ganz fallen und verwenden Sie ein weniger detailliertes Komponentendesign.
Sean Middleditch

@ SeanMiddleditch Wenn ich Attribut sage, beziehe ich mich im Wesentlichen auf Komponenten, die ich denke. Die Attribute sind nicht nur einzelne Werte wie Floats und Strings, wenn es so klingt. Es handelt sich vielmehr um Klassen, die bestimmte Informationen wie ein PositionAttribute enthalten. Wenn Komponente der akzeptierte Name dafür ist, sollte ich vielleicht ändern. Aber würden Sie Proxy auf Entitätsebene und nicht auf Komponenten- / Attributebene empfehlen?
Ross Hays

1
Ich empfehle, was Sie am einfachsten zu implementieren finden. Denken Sie daran, dass der Punkt, an dem ich Proxys abfragen könnte, ohne Sperren, ohne Atomics und ohne Deadlocks.
Sean Middleditch
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.