Taktiken zum Verschieben der Renderlogik aus der GameObject-Klasse

10

Wenn Sie Spiele erstellen, erstellen Sie häufig das folgende Spielobjekt, von dem alle Entitäten erben:

public class GameObject{
    abstract void Update(...);
    abstract void Draw(...);
}

In Ihrer Aktualisierungsschleife iterieren Sie also über alle Spielobjekte und geben ihnen die Möglichkeit, den Status zu ändern. In der nächsten Ziehschleife durchlaufen Sie erneut alle Spielobjekte und geben ihnen die Möglichkeit, sich selbst zu zeichnen.

Obwohl dies in einem einfachen Spiel mit einem einfachen Forward-Renderer ziemlich gut funktioniert, führt es oft zu einigen gigantischen Spielobjekten, die ihre Modelle speichern müssen, mehreren Texturen und am schlimmsten zu einer Fat Draw-Methode, die eine enge Kopplung zwischen dem Spielobjekt schafft. die aktuelle Renderstrategie und alle Rendering-bezogenen Klassen.

Wenn ich die Renderstrategie von vorwärts auf verzögert ändern würde, müsste ich viele Spielobjekte aktualisieren. Und die Spielobjekte, die ich mache, sind nicht so wiederverwendbar, wie sie sein könnten. Natürlich können Vererbung und / oder Komposition mir helfen, die Duplizierung von Code zu bekämpfen und die Änderung der Implementierung ein wenig zu vereinfachen, aber es scheint immer noch mangelhaft zu sein.

Ein besserer Weg wäre vielleicht, die Draw-Methode vollständig aus der GameObject-Klasse zu entfernen und eine Renderer-Klasse zu erstellen. Das GameObject müsste noch einige Daten über seine Grafik enthalten, z. B. mit welchem ​​Modell es dargestellt werden soll und welche Texturen auf das Modell gemalt werden sollen. Wie dies gemacht wird, bleibt jedoch dem Renderer überlassen. Es gibt jedoch oft viele Grenzfälle beim Rendern. Obwohl dies die enge Kopplung vom GameObject zum Renderer aufheben würde, müsste der Renderer immer noch alles über alle Spielobjekte wissen, die es fett machen würden, alles wissen und eng verbunden. Dies würde gegen einige gute Praktiken verstoßen. Vielleicht könnte datenorientiertes Design den Trick machen. Spielobjekte wären sicherlich Daten, aber wie würde der Renderer davon angetrieben? Ich bin mir nicht sicher.

Ich bin also ratlos und kann mir keine gute Lösung vorstellen. Ich habe versucht, die Prinzipien von MVC anzuwenden, und in der Vergangenheit hatte ich einige Ideen, wie man das in Spielen verwendet, aber in letzter Zeit scheint es nicht so anwendbar zu sein, wie ich dachte. Ich würde gerne wissen, wie Sie alle dieses Problem angehen.

Wie auch immer, ich bin daran interessiert, wie die folgenden Designziele erreicht werden können.

  • Keine Renderlogik im Spielobjekt
  • Lose Kopplung zwischen Spielobjekten und Render-Engine
  • Kein allwissender Renderer
  • Vorzugsweise Laufzeitumschaltung zwischen Render-Engines

Das ideale Projekt-Setup wäre ein separates 'Spiellogik'- und Renderlogik-Projekt, das nicht aufeinander verweisen muss.

Dieser Gedankengang begann, als ich John Carmack auf Twitter sagen hörte, dass er ein System hat, das so flexibel ist, dass er Render-Engines zur Laufzeit austauschen und sein System sogar anweisen kann, beide Renderer (einen Software-Renderer und einen hardwarebeschleunigten Renderer) zu verwenden. Gleichzeitig kann er Unterschiede untersuchen. Die Systeme, die ich bisher programmiert habe, sind nicht einmal annähernd so flexibel

Roy T.
quelle

Antworten:

7

Ein kurzer erster Schritt zum Entkoppeln:

Spielobjekte verweisen auf eine Kennung ihrer visuellen Elemente, jedoch nicht auf die Daten. Sagen wir etwas Einfaches wie eine Zeichenfolge. Beispiel: "human_male"

Der Renderer ist dafür verantwortlich, "human_male" -Referenzen zu laden und zu pflegen und ein zu verwendendes Handle an Objekte zurückzugeben.

Beispiel in schrecklichem Pseudocode:

GameObject( initialization parameters )
  me.render_handle = Renderer_Create( parameters.render_string )

- elsewhere
Renderer_Create( string )

  new data handle = Resources_Load( string );
  return new data handle

- some time later
GameObject( something happens to me parameters )
  me.state = something.what_happens
  Renderer_ApplyState( me.render_handle, me.state.effect_type )

- some time later
Renderer_Render()
  for each renderable thing
    for each rendering back end
        setup graphics for thing.effect
        render it

- finally
GameObject_Destroy()
  Renderer_Destroy( me.render_handle )

