Criando uma classe ResourceManager


17

Decidi que desejo escrever uma classe central ResourceManager / ResourceCache para o meu mecanismo de jogos por hobby, mas estou tendo problemas para projetar um esquema de cache.

A idéia é que o ResourceManager tenha um objetivo flexível para a memória total usada por todos os recursos do jogo combinados. Outras classes criarão objetos de recursos, que estarão em um estado descarregado, e os passarão para o ResourceManager. O ResourceManager decide quando carregar / descarregar os recursos fornecidos, mantendo o limite flexível em mente.

Quando um recurso é necessário por outra classe, uma solicitação é enviada ao ResourceManager para ele (usando um ID de string ou um identificador exclusivo). Se o recurso for carregado, uma referência somente leitura ao recurso será passada para a função de chamada (envolvida em um fraco_ptr contado referenciado). Se o recurso não for carregado, o gerente marcará o objeto a ser carregado na próxima oportunidade (geralmente no final do desenho do quadro).

Observe que, embora meu sistema faça alguma contagem de referência, ele só conta quando o recurso está sendo lido (portanto, a contagem de referência pode ser 0, mas uma entidade ainda pode estar acompanhando seu uid).

Também é possível marcar recursos para carregamento bem antes do primeiro uso. Aqui está um esboço das classes que estou 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();

};

O problema é que, para manter o uso total de dados pairando em torno do limite flexível, o gerente precisará ter uma maneira inteligente de determinar quais objetos descarregar.

Estou pensando em usar algum tipo de sistema de prioridade (por exemplo: Prioridade Temporária, Prioridade Freqüentemente Usada, Prioridade Permanente), combinado com o tempo da última desreferência e o tamanho do recurso, para determinar quando removê-lo. Mas não consigo pensar em um esquema decente para usar ou nas estruturas de dados corretas necessárias para gerenciá-las rapidamente.

Alguém que tenha implementado um sistema como esse pode fornecer uma visão geral de como os seus trabalhos funcionam. Existe um padrão de design óbvio que estou perdendo? Eu fiz isso muito complicado? Idealmente, preciso de um sistema eficiente e difícil de abusar. Alguma ideia?


4
A pergunta óbvia é "você precisa dos recursos que deseja implementar". Se você estiver trabalhando em um PC, definir um limite de memória flexível provavelmente é supérfluo, por exemplo. Se o seu jogo estiver dividido em níveis, e você pode determinar quais ativos serão usados ​​no nível, basta carregar tudo no início e evitar carregar / descarregar durante o jogo.
Tetrad

Respostas:


8

Não tenho certeza se isso se refere à sua pergunta 100%, mas algumas dicas de conselhos são as seguintes:

  1. Enrole seus recursos em uma alça. Seus recursos devem ser divididos em dois: a descrição (geralmente em XML) e os dados reais. O mecanismo deve carregar TODAS as descrições de recursos no início do jogo e criar todos os identificadores para eles. Quando um componente solicita um recurso, o identificador é retornado. Dessa forma, as funções podem prosseguir normalmente (elas ainda podem solicitar o tamanho etc.). Agora, e se você ainda não carregou o recurso? Crie um 'recurso nulo' usado para substituir qualquer recurso que seja tentado a ser desenhado, mas ainda não foi carregado.

Há muito mais. Recentemente, li este livro " Design e Implementação de Mecanismos de Jogo " e tem uma seção muito boa para onde ele vai e cria uma classe de gerenciador de recursos.

Sem as funcionalidades ResourceHandle e Memory Budget, aqui está o que o livro recomenda:

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:
};

Observe que a funcionalidade SetScope se refere a um design de mecanismo em camadas de cena em que ScopeLevel se refere à cena #. Após a entrada / saída de uma cena, todos os recursos de acordo com esse escopo são carregados e os que não pertencem ao escopo global são descarregados.


Eu realmente gosto da idéia de Objeto NULL e de acompanhar o escopo. Eu tinha acabado de passar pela biblioteca da minha escola procurando uma cópia do 'Game Engine Design and Implementation', mas sem sorte. O livro detalha como lidaria com um orçamento de memória?
Darcy Rayner

Ele detalha alguns esquemas simples de gerenciamento de memória. Em última análise, mesmo o básico deve ser muito melhor do que o malloc geral, pois isso tende a ser o melhor para todas as coisas.
Setheron 11/11/11

Acabei escolhendo um design bem parecido com isso.
Darcy Rayner
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.