Conception d'une classe ResourceManager

17

J'ai décidé que je voulais écrire une classe centrale ResourceManager / ResourceCache pour mon moteur de jeu de loisir, mais j'ai du mal à concevoir un schéma de mise en cache.

L'idée est que le ResourceManager a une cible souple pour la mémoire totale utilisée par toutes les ressources du jeu combinées. D'autres classes créeront des objets de ressource, qui seront dans un état non chargé, et les passeront au ResourceManager. Le ResourceManager décide alors quand charger / décharger les ressources données, en gardant à l'esprit la limite souple.

Lorsqu'une ressource est requise par une autre classe, une demande est envoyée au ResourceManager pour celle-ci (en utilisant soit un identifiant de chaîne soit un identifiant unique). Si la ressource est chargée, une référence en lecture seule à la ressource est transmise à la fonction appelante (enveloppée dans un faible_ptr compté référencé). Si la ressource n'est pas chargée, le gestionnaire marquera l'objet à charger à la prochaine opportunité (généralement à la fin du dessin du cadre).

Notez que, bien que mon système effectue un comptage de références, il ne compte que lorsque la ressource est en cours de lecture (donc le nombre de références peut être 0, mais une entité peut toujours garder une trace de son uid).

Il est également possible de marquer les ressources à charger bien avant la première utilisation. Voici un petit aperçu des classes que j'utilise:

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();

};

Le problème est que, pour maintenir l'utilisation totale des données autour / sous la limite souple, le gestionnaire devra avoir un moyen intelligent de déterminer les objets à décharger.

Je pense utiliser une sorte de système de priorité (par exemple, priorité temporaire, priorité fréquemment utilisée, priorité permanente), combiné avec l'heure de la dernière déréférence et la taille de la ressource, pour déterminer quand la supprimer. Mais je ne peux pas penser à un schéma décent à utiliser, ni aux bonnes structures de données nécessaires pour les gérer rapidement.

Quelqu'un qui a mis en place un système comme celui-ci pourrait-il donner un aperçu de son fonctionnement? Y a-t-il un modèle de conception évident qui me manque? Ai-je rendu cela trop compliqué? Idéalement, j'ai besoin d'un système efficace et difficile à abuser. Des idées?

Darcy Rayner
la source
4
La question évidente est "avez-vous besoin des fonctionnalités que vous avez l'intention de mettre en œuvre". Si vous travaillez sur un PC, la définition d'un soft cap mémoire est probablement superflue, par exemple. Si votre jeu est divisé en niveaux et que vous pouvez déterminer quels actifs vont être utilisés dans le niveau, chargez tout au début et évitez tout chargement / déchargement pendant le jeu.
Tetrad

Réponses:

8

Je ne sais pas si cela se rapporte à 100% à votre question, mais quelques conseils sont les suivants:

  1. Enveloppez vos ressources dans une poignée. Vos ressources doivent être divisées en deux: leur description (généralement en XML) et les données réelles. Le moteur doit charger TOUTES les descriptions de ressources au début du jeu et créer toutes les poignées pour elles. Lorsqu'un composant demande une ressource, le descripteur est renvoyé. De cette façon, les fonctions peuvent continuer normalement (elles peuvent toujours demander la taille, etc.). Et si vous n'avez pas encore chargé la ressource? Créez une «ressource nulle» qui est utilisée pour remplacer toute ressource qui a été tentée d'être dessinée mais qui n'a pas encore été chargée.

Il y en a beaucoup plus. J'ai récemment lu ce livre " Game Engine Design and Implementation " et a une très belle section où il va et conçoit une classe de gestionnaire de ressources.

Sans la fonctionnalité ResourceHandle et Memory Budget, voici ce que le livre recommande:

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

Notez que la fonctionnalité SetScope fait référence à une conception de moteur en couches de scène où ScopeLevel fait référence à la scène #. Une fois qu'une scène a été entrée / sortie, toutes les ressources selon cette portée sont chargées et toutes celles qui ne sont pas dans la portée globale sont déchargées.

Setheron
la source
J'aime vraiment l'idée NULL Object et l'idée de garder une trace de la portée. Je venais de parcourir la bibliothèque de mon école à la recherche d'une copie de «Game Engine Design and Implementation», mais sans succès. Le livre décrit-il en détail comment il gérerait un budget mémoire?
Darcy Rayner
Il détaille quelques schémas simples de gestion de la mémoire. En fin de compte, même un modèle de base devrait être bien meilleur que le malloc général, car il a tendance à essayer d'être le meilleur pour toutes choses.
Setheron
J'ai fini par choisir un design assez similaire à celui-ci.
Darcy Rayner, le