Separazione dei dati di gioco / logica dal rendering


21

Sto scrivendo un gioco usando C ++ e OpenGL 2.1. Stavo pensando come separare i dati / la logica dal rendering. Al momento uso una classe base "Renderable" che fornisce un metodo virtuale puro per implementare il disegno. Ma ogni oggetto ha un codice così specializzato, solo l'oggetto sa come impostare correttamente le uniformi dello shader e organizzare i dati del buffer dell'array di vertici. Finisco con molte chiamate a funzioni gl * su tutto il mio codice. Esiste un modo generico per disegnare gli oggetti?


4
Usa la composizione per collegare effettivamente un renderable al tuo oggetto e fai interagire il tuo oggetto con quel m_renderablemembro. In questo modo, puoi separare meglio la tua logica. Non imporre la "interfaccia" di rendering su oggetti generici che hanno anche fisica, ai e quant'altro. Successivamente, è possibile gestire i renderable separatamente. È necessario un livello di astrazione sulle chiamate di funzione OpenGL per disaccoppiare ancora di più le cose. Quindi, non aspettarti che un buon motore abbia chiamate API GL all'interno delle sue varie implementazioni renderizzabili. Questo è tutto, in poche parole.
teodron,

1
@teodron: Perché non l'hai inserito come risposta?
Tapio,

1
@Tapio: perché non è tanto una risposta; è invece più un suggerimento.
teodron,

Risposte:


20

Un'idea è quella di utilizzare il modello di progettazione del visitatore. È necessaria un'implementazione di Renderer che sappia come rendere gli oggetti di scena. Ogni oggetto può chiamare l'istanza del renderer per gestire il processo di rendering.

In poche righe di pseudocodice:

class Renderer {
public:
    void render( const ObjectA & obj );
    void render( const ObjectB & obj );
};


class ObjectA{
public:
    void draw( Renderer & r ){ r.render( *this ) };
}

class ObjectB{
public:
    void draw( Renderer & r ){ r.render( *this ) };
}

Il gl * stuff è implementato con i metodi del renderer e gli oggetti memorizzano solo i dati necessari per il rendering, posizione, tipo di trama, dimensione ... ecc.

Inoltre, puoi impostare diversi renderer (debugRenderer, hqRenderer, ... ecc.) E usarli dinamicamente, senza cambiare gli oggetti.

Anche questo può essere facilmente combinato con i sistemi Entity / Component.


1
Questa è una risposta piuttosto buona! Avresti potuto enfatizzare Entity/Componentun po 'di più l' alternativa poiché può aiutare a separare i fornitori di geometria da altre parti del motore (AI, Fisica, Networking o gameplay generale). +1!
teodron,

1
@teodron, non spiegherò l'alternativa E / C perché complicherebbe le cose. Ma penso che dovresti cambiare ObjectAe ObjectBper DrawableComponentAe DrawableComponentB, e all'interno dei metodi di rendering, usare altri componenti se ne hai bisogno, come: position = component->getComponent("Position");E nel ciclo principale, hai un elenco di componenti disegnabili con cui chiamare draw.
Zhen,

Perché non avere solo un'interfaccia (come Renderable) che ha una draw(Renderer&)funzione e tutti gli oggetti che possono essere renderizzati li implementano? In tal caso, è Renderersufficiente una funzione che accetta qualsiasi oggetto che implementa l'interfaccia e la chiamata comuni renderable.draw(*this);?
Vite Falcon,

1
@ViteFalcon, scusami se non mi chiarisco, ma per una spiegazione dettagliata, dovrei avere bisogno di più spazio e codice. Fondamentalmente, la mia soluzione sposta le gl_*funzioni nel renderer (separando la logica dal rendering), ma la soluzione sposta le gl_*chiamate negli oggetti.
Zhen,

In questo modo le funzioni gl * vengono effettivamente spostate dal codice oggetto, ma conservo ancora le variabili handle utilizzate nel rendering, come l'ID buffer / texture, le posizioni uniformi / attributi.
Felipe

4

So che hai già accettato la risposta di Zhen, ma mi piacerebbe metterne un altro nel caso in cui aiuti qualcun altro.

Per ribadire il problema, l'OP vuole la capacità di mantenere il codice di rendering separato dalla logica e dai dati.

