Ich schreibe ein Spiel mit C ++ und OpenGL 2.1. Ich überlegte, wie ich die Daten / Logik vom Rendern trennen könnte. Im Moment verwende ich eine Basisklasse 'Renderable', die eine rein virtuelle Methode zum Implementieren des Zeichnens bietet. Aber jedes Objekt hat so speziellen Code, dass nur das Objekt weiß, wie man Shader-Uniformen richtig einstellt und Vertex-Array-Pufferdaten organisiert. Ich habe am Ende eine Menge gl * -Funktionsaufrufe in meinem gesamten Code. Gibt es eine generische Möglichkeit, die Objekte zu zeichnen?
21
m_renderable
Mitglied interagieren zu lassen . Auf diese Weise können Sie Ihre Logik besser trennen. Erzwingen Sie die renderbare "Schnittstelle" nicht für allgemeine Objekte, die auch über Physik, AI und so weiter verfügen. Danach können Sie renderbare Objekte separat verwalten. Sie benötigen eine Abstraktionsebene über OpenGL-Funktionsaufrufe, um die Dinge noch mehr zu entkoppeln. Erwarten Sie also nicht, dass eine gute Engine GL-API-Aufrufe in ihren verschiedenen darstellbaren Implementierungen enthält. Das war's in aller Kürze.Antworten:
Eine Idee ist, das Besucher-Entwurfsmuster zu verwenden. Sie benötigen eine Renderer-Implementierung, die es versteht, Requisiten zu rendern. Jedes Objekt kann die Renderer-Instanz aufrufen, um den Render-Job zu verarbeiten.
In ein paar Zeilen Pseudocode:
Das gl * Zeug wird von den Methoden des Renderers implementiert, und die Objekte speichern nur die Daten, die zum Rendern benötigt werden, Position, Texturtyp, Größe usw.
Sie können auch verschiedene Renderer (debugRenderer, hqRenderer, ... usw.) einrichten und diese dynamisch verwenden, ohne die Objekte zu ändern.
Dies kann auch einfach mit Entity / Component-Systemen kombiniert werden.
quelle
Entity/Component
Alternative etwas mehr betonen können, da dies dazu beitragen kann, Geometrieanbieter von anderen Motorteilen (KI, Physik, Netzwerk oder allgemeines Gameplay) zu trennen. +1!ObjectA
undObjectB
proDrawableComponentA
undDrawableComponentB
in Render-Methoden, verwenden Sie andere Komponenten, wenn Sie es brauchen, wie:position = component->getComponent("Position");
Und in der Hauptschleife haben Sie eine Liste der zeichnungsfähigen Komponenten, mit denen Sie zeichnen können.Renderable
) mit einerdraw(Renderer&)
Funktion haben und alle Objekte, die gerendert werden können, diese implementieren? In welchem Fall wirdRenderer
nur eine Funktion benötigt, die ein Objekt akzeptiert, das die gemeinsame Schnittstelle und den Aufruf implementiertrenderable.draw(*this);
?gl_*
Funktionen in den Renderer (Trennung von Logik und Rendering), aber Ihre Lösung verschiebt diegl_*
Aufrufe in die Objekte.Ich weiß, dass Sie Zhens Antwort bereits akzeptiert haben, aber ich möchte noch eine herausbringen, nur für den Fall, dass es jemand anderem hilft.
Um das Problem zu wiederholen, möchte das OP, dass der Rendering-Code von der Logik und den Daten getrennt bleibt.
Meine Lösung besteht darin, eine andere Klasse zusammen zu verwenden, um die Komponente zu rendern, die von der
Renderer
und der Logikklasse getrennt ist. Zuerst muss es eineRenderable
Schnittstelle geben, die eine Funktion hat,bool render(Renderer& renderer);
und dieRenderer
Klasse verwendet das Besuchermuster, um alleRenderable
Instanzen abzurufen , wenn die Liste mitGameObject
s angegeben ist, und rendert die Objekte, die eineRenderable
Instanz haben. Auf diese Weise muss Renderer nicht jeden Objekttyp da draußen kennen und es liegt immer noch in der Verantwortung jedes Objekttyps, ihnRenderable
über diegetRenderable()
Funktion zu informieren . Alternativ können Sie eineRenderableVisitor
Klasse erstellen , die alle GameObjects besucht und basierend auf den individuellenGameObject
Bedingungen die Möglichkeit hat, dem Besucher das Rendering hinzuzufügen oder nicht hinzuzufügen. Wie auch immer, das Wichtigste ist, dass diegl_*
Aufrufe befinden sich alle außerhalb des Objekts selbst und befinden sich in einer Klasse, die intime Details des Objekts selbst kennt, anstatt dass sie Teil dieses Objekts sindRenderer
.HAFTUNGSAUSSCHLUSS : Ich habe diese Klassen im Editor von Hand geschrieben, sodass die Wahrscheinlichkeit groß ist, dass ich etwas im Code verpasst habe, aber Sie werden hoffentlich auf die Idee kommen.
So zeigen Sie ein (Teil-) Beispiel:
Renderable
SchnittstelleGameObject
Klasse:(Teil-)
Renderer
Klasse.RenderableObject
Klasse:ObjectA
Klasse:ObjectARenderable
Klasse:quelle
Erstellen Sie ein Renderbefehlssystem. Ein
OpenGLRenderer
übergeordnetes Objekt, das sowohl auf das Szenegraphen- als auch auf das Spielobjekt zugreifen kann, iteriert den Szenegraphen oder die Spielobjekte und erstellt einen Stapel von Objekten, die dann der ReiheRenderCmds
nach an dieOpenGLRenderer
jeweils Zeichnenden übergeben werden und dabei alle OpenGL- Objekte enthalten verwandter Code darin.Das hat mehr Vorteile als nur Abstraktion; Mit zunehmender Komplexität beim Rendern können Sie jeden Renderbefehl nach Textur oder Shader sortieren und gruppieren, um beispielsweise
Render()
viele Engpässe bei den Zeichenaufrufen zu beseitigen, die einen großen Unterschied in der Leistung bewirken können.quelle
Es hängt völlig davon ab, ob Sie Annahmen darüber treffen können, was für alle darstellbaren Entitäten gemeinsam ist oder nicht. In meiner Engine werden alle Objekte auf dieselbe Weise gerendert, sodass nur VBOS, Texturen und Transformationen bereitgestellt werden müssen. Dann holt der Renderer sie alle, so dass in den verschiedenen Objekten überhaupt keine OpenGL-Funktionsaufrufe erforderlich sind.
quelle
Stellen Sie Rendering-Code und Spielelogik auf jeden Fall in verschiedene Klassen. Die Komposition (wie von Teodron vorgeschlagen) ist wahrscheinlich der beste Weg, dies zu tun. Jede Entität in der Spielwelt hat ihre eigene Renderbarkeit - oder vielleicht eine Reihe davon.
Möglicherweise verfügen Sie noch über mehrere Unterklassen von Renderable, z. B. für Skelettanimationen, Partikelemitter und komplexe Shader, zusätzlich zu Ihrem grundlegenden strukturierten und beleuchteten Shader. Die Renderable-Klasse und ihre Unterklassen sollten nur Informationen enthalten, die zum Rendern benötigt werden: Geometrie, Texturen und Shader.
Außerdem sollten Sie eine Instanz eines bestimmten Netzes vom Netz selbst trennen. Angenommen, Sie haben hundert Bäume auf dem Bildschirm, von denen jeder dasselbe Netz verwendet. Sie möchten die Geometrie nur einmal speichern, benötigen jedoch separate Positions- und Rotationsmatrizen für jeden Baum. Komplexere Objekte, z. B. animierte Humanoide, verfügen auch über zusätzliche Statusinformationen (z. B. ein Skelett, die aktuell angewendeten Animationen usw.).
Beim Rendern besteht der naive Ansatz darin, jedes Spielelement zu durchlaufen und es anzuweisen, sich selbst zu rendern. Alternativ kann jede Entität (wenn sie erzeugt wird) ihre darstellbaren Objekte in ein Szenenobjekt einfügen. Anschließend weist Ihre Renderfunktion die Szene an, zu rendern. Auf diese Weise kann die Szene komplexe renderbezogene Aufgaben ausführen, ohne den Code in Spielelemente oder eine bestimmte renderbare Unterklasse einzubetten.
quelle
Dieser Rat ist nicht wirklich spezifisch für das Rendern, sollte aber helfen, ein System zu entwickeln, das die Dinge weitgehend getrennt hält. Versuchen Sie zunächst, die 'GameObject'-Daten von den Positionsinformationen zu trennen.
Es ist erwähnenswert, dass einfache XYZ-Positionsinformationen möglicherweise nicht so einfach sind. Wenn Sie eine Physik-Engine verwenden, können Ihre Positionsdaten in der Engine eines Drittanbieters gespeichert werden. Sie müssten entweder eine Synchronisierung zwischen ihnen durchführen (was viel sinnloses Kopieren des Speichers bedeuten würde) oder die Informationen direkt von der Engine abfragen. Aber nicht alle Objekte benötigen Physik, einige werden an Ort und Stelle fixiert, so dass ein einfacher Satz von Schwimmern dort gut funktioniert. Einige können sogar an andere Objekte angehängt sein, sodass ihre Position tatsächlich ein Versatz von einer anderen Position ist. In einer erweiterten Konfiguration ist die Position möglicherweise nur auf der GPU gespeichert. Dies ist auf der Computerseite nur für die Skripterstellung, Speicherung und Netzwerkreplikation erforderlich. Sie haben also wahrscheinlich mehrere Möglichkeiten für Ihre Positionsdaten. Hier ist es sinnvoll, Vererbung zu verwenden.
Anstelle eines Objekts, dessen Position es besitzt, sollte dieses Objekt selbst einer Indexdatenstruktur gehören. Zum Beispiel kann ein Level eine Octree-Szene oder eine Physik-Engine-Szene haben. Wenn Sie eine Renderszene rendern (oder einrichten) möchten, fragen Sie Ihre spezielle Struktur nach Objekten ab, die für die Kamera sichtbar sind.
Dies trägt auch zu einer guten Speicherverwaltung bei. Auf diese Weise hat ein Objekt, das sich nicht in einem Bereich befindet, nicht einmal eine sinnvolle Position, anstatt 0.0-Koordinaten oder die Koordinaten zurückzugeben, die es hatte, als es das letzte Mal in einem Bereich war.
Wenn Sie die Koordinaten nicht mehr im Objekt behalten, erhalten Sie anstelle von object.getX () level.getX (object). Das Problem dabei ist, dass das Objekt in der Ebene gesucht wird, wahrscheinlich ein langsamer Vorgang, da es alle Objekte durchsuchen und mit dem übereinstimmen muss, den Sie abfragen.
Um dies zu vermeiden, würde ich wahrscheinlich eine spezielle 'Link'-Klasse erstellen. Eine, die zwischen einer Ebene und einem Objekt bindet. Ich nenne es einen "Ort". Dies würde die xyz-Koordinaten sowie das Handle für die Ebene und ein Handle für das Objekt enthalten. Diese Verknüpfungsklasse würde in der räumlichen Struktur / Ebene gespeichert und das Objekt würde einen schwachen Verweis darauf haben (wenn die Ebene / Position zerstört wird, muss die Objektreferenz auf null aktualisiert werden. Es könnte sich auch lohnen, die Standortklasse tatsächlich zu haben "besitzen" Sie das Objekt, so dass, wenn eine Ebene gelöscht wird, auch die spezielle Indexstruktur, die darin enthaltenen Positionen und ihre Objekte.
Jetzt werden die Positionsinformationen nur noch an einem Ort gespeichert. Nicht dupliziert zwischen dem Objekt, der räumlichen Indexstruktur, dem Renderer usw.
Räumliche Datenstrukturen wie Octrees müssen oft nicht einmal die Koordinaten der Objekte haben, die sie speichern. Diese Position wird an der relativen Position der Knoten in der Struktur selbst gespeichert (dies könnte als eine Art verlustbehaftete Komprimierung angesehen werden, die Genauigkeit für schnelle Nachschlagezeiten opfert). Mit dem Standortobjekt im Octree werden dann die tatsächlichen Koordinaten darin gefunden, sobald die Abfrage abgeschlossen ist.
Oder wenn Sie eine Physik-Engine zum Verwalten Ihrer Objektpositionen oder einer Mischung aus beiden verwenden, sollte die Location-Klasse dies transparent handhaben und Ihren gesamten Code an einem Ort aufbewahren.
Ein weiterer Vorteil ist nun, dass die Position und die Referenz zum Level am selben Ort gespeichert werden. Sie können object.TeleportTo (other_object) implementieren und über mehrere Ebenen hinweg ausführen. In ähnlicher Weise könnte die KI-Wegfindung etwas in einen anderen Bereich verfolgen.
In Bezug auf das Rendern. Ihr Render kann eine ähnliche Bindung zum Ort haben. Außer, dass es das Rendering-spezifische Zeug gibt. Wahrscheinlich müssen Sie das Objekt oder die Ebene nicht in dieser Struktur speichern. Das Objekt kann nützlich sein, wenn Sie versuchen, etwas wie eine Farbauswahl vorzunehmen oder eine darüber schwebende Trefferleiste usw. zu rendern. Andernfalls kümmert sich der Renderer nur um das Netz und dergleichen. RenderableStuff wäre ein Mesh, könnte auch Rahmen haben und so weiter.
Möglicherweise müssen Sie dies nicht für jedes Bild einzeln durchführen. Sie können auch sicherstellen, dass Sie einen größeren Bereich aufnehmen, als von der Kamera aktuell angezeigt wird. Zwischenspeichern, Objektbewegungen verfolgen, um festzustellen, ob der Begrenzungsrahmen in Reichweite ist, Kamerabewegung verfolgen usw. Aber fangen Sie erst an, mit solchen Dingen herumzuspielen, wenn Sie sie getestet haben.
Ihre Physik-Engine selbst verfügt möglicherweise über eine ähnliche Abstraktion, da sie nicht nur die Kollisionsgitter- und Physik-Eigenschaften, sondern auch die Objektdaten benötigt.
Alle Ihre Kernobjektdaten enthalten würde, wäre der Name des Netzes, das das Objekt verwendet. Die Spiel-Engine kann diese dann in jedem beliebigen Format laden, ohne Ihre Objektklasse mit einer Reihe von renderspezifischen Dingen zu belasten (die möglicherweise für Ihre Rendering-API spezifisch sind, z. B. DirectX vs OpenGL).
Es hält auch verschiedene Komponenten getrennt. Dies macht es einfach, Dinge wie das Ersetzen Ihrer Physik-Engine zu tun, da dieses Zeug meist in sich geschlossen an einem Ort ist. Es macht auch das Testen viel einfacher. Sie können Dinge wie physikalische Abfragen testen, ohne dass Sie tatsächlich gefälschte Objekte einrichten müssen, da Sie lediglich die Location-Klasse benötigen. Sie können auch Dinge einfacher optimieren. Es wird klarer, welche Abfragen Sie für welche Klassen und einzelnen Standorte ausführen müssen, um sie zu optimieren (z. B. in der obigen Ebene. GetVisibleObject können Sie Dinge zwischenspeichern, wenn sich die Kamera nicht zu stark bewegt).
quelle