Ich hatte eine Frage zur Spielarchitektur: Wie lassen sich verschiedene Komponenten am besten miteinander kommunizieren?
Ich entschuldige mich wirklich, wenn diese Frage bereits eine Million Mal gestellt wurde, aber ich kann mit genau den Informationen, nach denen ich suche, nichts finden.
Ich habe versucht, ein Spiel von Grund auf neu aufzubauen (C ++, wenn es darauf ankommt) und habe einige Open-Source-Spielesoftware als Inspiration betrachtet (Super Maryo Chronicles, OpenTTD und andere). Ich stelle fest, dass in vielen dieser Spiele-Designs überall globale Instanzen und / oder Singletons verwendet werden (z. B. für Render-Warteschlangen, Entity-Manager, Videomanager usw.). Ich versuche, globale Instanzen und Singletons zu vermeiden und eine Engine zu bauen, die so locker wie möglich gekoppelt ist, aber ich stoße auf einige Hindernisse, die meiner Unerfahrenheit im effektiven Design geschuldet sind. (Ein Teil der Motivation für dieses Projekt ist es, dies anzusprechen :))
Ich habe ein Design erstellt, in dem ich ein GameCore
Hauptobjekt habe, dessen Mitglieder den globalen Instanzen entsprechen, die ich in anderen Projekten sehe (dh, es hat einen Eingabemanager, einen Videomanager, ein GameStage
Objekt, das alle Entitäten und das Spiel steuert für welche Bühne gerade geladen ist, etc). Das Problem ist, dass es GameCore
für verschiedene Komponenten nicht einfach ist, miteinander zu kommunizieren , da sich alles im Objekt befindet.
Wenn beispielsweise eine Komponente des Spiels mit einer anderen Komponente kommunizieren muss (dh ein feindliches Objekt möchte sich selbst in die Render-Warteschlange einfügen, die in der Render-Phase gezeichnet werden soll), spricht Super Maryo Chronicles nur mit dem globale Instanz.
Für mich muss ich meine Spielobjekte relevante Informationen an das GameCore
Objekt zurückgeben lassen, damit das GameCore
Objekt diese Informationen an die anderen Komponenten des Systems weitergeben kann, die sie benötigen (dh für die obige Situation jedes feindliche Objekt) ihre Renderinformationen an das GameStage
Objekt zurückgeben, das sie alle sammelt und an das Objekt zurückgibt GameCore
, das sie wiederum an den Videomanager zum Rendern weitergibt). Das fühlt sich an wie ein wirklich schreckliches Design, und ich habe versucht, eine Lösung dafür zu finden. Meine Gedanken zu möglichen Designs:
- Globale Instanzen (Design von Super Maryo Chronicles, OpenTTD usw.)
- Das
GameCore
Objekt als Vermittler fungieren zu lassen, über den alle Objekte kommunizieren (aktuelles Design wie oben beschrieben) - Geben Sie den Komponenten Zeiger auf alle anderen Komponenten, mit denen sie sprechen müssen (dh im obigen Maryo-Beispiel hätte die feindliche Klasse einen Zeiger auf das Videoobjekt, mit dem sie sprechen muss).
- Unterteilen Sie das Spiel in Subsysteme. Verfügen Sie beispielsweise über Managerobjekte in dem
GameCore
Objekt, die die Kommunikation zwischen Objekten in ihrem Subsystem verwalten - (Andere Optionen? ....)
Ich stelle mir die obige Option 4 als die beste Lösung vor, aber ich habe einige Probleme beim Entwerfen ... vielleicht, weil ich über die Entwürfe nachgedacht habe, die Globale verwenden. Anscheinend nehme ich das gleiche Problem, das in meinem aktuellen Design besteht, und repliziere es in jedem Subsystem, nur in kleinerem Maßstab. Beispielsweise ist das GameStage
oben beschriebene Objekt ein Versuch, aber das GameCore
Objekt ist immer noch in den Prozess involviert.
Kann hier jemand einen Konstruktionsratschlag geben?
Vielen Dank!
quelle
Antworten:
In unseren Spielen verwenden wir zur Organisation unserer globalen Daten das ServiceLocator- Entwurfsmuster. Der Vorteil dieses Musters gegenüber dem Singleton- Muster besteht darin, dass sich die Implementierung Ihrer globalen Daten während der Laufzeit der Anwendung ändern kann. Außerdem können Ihre globalen Objekte auch zur Laufzeit geändert werden. Ein weiterer Vorteil ist, dass die Reihenfolge der Initialisierung Ihrer globalen Objekte einfacher verwaltet werden kann, was insbesondere in C ++ sehr wichtig ist.
zB (C # -Code, der leicht in C ++ oder Java übersetzt werden kann)
Nehmen wir an, Sie haben eine Rendering-Backend-Oberfläche, die einige gängige Operationen zum Rendern von Inhalten enthält.
Und dass Sie die Standard-Render-Backend-Implementierung haben
In einigen Designs scheint es legitim zu sein, global auf das Render-Backend zugreifen zu können. Im Singleton- Muster bedeutet dies, dass jede IRenderBackend- Implementierung als eindeutige globale Instanz implementiert werden sollte. Für die Verwendung des ServiceLocator- Musters ist dies jedoch nicht erforderlich.
Hier ist wie:
Um auf Ihr globales Objekt zugreifen zu können, müssen Sie es zuerst initialisieren.
Angenommen, Ihr Spiel verfügt über ein Minispiel, in dem das Rendern isometrisch ist, und Sie implementieren ein IsometricRenderBackend , um zu veranschaulichen, wie die Implementierungen während der Laufzeit variieren können .
Wenn Sie vom aktuellen Status in den Minispielstatus wechseln, müssen Sie nur das vom Service Locator bereitgestellte globale Render-Backend ändern.
Ein weiterer Vorteil ist, dass Sie auch Null-Services verwenden können. Wenn wir zum Beispiel einen ISoundManager- Dienst hatten und der Benutzer den Sound ausschalten wollte, konnten wir einfach einen NullSoundManager implementieren, der beim Aufrufen seiner Methoden nichts tut, indem wir das Service -Objekt des ServiceLocators auf ein NullSoundManager- Objekt setzen, das wir erreichen konnten dieses ergebnis mit kaum arbeitsaufwand.
Zusammenfassend lässt sich sagen, dass es manchmal unmöglich ist, globale Daten zu löschen, dies bedeutet jedoch nicht, dass Sie sie nicht ordnungsgemäß und objektorientiert organisieren können.
quelle
std::unique_ptr<ISomeService>
.Es gibt viele Möglichkeiten, eine Spiel-Engine zu entwerfen, und es kommt wirklich auf die Vorlieben an.
Um die Grundlagen aus dem Weg zu räumen, bevorzugen einige Entwickler das Entwerfen einer Pyramide, bei der es eine Top-Core-Klasse gibt, die oft als Kernel-, Core- oder Framework-Klasse bezeichnet wird und die eine Reihe von Subsystemen erstellt, besitzt und initialisiert, z B. Audio-, Grafik-, Netzwerk-, Physik-, KI- und Aufgaben-, Entitäts- und Ressourcenverwaltung. Im Allgemeinen werden diese Subsysteme von dieser Framework-Klasse für Sie verfügbar gemacht, und in der Regel übergeben Sie diese Framework-Klasse gegebenenfalls als Konstruktorargument an Ihre eigenen Klassen.
Ich glaube, Sie sind auf dem richtigen Weg, wenn Sie über Option 4 nachdenken.
Denken Sie daran, wenn es um die Kommunikation selbst geht, muss dies nicht immer einen direkten Funktionsaufruf selbst implizieren. Es gibt viele indirekte Wege, wie Kommunikation stattfinden kann, sei es durch eine indirekte Methode unter Verwendung
Signal and Slots
oder unter VerwendungMessages
.Manchmal ist es in Spielen wichtig, dass Aktionen asynchron ablaufen, damit sich unsere Spieleschleife so schnell wie möglich bewegt, damit die Bildraten für das bloße Auge flüssig sind. Die Spieler mögen keine langsamen und abgehackten Szenen und deshalb müssen wir Wege finden, die Dinge für sie fließend zu halten, aber die Logik fließend zu halten, aber in Schach zu halten und auch zu ordnen. Während asynchrone Operationen ihren Platz haben, sind sie auch nicht die Antwort für jede Operation.
Sie müssen nur wissen, dass Sie eine Mischung aus synchroner und asynchroner Kommunikation haben. Wählen Sie die geeignete Option aus, aber Sie müssen wissen, dass beide Stile in Ihren Subsystemen unterstützt werden müssen. Das Entwerfen von Unterstützung für beide wird Ihnen auch in Zukunft von Nutzen sein.
quelle
Sie müssen nur sicherstellen, dass es keine umgekehrten oder zyklischen Abhängigkeiten gibt. Wenn Sie beispielsweise eine Klasse
Core
haben und dieseCore
eineLevel
und dieLevel
eine Liste von hatEntity
, sollte der Abhängigkeitsbaum wie folgt aussehen:In Anbetracht dieses anfänglichen Abhängigkeitsbaums sollten Sie sich also niemals
Entity
aufLevel
oder verlassenCore
undLevel
sollten sich niemals darauf verlassenCore
. Wenn einLevel
oderEntity
Notwendigkeit , den Zugang zu Daten haben , die in der Abhängigkeitsstruktur höher sind, sollte es als Parameter als Referenz übergeben werden.Betrachten Sie den folgenden Code (C ++):
Mit dieser Technik können Sie sehen, dass jeder
Entity
Zugriff aufLevel
und derLevel
Zugriff auf hatCore
. Beachten Sie, dass jederEntity
einen Verweis auf denselbenLevel
Speicher speichert , wodurch Speicher verschwendet wird. Wenn Sie dies bemerken, sollten Sie sich fragen, ob jederEntity
wirklich Zugriff auf das Internet benötigt oder nichtLevel
.Nach meiner Erfahrung gibt es entweder A) eine wirklich offensichtliche Lösung, um umgekehrte Abhängigkeiten zu vermeiden, oder B) es gibt keine Möglichkeit, globale Instanzen und Singletons zu vermeiden.
quelle
Sie möchten also grundsätzlich einen globalen veränderlichen Zustand vermeiden ? Sie können es entweder lokal, unveränderlich oder gar nicht zu einem Bundesstaat machen. Letzteres ist am effizientesten und flexibelsten, imo. Es ist bekannt als Iplementation Hiding.
quelle
Die Frage scheint tatsächlich zu sein, wie die Kopplung verringert werden kann, ohne die Leistung zu beeinträchtigen. Alle globalen Objekte (Dienste) bilden normalerweise eine Art Kontext, der während der Laufzeit des Spiels geändert werden kann. In diesem Sinne verteilt das Dienstlokalisierungsmuster verschiedene Teile des Kontexts auf verschiedene Teile der Anwendung, die möglicherweise Ihren Wünschen entsprechen oder auch nicht. Ein anderer realer Ansatz wäre, eine Struktur wie die folgende zu deklarieren:
Und geben Sie es als nicht-besitzenden rohen Zeiger weiter
sEnvironment*
. Hier verweisen Zeiger auf Schnittstellen, sodass die Kopplung im Vergleich zum Service-Locator reduziert ist. Alle Dienste befinden sich jedoch an einem Ort (was gut oder schlecht sein kann). Dies ist nur ein weiterer Ansatz.quelle