2014-09-28 26 views
6

Sono un grande fan di avere un motore di gioco che ha la capacità di adattarsi, non solo in quello che può fare, ma anche in come può gestire il nuovo codice. Recentemente, per la mia grafica sottosistema, ho scritto una class per essere ignorato che funziona in questo modo:Wrapper su API grafiche

class LowLevelGraphicsInterface { 
    virtual bool setRenderTarget(const RenderTarget* renderTarget) = 0; 
    virtual bool setStreamSource(const VertexBuffer* vertexBuffer) = 0; 
    virtual bool setShader(const Shader* shader) = 0; 
    virtual bool draw(void) = 0; 

    //etc. 
}; 

La mia idea era quella di creare un elenco di funzioni che sono universali tra la maggior parte API grafiche. Poi per DirectX11 vorrei solo creare un nuovo bambino class:

class LGI_DX11 : public LowLevelGraphicsInterface { 
    virtual bool setRenderTarget(const RenderTarget* renderTarget); 
    virtual bool setStreamSource(const VertexBuffer* vertexBuffer); 
    virtual bool setShader(const Shader* shader); 
    virtual bool draw(void); 

    //etc. 
}; 

Ciascuna di queste funzioni sarebbe poi interfacciarsi con DX11 direttamente. Capisco che qui c'è uno strato di riferimento indiretto. Le persone sono disattivate da questo fatto?

È un metodo ampiamente utilizzato? C'è qualcos'altro che potrei/dovrei fare? C'è la possibilità di usare il preprocessore ma questo mi sembra disordinato. Qualcuno ha anche menzionato i modelli . Che cosa ne pensate?

+1

La soluzione è più leggibile, mentre i preprocessori ei modelli possono sbarazzarsi delle funzioni 'virtuali'. – dari

+0

@dari Effettivamente. Adoro la leggibilità e la semplicità, ma non voglio che lo strato di riferimento indiretto rallenti troppo.Onestamente, non esiterei se non fosse per il fatto che stiamo parlando di grafica qui. –

+0

C'è anche l'opzione di un semplice collegamento con l'implementazione pertinente di una classe. Quindi non è necessario l'indirezione. Ma perdi la capacità di gestire diversi di questi allo stesso tempo, e la possibilità di spedire un singolo exe che funziona con qualsiasi combinazione di tali elementi. –

risposta

7

Se le chiamate di funzione virtuale diventano un problema, esiste un metodo di compilazione che rimuove le chiamate virtuali utilizzando una piccola quantità di preprocessore e un'ottimizzazione del compilatore. Una possibile implementazione è qualcosa di simile:

dichiarare il vostro renderer base con funzioni virtuali puri:

class RendererBase { 
public: 
    virtual bool Draw() = 0; 
}; 

dichiarare una specifica implementazione:

#include <d3d11.h> 
class RendererDX11 : public RendererBase { 
public: 
    bool Draw(); 
private: 
    // D3D11 specific data 
}; 

Creare un colpo di testa RendererTypes.h per inoltrare dichiaro basa renderer sul tipo che si desidera utilizzare con un preprocessore:

#ifdef DX11_RENDERER 
    class RendererDX11; 
    typedef RendererDX11 Renderer; 
#else 
    class RendererOGL; 
    typedef RendererOGL Renderer; 
#endif 

Crea anche un colpo di testa Renderer.h per includere le intestazioni appropriate per il renderer:

#ifdef DX11_RENDERER 
    #include "RendererDX11.h" 
#else 
    #include "RendererOGL.h" 
#endif 

Ora ovunque si utilizza il renderer riferiscono ad essa come il tipo Renderer, includono RendererTypes.h nei file di intestazione e Renderer.h nei file cpp.

Ciascuna delle implementazioni del renderer deve essere in progetti diversi. Quindi crea diverse configurazioni di compilazione per compilare con qualsiasi implementazione di renderer che desideri utilizzare. Ad esempio, non vuoi includere il codice DirectX per una configurazione Linux.

Nelle build di debug, le chiamate alle funzioni virtuali potrebbero essere ancora eseguite, ma nelle versioni di rilascio sono ottimizzate perché non si effettuano mai chiamate tramite l'interfaccia di base. Viene utilizzato solo per applicare una firma comune per le classi di renderer in fase di compilazione.

Mentre è necessario un po 'di preprocessore per questo metodo, è minimo e non interferisce con la leggibilità del codice poiché è isolato e limitato ad alcuni typedef e include. L'unico svantaggio è che non è possibile cambiare le implementazioni del renderer in fase di esecuzione utilizzando questo metodo, poiché ogni implementazione sarà costruita su un eseguibile separato. Tuttavia, non c'è davvero molto bisogno di cambiare le configurazioni in fase di esecuzione.

+2

