Progettazione di una classe ResourceManager


17

Ho deciso di voler scrivere una classe ResourceManager / ResourceCache centrale per il mio motore di gioco per hobby, ma ho problemi a progettare uno schema di memorizzazione nella cache.

L'idea è che ResourceManager abbia un obiettivo soft per la memoria totale utilizzata da tutte le risorse del gioco combinate. Altre classi creeranno oggetti risorsa, che si troveranno in uno stato non caricato, e li passeranno al ResourceManager. Il ResourceManager decide quindi quando caricare / scaricare le risorse indicate, tenendo presente il limite flessibile.

Quando una risorsa è necessaria a un'altra classe, viene inviata una richiesta al ResourceManager (utilizzando un ID stringa o un identificatore univoco). Se la risorsa viene caricata, un riferimento di sola lettura alla risorsa viene passato alla funzione chiamante (racchiuso in un riferimento conteggiato debole_ptr). Se la risorsa non è caricata, il gestore contrassegnerà l'oggetto da caricare alla prossima opportunità (di solito alla fine del disegno della cornice).

Nota che, sebbene il mio sistema esegua un conteggio dei riferimenti, conta solo quando la risorsa viene letta (quindi il conteggio dei riferimenti può essere 0, ma un'entità potrebbe comunque tenere traccia del proprio uid).

È anche possibile contrassegnare le risorse per il caricamento con largo anticipo rispetto al primo utilizzo. Ecco un po 'uno schizzo delle classi che sto usando:

typedef unsigned int ResourceId;

// Resource is an abstract data type.
class Resource
{
   Resource();
   virtual ~Resource();

   virtual bool load() = 0;
   virtual bool unload() = 0;
   virtual size_t getSize() = 0; // Used in determining how much memory is 
                                 // being used.
   bool isLoaded();
   bool isMarkedForUnloading();
   bool isMarkedForReload();
   void reference();
   void dereference();
};

// This template class works as a weak_ptr, takes as a parameter a sub-class
// of Resource. Note it only hands give a const reference to the Resource, as
// it is read only.
template <class T>
class ResourceGuard
{
   public:
     ResourceGuard(T *_resource): resource(_resource)
     {
        resource->reference();
     }

     virtual ~ResourceGuard() { resource->dereference();}
     const T* operator*() const { return (resource); }
   };

class ResourceManager
{
   // Assume constructor / destructor stuff
   public:
      // Returns true if resource loaded successfully, or was already loaded.
      bool loadResource(ResourceId uid);

      // Returns true if the resource could be reloaded,(if it is being read
      // it can't be reloaded until later).
      bool reloadResource(ResourceId uid)

      // Returns true if the resource could be unloaded,(if it is being read
      // it can't be unloaded until later)
      bool unloadResource(ResourceId uid);

      // Add a resource, with it's named identifier.
      ResourceId addResource(const char * name,Resource *resource);

      // Get the uid of a resource. Returns 0 if it doesn't exist.
      ResourceId getResourceId(const char * name);

      // This is the call most likely to be used when a level is running, 
      // load/reload/unload might get called during level transitions.
      template <class T>
      ResourceGuard<T> &getResource(ResourceId resourceId)
      {
         // Calls a private method, pretend it exits
         T *temp = dynamic_cast<T*> (_getResource(resourceId));
         assert(temp != NULL);
         return (ResourceGuard<T>(temp));
      }

      // Generally, this will automatically load/unload data, and is called
      // once per frame. It's also where the caching scheme comes into play.
      void update();

};

Il problema è che, per mantenere l'utilizzo totale dei dati in bilico / al di sotto del limite flessibile, il gestore dovrà avere un modo intelligente per determinare quali oggetti scaricare.

Sto pensando di utilizzare un qualche tipo di sistema di priorità (ad es. Priorità temporanea, Priorità utilizzata frequentemente, Priorità permanente), combinato con il tempo dell'ultima dereferenza e la dimensione della risorsa, per determinare quando rimuoverlo. Ma non riesco a pensare a uno schema decente da utilizzare o alle giuste strutture dati necessarie per gestirle rapidamente.

Qualcuno che ha implementato un sistema come questo potrebbe dare una panoramica di come ha funzionato. Esiste un ovvio modello di design che mi sto perdendo? L'ho reso troppo complicato? Idealmente ho bisogno di un sistema efficiente e difficile da abusare. Qualche idea?


4
La domanda ovvia è "hai bisogno delle funzionalità che hai impostato per implementare". Se stai lavorando su un PC, l'impostazione di un soft cap di memoria è probabilmente superflua, ad esempio. Se il tuo gioco è suddiviso in livelli e puoi determinare quali risorse verranno utilizzate nel livello, carica tutto all'inizio ed evita di caricare / scaricare durante il gioco.
Tetrad,

Risposte:


8

Non sono sicuro che ciò riguardi la tua domanda al 100%, ma alcuni consigli sono i seguenti:

  1. Avvolgi le tue risorse in una maniglia. Le tue risorse dovrebbero essere divise in due: la loro descrizione (di solito in XML) e i dati effettivi. Il motore dovrebbe caricare TUTTE le descrizioni delle risorse all'inizio del gioco e creare tutti gli handle per loro. Quando un componente richiede una risorsa, viene restituito l'handle. In questo modo le funzioni possono procedere normalmente (possono comunque richiedere le dimensioni ecc.). E se non avessi ancora caricato la risorsa? Crea una "risorsa nulla" che viene utilizzata per sostituire qualsiasi risorsa che si tenta di disegnare ma che non è stata ancora caricata.

C'è un mucchio di più. Di recente ho letto questo libro " Progettazione e implementazione di Game Engine " e una sezione molto bella in cui va e progetta una classe di gestione delle risorse.

Senza la funzionalità ResourceHandle e Memory Budget ecco cosa consiglia il libro:

typedef enum
{
    RESOURCE_NULL = 0,
    RESOURCE_GRAPHIC = 1,
    RESOURCE_MOVIE = 2,
    RESOURCE_AUDIO = 3,
    RESOURCE_TEXT =4,
}RESOURCE_TYPE;


class Resource : public EngineObject
{
public:
    Resource() : _resourceID(0), _scope(0), _type(RESOURCE_NULL) {}
    virtual ~Resource() {}
    virtual void Load() = 0;
    virtual void Unload()= 0;

    void SetResourceID(UINT ID) { _resourceID = ID; }
    UINT GetResourceID() const { return _resourceID; }

    void SetFilename(std::string filename) { _filename = filename; }
    std::string GetFilename() const { return _filename; }

    void SetResourceType(RESOURCE_TYPE type) { _type = type; }
    RESOURCE_TYPE GetResourceType() const { return _type; }

    void SetResourceScope(UINT scope) { _scope = scope; }
    UINT GetResourceScope() const { return _scope; }

    bool IsLoaded() const { return _loaded; }
    void SetLoaded(bool value) { _loaded = value; }

protected:
    UINT _resourceID;
    UINT _scope;
    std::string _filename;
    RESOURCE_TYPE _type;
    bool _loaded;
private:
};

class ResourceManager : public Singleton<ResourceManager>, public EngineObject
{
public:
    ResourceManager() : _currentScope(0), _resourceCount(0) {};
    virtual ~ResourceManager();
    static ResourceManager& GetInstance() { return *_instance; }

    Resource * FindResourceByID(UINT ID);
    void Clear();
    bool LoadFromXMLFile(std::string filename);
    void SetCurrentScope(UINT scope);
    const UINT GetResourceCount() const { return _resourceCount; }
protected:
    UINT _currentScope;
    UINT _resourceCount; //Total number of resources unloaded and loaded
    std::map<UINT, std::list<Resource*> > _resources; //Map of form <scope, resource list>

private:
};

Si noti che la funzionalità SetScope si riferisce a un design del motore a livelli di scena in cui ScopeLevel si riferisce al numero di scena. Una volta che una scena è stata inserita / uscita, tutte le risorse in base a tale ambito vengono caricate e tutte quelle che non rientrano nell'ambito globale vengono scaricate.


Mi piace molto l'idea NULL Object e l'idea di tenere traccia dell'ambito. Avevo appena passato la biblioteca della mia scuola a cercare una copia di "Game Engine Design and Implementation", ma senza fortuna. Il libro fornisce dettagli su come gestire un budget di memoria?
Darcy Rayner,

Descrive in dettaglio alcuni semplici schemi di gestione della memoria. Alla fine anche uno di base dovrebbe essere molto meglio del malloc generale poiché tende a cercare di essere il migliore per tutte le cose.
Setheron,

Ho finito per scegliere un design abbastanza simile a questo.
Darcy Rayner,
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.