Si tratta di una situazione comunemente riscontrata in C ++? C'è un modello per gestirlo?

2

Sto scrivendo un gioco in C ++ e qualcosa che ho notato è che ho molti file di risorse che devono essere caricati dopo un particolare punto di inizializzazione.

Ad esempio, le trame OpenGL e i VAO non possono essere creati finché non c'è un contesto OpenGL corrente. Una volta caricati, non saranno mai modificati, quindi è più conveniente renderli globali.

Se questi oggetti potessero essere creati senza un contesto OpenGL corrente, sarebbe banale da fare:

const texture ground_texture = load_texture("ground.png");
const texture robot_texture = load_texture("robot.png");
const model robot_model = load_model("robot.obj");

dove texture e model sono semplici strutture POD contenenti (tra le altre cose) ID oggetto OpenGL.

Invece, posso farlo:

texture ground_texture;
texture robot_texture;
model robot_model;

void load_resources() {
    ground_texture = load_texture("ground.png");
    robot_texture = load_texture("robot.png");
    robot_model = load_model("robot.obj");
}

e quindi chiama load_resources dopo l'inizializzazione del contesto OpenGL. Ma questo richiede circa il doppio del codice (tre volte se anche questi globali vengono dichiarati anche in un file di intestazione) e perde il modificatore const .

Si, concettualmente, potrebbe essere prodotta da un semplice script per ridurre la duplicazione - allora l'unico aspetto negativo rispetto al codice non-lavoro originale è che le variabili globali non sono const

.

C'è anche questo:

class texture_loader;
class model_loader;
static std::vector<const texture_loader*> tex_loaders;
static std::vector<const model_loader*> model_loaders;

class texture_loader {
    mutable texture tex;
    mutable bool initialized;
    const char *filename;
public:
    texture_loader(const char *filename)
        : initialized(false), filename(filename)
    {
        tex_loaders.push_back(this);
    }

    void load() const {
        tex = load_texture(filename);
        initialized = true;
    }

    operator texture() const {
        assert(initialized);
        return tex;
    }
};
// similarly for model_loader

const texture_loader ground_texture("ground.png");
const texture_loader robot_texture("robot.png");
const model_loader robot_model("robot.obj");

void load_textures() {
    for(texture_loader *t : tex_loaders) t->load();
    for(model_loader *m : model_loaders) m->load();
}

ma sembra abbastanza complessa per qualcosa che è concettualmente semplice. Inoltre, texture_loader e model_loader (e anything_else_loader ) dovrebbero essere esposti all'esterno del modulo di caricamento delle risorse in modo che l'altro codice possa essere in grado di utilizzare i globali.

Si tratta di un caso comune? Come è in genere risolto?

    
posta immibis 03.10.2014 - 10:19
fonte

4 risposte

3

Anche se è vero che queste cose non cambieranno mai, queste trame e questi modelli sono logicamente parte di una parte specifica del tuo programma: l'ambiente 3D. Hanno senso solo e saranno usati solo in questo contesto. Quindi non dovrebbero essere globali; dovrebbero essere dichiarati all'interno del contesto pertinente del tuo programma.

Un principio di base è che tutte le variabili dovrebbero avere lo scopo più piccolo che si adatta al loro caso d'uso e dovrebbero essere dichiarate il più vicino possibile a dove sono usate. Anche se le variabili non cambieranno mai, c'è un vantaggio nel limitarne l'ambito:

  • Rende il codice più facile da capire.
  • Rende il codice più flessibile da modificare in futuro.
  • Aiuta il riutilizzo del codice (puoi prendere un pezzo di codice specifico e utilizzarlo altrove senza portare con sé un gruppo di variabili globali).
risposta data 03.10.2014 - 10:35
fonte
1

In genere i giochi hanno risorse comuni che vengono create una volta che rimangono residente. Tuttavia, consiglierei di esporre tali risorse come globali variabili. Un'opzione migliore sarebbe gestire tutta la gestione delle risorse tramite un tipo ResourceManager . Il gestore risorse potrebbe essere un singleton.