Vale la pena notare che la classe base e le funzioni virtuali non sono necessarie, poiché tutto il codice si riferisce al tipo di calcestruzzo 'Renderer'. Dopo aver risolto ciò, ciò che rimane è la selezione dell'implementazione corretta nel codice sorgente, usando il proprocessore. Non c'è davvero alcuna valida alternativa per fare quella selezione di implementazione nel codice sorgente, ma può essere fatta più in generale tramite il meccanismo di compilazione semplicemente disponendo di appropriati percorsi di intestazione per il sistema in questione. –

+0

In breve, questo può essere notevolmente semplificato. Essenzialmente al mio primo commento (prima che questa risposta fosse pubblicata). –

+1

Vero che la classe base non è necessaria, ma mi piace avere due ragioni. Uno, per far sì che le implementazioni forniscano un'interfaccia coerente e rimangano sincronizzati. Due, per consentire il codice condiviso tra le implementazioni nella classe base. Concordato che l'impostazione dei percorsi di inclusione è una buona soluzione per la gestione delle intestazioni. – megadan

0

Uso l'approccio con una classe di base astratta per il dispositivo di rendering nella mia applicazione. Funziona bene e mi permette di scegliere dinamicamente il renderer da utilizzare in fase di runtime. (Lo uso per passare da DirectX10 a DirectX9 se il primo non è supportato, ad esempio su Windows XP).

Vorrei sottolineare che la chiamata di funzione virtuale non è la parte che costa le prestazioni, ma la conversione dei tipi di argomenti coinvolti. Per essere veramente generico, l'interfaccia pubblica per il renderer utilizza il proprio set di parametri come un IShader personalizzato e un tipo Matrix3D personalizzato. Nessun tipo dichiarato nell'API DirectX è visibile al resto dell'applicazione, poiché OpenGL avrebbe diversi tipi di Matrix e interfacce shader. Lo svantaggio di questo è che devo convertire tutti i tipi Matrix e Vector/Point dal mio tipo personalizzato a quello che lo shader usa nell'implementazione concreta del dispositivo di rendering. Questo è molto più costoso del costo di una chiamata di funzione virtuale.

Se si esegue la distinzione utilizzando il preprocessore, è inoltre necessario mappare i diversi tipi di interfaccia come questo. Molti sono gli stessi tra DirectX10 e DirectX11, ma non tra DirectX e OpenGL.

Modifica: vedere la risposta in c++ Having multiple graphics options per un'implementazione di esempio.

0

Quindi, mi rendo conto che questa è una domanda vecchia, ma non riesco a resistere alla chimera. Volere scrivere un codice come questo è solo un effetto collaterale del tentativo di affrontare l'indottrinamento orientato agli oggetti.

La prima domanda è se lo o realmente debba sostituire i back-end di rendering o semplicemente pensare che sia bello. Se un back-end appropriato può essere determinato al momento della compilazione per una determinata piattaforma, allora il problema è risolto: utilizzare un'interfaccia semplice, non virtuale con un'implementazione selezionata al momento della compilazione.

Se si scopre che è davvero necessario sostituirlo, utilizzare comunque un'interfaccia non virtuale, caricare le implementazioni come librerie condivise. Con questo tipo di scambio, è probabile che sia il codice di rendering del motore sia il codice di rendering specifico per il gioco con prestazioni elevate siano estrapolati e sostituibili. In questo modo, è possibile utilizzare l'interfaccia di rendering del motore di alto livello comune per le operazioni eseguite principalmente dal motore, pur continuando ad accedere al codice specifico di back-end per evitare i costi di conversione menzionati da PMF.

Ora, va detto che mentre lo scambio con le librerie condivise introduce indirezione, 1. Si può facilmente ottenere l'indirezione di essere < a ~ = quella di chiamate virtuali e 2. Questo riferimento indiretto ad alto livello è mai un preoccupazione per le prestazioni in qualsiasi gioco/motore sostanziale. Il vantaggio principale è quello di mantenere il codice morto scaricato (e fuori mano) e semplificare le API e la progettazione generale del progetto, aumentando la leggibilità e la comprensione.

principianti non sono in genere consapevoli di questo, perché c'è così tanto OO cieca spingendo in questi giorni, ma questo stile di "OO prima, porre domande mai" è non senza costi. Questo tipo di progettazione ha un costo di comprensione del codice di tassazione e conduce al codice (molto più basso di questo esempio) che è intrinsecamente lento. L'orientamento all'oggetto ha il suo posto, certamente, ma (nei giochi e in altre applicazioni ad alte prestazioni) il modo migliore di progettare quello che ho trovato è scrivere le applicazioni nel modo più minimale possibile, concedendo solo quando un problema ti costringe a forzare la mano. Svilupperai un'intuizione su dove tracciare la linea man mano che acquisisci più esperienza.