Diseñando una clase ResourceManager

17

He decidido que quiero escribir una clase central de ResourceManager / ResourceCache para mi motor de juego de hobby, pero tengo problemas para diseñar un esquema de almacenamiento en caché.

La idea es que ResourceManager tenga un objetivo flexible para la memoria total utilizada por todos los recursos combinados del juego. Otras clases crearán objetos de recursos, que estarán en un estado descargado, y los pasarán al ResourceManager. El ResourceManager luego decide cuándo cargar / descargar los recursos dados, teniendo en cuenta el límite flexible.

Cuando otra clase necesita un recurso, se envía una solicitud al ResourceManager para ello (ya sea utilizando una identificación de cadena o un identificador único). Si se carga el recurso, se pasa una referencia de solo lectura al recurso a la función que realiza la llamada (envuelta en un punto débil contado al que se hace referencia). Si el recurso no está cargado, el administrador marcará el objeto que se cargará en la próxima oportunidad (generalmente al final del dibujo del marco).

Tenga en cuenta que, aunque mi sistema hace un recuento de referencias, solo cuenta cuando se lee el recurso (por lo tanto, el recuento de referencia puede ser 0, pero una entidad aún puede estar haciendo un seguimiento de su uid).

También es posible marcar recursos para cargar con mucha anticipación al primer uso. Aquí hay un boceto de las clases que estoy 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();

};

El problema es que, para mantener el uso total de datos rondando / por debajo del límite flexible, el administrador tendrá que tener una forma inteligente de determinar qué objetos descargar.

Estoy pensando en usar algún tipo de sistema de prioridad (por ejemplo, Prioridad temporal, Prioridad de uso frecuente, Prioridad permanente), combinado con el tiempo de la última desreferencia y el tamaño del recurso, para determinar cuándo eliminarlo. Pero no puedo pensar en un esquema decente para usar, o las estructuras de datos correctas requeridas para administrarlos rápidamente.

¿Podría alguien que ha implementado un sistema como este dar una visión general de cómo ha funcionado? ¿Hay un patrón de diseño obvio que me estoy perdiendo? ¿He hecho esto demasiado complicado? Idealmente, necesito un sistema eficiente y difícil de abusar. ¿Algunas ideas?

Darcy Rayner
fuente
44
La pregunta obvia es "¿necesita las características que estableció para implementar". Si está trabajando en una PC, la configuración de una tapa de memoria es probablemente superflua, por ejemplo. Si su juego está dividido en niveles, y puede determinar qué activos se utilizarán en el nivel, simplemente cargue todo al principio y evite cargar / descargar durante el juego.
Tetrad

Respuestas:

8

No estoy seguro de si esto corresponde a su pregunta al 100%, pero algunos consejos son los siguientes:

  1. Envuelva sus recursos en una manija. Sus recursos deben dividirse en dos: su descripción (generalmente en XML) y los datos reales. El motor debe cargar TODAS las descripciones de recursos al comienzo del juego y crear todos los identificadores para ellos. Cuando un componente solicita un recurso, se devuelve el identificador. De esa forma, las funciones pueden proceder normalmente (aún pueden solicitar el tamaño, etc.). ¿Qué sucede si aún no ha cargado el recurso? Haga un 'recurso nulo' que se use para reemplazar cualquier recurso que se intente dibujar pero que aún no se haya cargado.

Hay un montón más. Hace poco leí este libro " Diseño e implementación de motores de juego " y tiene una muy buena sección donde se dirige y diseña una clase de administrador de recursos.

Sin la funcionalidad ResourceHandle y Memory Budget, esto es lo que recomienda el 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:
};

Tenga en cuenta que la funcionalidad de SetScope se refiere a un diseño de motor en capas de escena donde ScopeLevel se refiere a la escena #. Una vez que se ha ingresado / salido de una escena, todos los recursos de acuerdo con ese alcance se cargan y los que no están en el alcance global se descargan.

Setheron
fuente
Realmente me gusta la idea del objeto NULL y la idea de hacer un seguimiento del alcance. Acababa de pasar por la biblioteca de mi escuela buscando una copia de 'Diseño e implementación del motor de juego', pero sin suerte. ¿El libro entra en detalles sobre cómo manejaría un presupuesto de memoria?
Darcy Rayner
Detalla algunos esquemas simples de administración de memoria. En última instancia, incluso uno básico debería ser mucho mejor que el malloc general, ya que tiende a tratar de ser el mejor para todas las cosas.
Setheron
Terminé eligiendo un diseño bastante similar a este.
Darcy Rayner