Разработка класса ResourceManager

17

Я решил написать центральный класс ResourceManager / ResourceCache для своего движка хобби, но у меня возникли проблемы при разработке схемы кэширования.

Идея состоит в том, что ResourceManager имеет мягкую цель для общего объема памяти, используемой всеми ресурсами игры вместе взятыми. Другие классы будут создавать объекты ресурсов, которые будут в незагруженном состоянии, и передавать их в ResourceManager. Затем ResourceManager решает, когда загружать / выгружать данные ресурсы, учитывая мягкий предел.

Когда ресурс нужен другому классу, в ResourceManager отправляется запрос (с использованием строкового идентификатора или уникального идентификатора). Если ресурс загружен, то вызывающей функции передается доступная только для чтения ссылка на ресурс (обернутая в подсчитанный счётчик weak_ptr). Если ресурс не загружен, менеджер помечает объект для загрузки при следующей возможности (обычно в конце рисования кадра).

Обратите внимание, что, хотя моя система выполняет подсчет ссылок, она учитывается только при чтении ресурса (поэтому счетчик ссылок может быть равен 0, но объект все еще может отслеживать его идентификатор).

Также возможно пометить ресурсы для загрузки заблаговременно до первого использования. Вот немного набросок классов, которые я использую:

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

};

Проблема в том, что для того, чтобы общее использование данных колебалось вокруг или ниже мягкого предела, у менеджера должен быть умный способ определить, какие объекты выгрузить.

Я имею в виду использование какой-либо системы приоритетов (например, Временный приоритет, Часто используемый приоритет, Постоянный приоритет) в сочетании с временем последней разыменования и размером ресурса, чтобы определить, когда его удалять. Но я не могу придумать подходящую схему для использования или правильные структуры данных, необходимые для быстрого управления ими.

Может ли кто-то, кто внедрил подобную систему, дать обзор того, как он работает. Есть ли очевидный шаблон дизайна, который я пропускаю? Я сделал это слишком сложным? В идеале мне нужна эффективная и жесткая система. Есть идеи?

Дарси Рейнер
источник
4
Очевидный вопрос: «Вам нужны функции, которые вы намеревались реализовать». Например, если вы работаете на ПК, установка мягкой крышки памяти излишне. Если ваша игра разбита на уровни, и вы можете определить, какие ресурсы будут использоваться на уровне, просто загрузите все в начале и избегайте загрузки / выгрузки вообще во время игры.
Тетрад

Ответы:

8

Я не уверен, что это относится к вашему вопросу на 100%, но несколько советов следующие:

  1. Оберните ваши ресурсы в ручку. Ваши ресурсы должны быть разделены на две части: их описание (обычно в XML) и фактические данные. Движок должен загрузить ВСЕ описания ресурсов в начале игры и создать для них все дескрипторы. Когда компонент запрашивает ресурс, дескриптор возвращается. Таким образом, функции могут работать как обычно (они могут запрашивать размер и т. Д.). А что если вы еще не загрузили ресурс? Создайте 'нулевой ресурс', который используется для замены любого ресурса, который пытался получить, но еще не был загружен.

Есть еще куча. Недавно я прочитал эту книгу « Проектирование и реализация игрового движка », и в ней есть очень хороший раздел, в котором рассказывается о дизайне класса менеджера ресурсов.

Без функций ResourceHandle и Memory Budget книга рекомендует следующее:

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

Обратите внимание, что функциональность SetScope относится к Scene-Layered Engine Design, где ScopeLevel ссылается на Scene #. Как только сцена введена / завершена, все ресурсы в соответствии с этой областью загружаются, а любые, не входящие в глобальную область, выгружаются.

Setheron
источник
Мне очень нравится идея объекта NULL и идея отслеживания области видимости. Я только что прошел через мою школьную библиотеку в поисках копии «Проектирование и реализация игрового движка», но безуспешно. Рассматривается ли книга подробно, как она будет использовать бюджет памяти?
Дарси Рейнер
В нем подробно описаны несколько простых схем управления памятью. В конечном счете, даже базовый должен быть намного лучше, чем обычный malloc, поскольку он старается быть лучшим для всех вещей.
Сетерон
В итоге я выбрал дизайн, очень похожий на этот.
Дарси Рейнер