Projektowanie klasy ResourceManager

17

Zdecydowałem, że chcę napisać centralną klasę ResourceManager / ResourceCache dla mojego silnika gry hobby, ale mam problem z zaprojektowaniem schematu buforowania.

Chodzi o to, że ResourceManager ma miękki cel dla całkowitej pamięci używanej przez wszystkie zasoby gry łącznie. Inne klasy utworzą obiekty zasobów, które będą w stanie rozładowanym, i przekażą je do menedżera zasobów. Następnie ResourceManager decyduje, kiedy załadować / rozładować dane zasoby, pamiętając o miękkim limicie.

Gdy zasób jest potrzebny innej klasie, do ResourceManager wysyłane jest żądanie (za pomocą identyfikatora ciągu lub unikalnego identyfikatora). Jeśli zasób jest załadowany, wówczas odwołanie tylko do odczytu do zasobu jest przekazywane do funkcji wywołującej, (zawinięte w liczony jako referencyjny słaby_ptr) Jeśli zasób nie zostanie załadowany, menedżer oznaczy obiekt do załadowania przy następnej okazji (zwykle na końcu rysowania ramki).

Zauważ, że chociaż mój system wykonuje pewne odliczanie referencji, liczy się tylko podczas odczytywania zasobu (więc liczba referencji może wynosić 0, ale jednostka może nadal śledzić swój identyfikator użytkownika).

Możliwe jest również oznaczenie zasobów do załadowania z dużym wyprzedzeniem przed pierwszym użyciem. Oto trochę szkic klas, z których korzystam:

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

};

Problem polega na tym, że aby utrzymać całkowite wykorzystanie danych w pobliżu / poniżej miękkiego limitu, menedżer musi mieć sprytny sposób określania, które obiekty należy rozładować.

Zastanawiam się nad użyciem pewnego rodzaju systemu priorytetów (np. Priorytet tymczasowy, Często używany priorytet, Stały priorytet), w połączeniu z czasem ostatniej dereferencji i wielkością zasobu, aby określić, kiedy należy go usunąć. Ale nie mogę wymyślić przyzwoitego schematu do użycia ani odpowiednich struktur danych wymaganych do szybkiego zarządzania nimi.

Czy ktoś, kto wdrożył taki system, może przedstawić przegląd działania swojego systemu? Czy brakuje mi oczywistego wzorca projektowego? Czy uczyniłem to zbyt skomplikowanym? Idealnie potrzebuję sprawnego i trudnego do nadużyć systemu. Jakieś pomysły?

Darcy Rayner
źródło
4
Oczywistym pytaniem jest „czy potrzebujesz funkcji, które zamierzasz wdrożyć”. Jeśli pracujesz na komputerze, ustawienie miękkiej nakładki pamięci jest prawdopodobnie na przykład zbyteczne. Jeśli Twoja gra jest podzielona na poziomy i możesz określić, jakie zasoby będą używane na danym poziomie, po prostu załaduj wszystko na początku i unikaj ładowania / rozładowywania w ogóle podczas gry.
Tetrad

Odpowiedzi:

8

Nie jestem pewien, czy odnosi się to do twojego pytania w 100%, ale kilka porad jest następujących:

  1. Owiń swoje zasoby w uchwyt. Twoje zasoby powinny być podzielone na dwa: ich opis (zwykle w formacie XML) i rzeczywiste dane. Silnik powinien załadować WSZYSTKIE opisy zasobów na początku gry i utworzyć dla nich wszystkie uchwyty. Gdy składnik żąda zasobu, uchwyt jest zwracany. W ten sposób funkcje mogą działać normalnie (nadal mogą żądać rozmiaru itp.). Co teraz, jeśli nie załadowałeś jeszcze zasobu? Utwórz „zerowy zasób”, który będzie używany do zastąpienia dowolnego zasobu, który próbuje zostać narysowany, ale nie został jeszcze załadowany.

Jest o wiele więcej. Niedawno przeczytałem tę książkę „ Projektowanie i wdrażanie silnika gry ” i ma bardzo fajną sekcję, w której opisuje i projektuje klasę menedżera zasobów.

Bez funkcji ResourceHandle i Memory Memory oto co zaleca książka:

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

Zauważ, że funkcjonalność SetScope odnosi się do projektu silnika warstwowego, w którym ScopeLevel odnosi się do sceny #. Po wejściu / wyjściu sceny wszystkie zasoby zgodnie z tym zakresem są ładowane, a wszystkie nie znajdujące się w zasięgu globalnym są rozładowywane.

Setheron
źródło
Bardzo podoba mi się idea obiektu NULL i pomysł śledzenia zakresu. Właśnie przeszukałem bibliotekę szkolną, szukając kopii „Projektowanie i wdrażanie silnika gry”, ale bez powodzenia. Czy książka szczegółowo opisuje, jak poradzi sobie z budżetem pamięci?
Darcy Rayner,
Opisuje kilka prostych schematów zarządzania pamięcią. Ostatecznie nawet podstawowy powinien być znacznie lepszy niż ogólny malloc, ponieważ zwykle stara się być najlepszy dla wszystkich rzeczy.
Setheron
Ostatecznie wybrałem projekt bardzo podobny do tego.
Darcy Rayner,