Entwerfen einer ResourceManager-Klasse

17

Ich habe beschlossen, eine zentrale ResourceManager / ResourceCache-Klasse für meine Hobby-Game-Engine zu schreiben, habe jedoch Probleme beim Entwerfen eines Caching-Schemas.

Die Idee ist, dass der ResourceManager ein weiches Ziel für den Gesamtspeicher hat, der von allen Ressourcen des Spiels zusammen verwendet wird. Andere Klassen erstellen Ressourcenobjekte, die sich im entladenen Zustand befinden, und übergeben sie an den ResourceManager. Der ResourceManager entscheidet dann, wann die angegebenen Ressourcen geladen / entladen werden sollen, wobei das weiche Limit berücksichtigt wird.

Wenn eine Ressource von einer anderen Klasse benötigt wird, wird eine Anforderung an den ResourceManager gesendet (entweder unter Verwendung einer Zeichenfolgen-ID oder eines eindeutigen Bezeichners). Wenn die Ressource geladen ist, wird ein schreibgeschützter Verweis auf die Ressource an die aufrufende Funktion übergeben (eingeschlossen in ein referenziertes counted weak_ptr). Wenn die Ressource nicht geladen ist, markiert der Manager das zu ladende Objekt bei der nächsten Gelegenheit (normalerweise am Ende des Zeichnens des Rahmens).

Beachten Sie, dass mein System zwar eine Referenzzählung durchführt, diese jedoch nur zählt, wenn die Ressource gelesen wird (der Referenzzähler kann also 0 sein, aber eine Entität kann immer noch die UID verfolgen).

Es ist auch möglich, Ressourcen rechtzeitig vor der ersten Verwendung zum Laden zu markieren. Hier ist eine kleine Skizze der Klassen, die ich benutze:

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

};

Das Problem ist, dass der Manager über eine intelligente Methode verfügen muss, um zu bestimmen, welche Objekte entladen werden sollen, damit die gesamte Datennutzung im Bereich des Softlimits bleibt.

Ich denke an die Verwendung eines Prioritätssystems (z. B. Temporäre Priorität, Häufig verwendete Priorität, Permanente Priorität), kombiniert mit dem Zeitpunkt der letzten Dereferenzierung und der Größe der Ressource, um zu bestimmen, wann diese entfernt werden soll. Aber ich kann mir kein vernünftiges Schema vorstellen oder die richtigen Datenstrukturen, um sie schnell zu verwalten.

Könnte jemand, der ein solches System implementiert hat, einen Überblick über die Funktionsweise seines Systems geben? Gibt es ein offensichtliches Designmuster, das ich verpasse? Habe ich das zu kompliziert gemacht? Idealerweise brauche ich ein effizientes und schwer zu missbrauchendes System. Irgendwelche Ideen?

Darcy Rayner
quelle
4
Die naheliegende Frage lautet: "Benötigen Sie die Funktionen, die Sie implementieren möchten?". Wenn Sie an einem PC arbeiten, ist es wahrscheinlich überflüssig, beispielsweise eine Speicher-Softcap festzulegen. Wenn Ihr Spiel in Ebenen unterteilt ist und Sie bestimmen können, welche Assets in der Ebene verwendet werden sollen, laden Sie einfach alles zu Beginn und vermeiden Sie das Laden / Entladen während des Spiels.
Tetrad

Antworten:

8

Ich bin mir nicht sicher, ob dies zu 100% Ihre Frage betrifft, aber ein paar Ratschläge sind die folgenden:

  1. Wickeln Sie Ihre Ressourcen in einen Griff. Ihre Ressourcen sollten in zwei Teile aufgeteilt werden: ihre Beschreibung (normalerweise in XML) und die tatsächlichen Daten. Die Engine sollte zu Beginn des Spiels ALLE Ressourcenbeschreibungen laden und alle Handles für diese erstellen. Wenn eine Komponente eine Ressource anfordert, wird das Handle zurückgegeben. Auf diese Weise können Funktionen wie gewohnt ausgeführt werden (sie können weiterhin die Größe usw. anfordern). Was ist nun, wenn Sie die Ressource noch nicht geladen haben? Erstellen Sie eine 'Null-Ressource', die verwendet wird, um jede Ressource zu ersetzen, die gezeichnet werden soll, aber noch nicht geladen wurde.

Es gibt noch ein paar mehr. Ich habe kürzlich dieses Buch " Game Engine Design and Implementation " gelesen und einen sehr schönen Abschnitt, in dem es darum geht, eine Ressourcenmanager-Klasse zu entwerfen.

Ohne die Funktionen ResourceHandle und Memory Budget empfiehlt das Buch Folgendes:

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

Beachten Sie, dass sich die SetScope-Funktionalität auf ein Engine-Design mit Szenenebene bezieht, bei dem sich ScopeLevel auf die Szenennummer bezieht. Sobald eine Szene betreten / verlassen wurde, werden alle Ressourcen gemäß diesem Bereich geladen und alle Ressourcen, die nicht im globalen Bereich liegen, werden entladen.

Setheron
quelle
Ich mag die NULL-Objekt-Idee und die Idee, den Überblick über den Umfang zu behalten. Ich hatte gerade in meiner Schulbibliothek nach einer Kopie von 'Game Engine Design and Implementation' gesucht, aber ohne Glück. Geht das Buch auf Details ein, wie es mit einem Speicherbudget umgehen würde?
Darcy Rayner
Es werden einige einfache Speicherverwaltungsschemata beschrieben. Letztendlich sollte sogar eine einfache viel besser sein als die allgemeine Malloc, da dies dazu tendiert, das Beste für alle Dinge zu sein.
Setheron
Am Ende entschied ich mich für ein ähnliches Design.
Darcy Rayner