La mia soluzione è utilizzare una classe diversa tutti insieme per rendere il componente, che è separato dalla Rendererclasse logica e. Per prima cosa deve esserci Renderableun'interfaccia che ha una funzione bool render(Renderer& renderer);e la Rendererclasse usa il modello visitatore per recuperare tutte le Renderableistanze, dato l'elenco di se GameObjectrende gli oggetti che hanno Renderableun'istanza. In questo modo, Renderer non ha bisogno di conoscere ogni tipo di oggetto là fuori ed è comunque responsabilità di ogni tipo di oggetto informarlo Renderabletramite la getRenderable()funzione. O in alternativa, puoi creare una RenderableVisitorclasse che visita tutti gli oggetti di gioco e in base alle GameObjectcondizioni individuali possono scegliere di aggiungere / non aggiungere il loro renderizzabile al visitatore. Ad ogni modo, l'essenza principale è che ilgl_*le chiamate sono tutte al di fuori dell'oggetto stesso e risiedono in una classe che conosce i dettagli intimi dell'oggetto stesso, invece di farne parte Renderer.

NOTA BENE : ho scritto a mano queste classi nell'editor, quindi c'è una buona possibilità che mi sia sfuggito qualcosa nel codice, ma spero che tu abbia l'idea.

Per mostrare un esempio (parziale):

Renderable interfaccia

class Renderable {
public:
    Renderable(){}
    virtual ~Renderable(){}
    virtual void render(Renderer& renderer) const = 0;
};

GameObject classe:

class GameObject {
public:
    GameObject()
        : mVisible(true)
        , mMarkedForDelete(false) {}

    virtual ~GameObject(){}

    virtual Renderable* getRenderable() {
        // By default, all GameObjects are missing their Renderable
        return NULL;
    }

    void setVisible(bool visible) {
        mVisible = visible;
    }

    bool isVisible() const {
        return getRenderable() != null && !isMarkedForDeletion() && mVisible;
    }

    void markForDeletion() {
        mMarkedForDelete = true;
    }

    bool isMarkedForDeletion() const {
        return mMarkedForDelete;
    }

    // More GameObject functions

private:
    bool mVisible;
    bool mMarkedForDelete;
};

RendererClasse (parziale) .

class Renderer {
public:
    void renderObjects(std::vector<GameObject>& gameObjects) {
        // If you want to do something fancy with the renderable GameObjects,
        // create a visitor class to return the list of GameObjects that
        // are visible instead of rendering them straight-away
        std::list<GameObject>::iterator itr = gameObjects.begin(), end = gameObjects.end();
        while (itr != end) {
            GameObject* gameObject = *itr++;
            if (gameObject == null || !gameObject->isVisible()) {
                continue;
            }
            gameObject->getRenderable()->render(*this);
        }
    }

};

RenderableObject classe:

template <typename T>
class RenderableObject : public Renderable {
public:
    RenderableObject(T& object)
        :mObject(object) {}
    virtual ~RenderableObject(){}

    virtual void render(Renderer& renderer) {
        return render(renderer, mObject);
    }

protected:
    virtual void render(Renderer& renderer, T& object) = 0;
};

ObjectA classe:

// Forward delcare ObjectARenderable and make sure the constructor
// definition in the CPP file where ObjectARenderable gets included
class ObjectARenderable;

class ObjectA : public GameObject {
public:
    ObjectA()
        : mRenderable(new ObjectARenderable(*this)) {}

    // All data/logic

    Renderable* getRenderable() {
        return mRenderable.get();
    }

protected:
    // boost or std shared_ptr to make sure that the renderable instance is
    // cleaned up with the destruction of this object.
    shared_ptr<Renderable> mRenderable;
};

ObjectARenderable classe:

#include "ObjectA.h"

class ObjectARenderable : public RenderableObject<ObjectA> {
public:
    ObjectARenderable(ObjectA& instance) {
        : RenderableObject<ObjectA>(instance) {}

protected:
    virtual void render(Renderer& renderer, T& object) {
        // gl_* class to render ObjectA
    }
};

4

Costruisci un sistema di comandi di rendering. Un oggetto di alto livello, che ha accesso sia agli OpenGLRendereroggetti scenegraph / game, itererà il grafico della scena o degli oggetti gioco e costruirà un batch di RenderCmds, che sarà quindi sottoposto al OpenGLRendererquale disegnerà ciascuno a turno, e quindi contenente tutti OpenGL relativo codice in esso.

Ci sono più vantaggi di questo oltre alla semplice astrazione; eventualmente, man mano che la complessità del rendering aumenta, è possibile ordinare e raggruppare ciascun comando di rendering in base alla trama o allo shader, ad esempio Render()per eliminare molti colli di bottiglia nelle chiamate di disegno che possono fare un'enorme differenza nelle prestazioni.

class OpenGLRenderer
{
public:
    typedef GLuint GeometryBuffer;
    typedef GLuint TextureID;
    typedef std::vector<RenderCmd> RenderBatch; 

    void Render(const RenderBatch& renderBatch);   // set shaders, set active textures, draw geometry, ...

    MeshID CreateGeometryBuffer(...);
    TextureID CreateTexture(...);