Entschuldigen Sie dieses Durcheinander, trotzdem werden Ihre Bedingungen durch diese einfache Abkehr von reinem OOP erfüllt, die darauf basiert, Dinge wie Objekte der realen Welt zu betrachten, und in OOP, basierend auf Verantwortlichkeiten.

  • Keine Renderlogik im Spielobjekt (fertig, alles, was das Objekt weiß, ist ein Handle, damit es Effekte auf sich selbst anwenden kann)
  • Lose Kopplung zwischen Spielobjekten und Render-Engine (erledigt, jeder Kontakt erfolgt über ein abstraktes Handle, Zustände, die angewendet werden können und nicht, was mit diesen Zuständen zu tun ist)
  • Kein allwissender Renderer (fertig, weiß nur über sich selbst Bescheid)
  • Vorzugsweise Laufzeitumschaltung zwischen Render-Engines (dies erfolgt in der Phase Renderer_Render (), Sie müssen jedoch beide Backends schreiben).

Schlüsselwörter, nach denen Sie suchen können, um über ein einfaches Refactoring von Klassen hinauszugehen, sind "Entity / Component System" und "Dependency Injection" sowie möglicherweise "MVC" -GUI-Muster, nur um die alten Gehirnzahnräder zum Laufen zu bringen.

Patrick Hughes
quelle
Das ist ganz anders als alles, was ich vorher gemacht habe. Es hört sich so an, als hätte es einiges an Potenzial. Zum Glück bin ich nicht an einen vorhandenen Motor gebunden, also kann ich einfach basteln. Ich werde auch die von Ihnen erwähnten Begriffe nachschlagen, obwohl die Abhängigkeitsinjektion mein Gehirn immer verletzt: P.
Roy T.
2

Was ich für meine eigene Engine getan habe, ist, alles in Module zu gruppieren. Also habe ich meine GameObjectKlasse und sie hat einen Griff zu:

  • ModuleSprite - Sprites zeichnen
  • ModuleWeapon - Schusswaffen
  • ModuleScriptingBase - Skripterstellung
  • ModuleParticles - Partikeleffekte
  • ModuleCollision - Kollisionserkennung und -reaktion

Ich habe also eine PlayerKlasse und eine BulletKlasse. Beide stammen von GameObjectund werden dem hinzugefügt Scene. Hat Playeraber folgende Module:

  • ModuleSprite
  • Modulwaffe
  • ModulePartikel
  • ModuleCollision

Und Bullethat diese Module:

  • ModuleSprite
  • ModuleCollision

Diese Art, Dinge zu organisieren, vermeidet den "Diamanten des Todes", in dem Sie ein Vehicle, ein VehicleLandund ein haben VehicleWaterund jetzt ein wollen VehicleAmphibious. Stattdessen hast du ein Vehicleund es kann ein ModuleWaterund ein haben ModuleLand.

Zusätzlicher Bonus: Sie können Objekte mit einer Reihe von Eigenschaften erstellen. Alles, was Sie wissen müssen, ist der Basistyp (Player, Enemy, Bullet usw.) und dann Handles für Module erstellen, die Sie für diesen Typ benötigen.

In meiner Szene mache ich Folgendes:

  • Rufen Sie das Updatefür alle GameObjectGriffe auf.
  • Führen Sie eine Kollisionsprüfung und eine Kollisionsreaktion für diejenigen durch, die ein ModuleCollisionHandle haben.
  • Rufen Sie die UpdatePostfür alle GameObjectGriffe an, um ihre endgültige Position nach der Physik zu erfahren.
  • Zerstören Sie Objekte, deren Flag gesetzt ist.
  • Fügen Sie der m_ObjectsCreatedListe neue Objekte aus der m_ObjectsListe hinzu.

Und ich könnte es weiter organisieren: nach Modulen statt nach Objekten. Dann würde ich eine Liste von rendern, eine Reihe von ModuleSpriteaktualisieren ModuleScriptingBaseund Kollisionen mit einer Liste von durchführen ModuleCollision.

Ritter666
quelle
Klingt nach maximaler Komposition! Sehr schön. Ich sehe hier jedoch nicht viele Rendering-spezifische Tipps. Wie gehen Sie damit um, indem Sie einfach verschiedene Module hinzufügen?
Roy T.
Oh ja. Das ist der Nachteil dieses Systems: Wenn Sie eine bestimmte Anforderung für a haben GameObject(zum Beispiel eine Möglichkeit, eine "Schlange" von Sprites zu rendern), müssen Sie entweder ein untergeordnetes Element ModuleSpritefür diese bestimmte Funktionalität erstellen ( ModuleSpriteSnake) oder ein neues Modul hinzufügen ( ModuleSnake). Zum Glück sind sie nur Zeiger, aber ich habe Code gesehen, in dem GameObjectbuchstäblich alles getan wurde, was ein Objekt tun konnte.
Knight666