Il gestore delle risorse dovrebbe funzionare fondamentalmente come una cache di risorse. Consentendo all'utente di richiedere una risorsa tramite un identificatore, come il nome del file, caricare la risorsa in modo trasparente se non è stata memorizzata nella cache o è appena tornata un'istanza esistente. Il gestore risorse potrebbe anche essere in grado di precaricare risorse una volta avviata l'applicazione. È anche l'entità responsabile per la gestione della durata delle risorse.

Concettualmente, potrebbe essere qualcosa del genere:

class ResourceManager {
public:

    // Preload important stuff.
    // This should probably be called at startup,
    // but after the rendering context is created.
    void preloadResources();

    // Find or load a new resource.
    // If the resource is already available, this is a quick
    // table lookup, else, it is loaded and cached.
    const Texture * findOrLoadTexture(const string & filename);

    // Other methods for other resource types...
    // Or you could have a generic 'Resource' base type.
};

Quindi nel codice della logica di gioco, fare riferimento a una risorsa diventa molto semplice:

void Game::init()
{
    ResourceManager::preloadResources();
}

void Player::init()
{
    robot_texture = ResourceManager::findOrLoadTexture("robot.png");
    robot_model   = ResourceManager::findOrLoadModel("robot.obj");
}

Puoi diventare molto sofisticato con ResourceManager , aggiungendo uno smart cache che può "garbage raccogliere" risorse inutilizzate. Una volta che hai un solido interfaccia installata, questo diventa facile. Inoltre, probabilmente lo faresti non utilizzare direttamente i puntatori grezzi, ma i puntatori intelligenti (come std::shared_ptr ) per tenere traccia corretta ed efficiente della durata delle risorse.

    
risposta data 03.10.2014 - 19:59
fonte
0

In genere, i globals e il loro travestimento amichevole sono comunemente usati eccessivamente.

Tuttavia, il fatto che i dati non vengano modificati una volta caricati significa che, se il processo è multithread, una volta che si è certi di essere effettivamente caricati, è possibile che più thread accedano ai dati allo stesso modo.

Ci sono diversi modelli che potrebbero essere utilizzati per caricare i dati:

  • Tutti in anticipo. Il modello più semplice All'avvio, carichi tutto prima che tutto possa andare. Purtroppo trovo che troppe app siano colpevoli di questo, soprattutto se caricano qualcosa di cui non ti interessa. Windows Media Player ti fa aspettare mentre carica / aggiorna la sua "libreria" quando tutto ciò che vuoi è riprodurre un file musicale o un video di cui sai che la posizione è un tipico esempio.

  • Tutti pigri. Nulla è precaricato e caricato solo la prima volta che viene richiesto. Questo funziona in alcuni casi.

  • Un mix: alcuni pre-caricamento, altri caricati pigri.

  • Precaricamento in un thread in background. Gli articoli vengono caricati all'inizio ma l'applicazione può essere eseguita prima che vengano caricati tutti. Se richiedi qualcosa che non è ancora stato caricato, devi aspettare (o avere una logica per caricare quell'elemento istantaneamente).

In un ambiente di caricamento multithreading puoi utilizzare le chiamate future e una volta caricate per assicurarti che i dati vengano caricati esattamente.

    
risposta data 03.10.2014 - 12:18
fonte
0

Crea un oggetto che carica quelle trame e richiede un parametro costruttore per l'oggetto che imposta il prerequisito.

class OpenGLContext
{
public: 
    OpenGLContext() { // set up context etc }
}

class TextureRepository
{
public: 
     TextureRepository(OpenGLContext& context) { // set up textures } 
     TextureRepository() = delete;
}

Poiché il responsabile di OpenGLContext imposta il contesto e il repository richiede un riferimento a quell'oggetto, si ha un modo naturale di esprimere tale dipendenza, e il compilatore ti dirà se lo dimentichi.

Questo approccio può essere esteso a tutti i tipi di risorse con tali dipendenze.

    
risposta data 03.10.2014 - 13:54
fonte

Leggi altre domande sui tag