    // ....
}

struct RenderCmd
{
    GeometryBuffer mGeometryBuffer;
    TextureID mTexture;
    Mat4& mWorldMatrix;
    bool mLightingEnabled;
    // .....
}

std::vector<GameObject> gYourGameObjects;
RenderBatch BuildRenderBatch()
{
    RenderBatch ret;

    for (GameObject& object : gYourGameObjects)
    { 
        // ....
    }

    return ret;
}

3

Dipende completamente dal fatto che sia possibile fare ipotesi su ciò che è comune per tutte le entità renderizzabili o meno. Nel mio motore, tutti gli oggetti sono resi allo stesso modo, quindi è sufficiente fornire vbo, trame e trasformazioni. Quindi il renderer li recupera tutti, quindi non sono necessarie chiamate alle funzioni OpenGL nei diversi oggetti.


1
tempo = pioggia, sole, caldo, freddo: P -> tempo
Tobias Kienzler

3
@TobiasKienzler Se hai intenzione di correggere la sua ortografia, prova a precisare se correttamente :-)
TASagent

@TASagent What, e frenare la legge di Muphry ? m- /
Tobias Kienzler

1
corretto quel refuso
danijar il

2

Metti sicuramente il codice di rendering e la logica di gioco in diverse classi. La composizione (come suggerito da teodron) è probabilmente il modo migliore per farlo; ogni Entità nel mondo di gioco avrà il proprio Renderable - o forse un set di essi.

Potresti avere ancora più sottoclassi di Rendering, ad esempio per gestire l'animazione scheletrica, gli emettitori di particelle e gli shader complessi, oltre allo shader strutturato e illuminato di base. La classe di rendering e le sue sottoclassi dovrebbero contenere solo le informazioni necessarie per il rendering: geometria, trame e shader.

Inoltre, è necessario separare un'istanza di una determinata mesh dalla mesh stessa. Supponi di avere un centinaio di alberi sullo schermo, ognuno con la stessa maglia. Desideri memorizzare la geometria una sola volta, ma avrai bisogno di matrici di posizione e rotazione separate per ciascun albero. Gli oggetti più complessi, come gli umanoidi animati, avranno anche informazioni aggiuntive sullo stato (come uno scheletro, l'insieme delle animazioni attualmente applicate, ecc.).

Per rendere, l'approccio ingenuo è quello di iterare su ogni entità del gioco e dirgli di renderizzare se stesso. In alternativa, ogni entità (quando si genera) può inserire i suoi oggetti renderizzabili in un oggetto scena. Quindi, la tua funzione di rendering dice alla scena di renderizzare. Ciò consente alla scena di eseguire operazioni complesse relative al rendering senza incorporare quel codice in entità di gioco o in una sottoclasse di rendering specifica.


2

Questo consiglio non è molto specifico per il rendering ma dovrebbe aiutare a creare un sistema che mantenga le cose in gran parte separate. Innanzitutto, cerca di mantenere separati i dati "GameObject" dalle informazioni sulla posizione.

Vale la pena notare che le semplici informazioni sulla posizione XYZ potrebbero non essere così semplici. Se si utilizza un motore fisico, i dati di posizione potrebbero essere memorizzati all'interno del motore di terze parti. Dovresti o sincronizzarti tra loro (il che implicherebbe un sacco di inutili copie di memoria) o interrogare le informazioni direttamente dal motore. Ma non tutti gli oggetti necessitano di fisica, alcuni saranno riparati in posizione, quindi un semplice set di galleggianti funziona bene lì. Alcuni potrebbero anche essere collegati ad altri oggetti, quindi la loro posizione è in realtà un offset di un'altra posizione. In una configurazione avanzata potresti avere la posizione memorizzata solo sulla GPU l'unica volta che sarebbe necessario lato computer è per lo scripting, l'archiviazione e la replica di rete. Quindi avrai probabilmente diverse scelte possibili per i tuoi dati posizionali. Qui ha senso usare l'eredità.

Piuttosto che un oggetto che possiede la sua posizione, tale oggetto dovrebbe essere esso stesso posseduto da una struttura di dati di indicizzazione. Ad esempio, un "Livello" potrebbe avere un Octree, o forse una "scena" di un motore fisico. Quando si desidera eseguire il rendering (o impostare una scena di rendering), si richiede alla struttura speciale oggetti visibili alla telecamera.

Questo aiuta anche a dare una buona gestione della memoria. In questo modo un oggetto che non si trova in un'area non ha nemmeno una posizione che ha senso piuttosto che restituire 0,0 coords o i coords che aveva quando era l'ultimo in un'area.

Se non conservi più le coordinate nell'oggetto, invece di object.getX () finiresti per avere level.getX (oggetto). Il problema è che la ricerca dell'oggetto nel livello sarà probabilmente un'operazione lenta poiché dovrà guardare attraverso tutti i suoi oggetti e corrispondere a quello richiesto.

Per evitarlo probabilmente creerei una classe speciale di "link". Uno che si lega tra un livello e un oggetto. Lo chiamo "Posizione". Ciò conterrebbe le coordinate xyz, nonché l'handle al livello e un handle all'oggetto. Questa classe di collegamento verrebbe memorizzata nella struttura / livello spaziale e l'oggetto avrebbe un debole riferimento ad essa (se il livello / posizione viene distrutto, la rifrazione degli oggetti deve essere aggiornata su null. Potrebbe valere la pena avere effettivamente la classe Location 'possiede' l'oggetto, in questo modo se un livello viene eliminato, così è la struttura dell'indice speciale, le posizioni che contiene e i suoi oggetti.

typedef std::tuple<Level, Object, PositionXYZ> Location;

Ora le informazioni sulla posizione sono memorizzate in un'unica posizione. Non duplicato tra l'oggetto, la struttura di indicizzazione spaziale, il renderer e così via.

Strutture di dati spaziali come Octrees spesso non hanno nemmeno bisogno di avere le coordinate degli oggetti che memorizzano. La posizione viene memorizzata nella posizione relativa dei nodi nella struttura stessa (potrebbe essere considerata una sorta di compressione con perdita di dati, sacrificando l'accuratezza per tempi di ricerca rapidi). Con l'oggetto location nell'ottobre, al termine della query vengono trovate le coordinate effettive al suo interno.

O se stai usando un motore fisico per gestire le posizioni dei tuoi oggetti o una miscela tra i due, la classe Location dovrebbe gestirla in modo trasparente mantenendo tutto il tuo codice in un unico posto.

Un altro vantaggio è ora la posizione e il riferimento al livello è memorizzato nella stessa posizione. È possibile implementare object.TeleportTo (other_object) e farlo funzionare su più livelli. Allo stesso modo, la ricerca del percorso dell'IA potrebbe seguire qualcosa in un'area diversa.

Per quanto riguarda il rendering. Il rendering può avere un'associazione simile alla posizione. Tranne che avrebbe il materiale specifico per il rendering lì dentro. Probabilmente non è necessario che "Oggetto" o "Livello" siano memorizzati in questa struttura. L'oggetto potrebbe essere utile se stai cercando di fare qualcosa come la selezione del colore o il rendering di una hitbar che galleggia sopra di esso e così via, ma per il resto il renderer si preoccupa solo della mesh e simili. RenderableStuff sarebbe una Mesh, potrebbe anche avere delimitatori e così via.

typedef std::pair<RenderableStuff, PositionXYZ> RenderThing;

renderer.render(level, camera);
renderer: object = level.getVisibleObjects(camera);
level: physics.getObjectsInArea(physics.getCameraFrustrum(camera));
for(object in objects) {
    //This could be depth sorted, meshes could be broken up and sorted by material for batch rendering or whatever
    rendering_que.addObjectToRender(object);
}

Potresti non aver bisogno di farlo ogni frame, puoi assicurarti di prendere una regione più grande di quella attualmente mostrata dalla fotocamera. Memorizzalo nella cache, traccia i movimenti degli oggetti per vedere se il riquadro di selezione rientra nel raggio d'azione, traccia i movimenti della fotocamera e così via. Ma non iniziare a fare casini con quel tipo di cose fino a quando non le hai confrontate.

Lo stesso motore fisico potrebbe avere un'astrazione simile, dal momento che non ha bisogno nemmeno dei dati Oggetto, ma solo della mesh di collisione e delle proprietà fisiche.

Tutti i dati del tuo oggetto principale dovrebbero contenere il nome della mesh utilizzata dall'oggetto. Il motore di gioco può quindi andare avanti e caricarlo in qualsiasi formato preferisca senza sovraccaricare la classe di oggetti con un mucchio di cose specifiche per il rendering (che potrebbero essere specifiche per l'API di rendering, ovvero DirectX vs OpenGL).

Mantiene anche diversi componenti separati. Questo rende facile fare cose come sostituire il tuo motore fisico poiché quella roba è per lo più autonoma in un'unica posizione. Rende inoltre molto più semplice l'iterazione. Puoi testare cose come le query di fisica senza dover configurare alcun oggetto falso reale poiché tutto ciò di cui hai bisogno è la classe Location. Puoi anche ottimizzare le cose più facilmente. Rende più ovvio quali query è necessario eseguire su quali classi e singole posizioni per ottimizzarle (ad esempio il livello sopra.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.