I've decided I want to write a central ResourceManager/ResourceCache class for my hobby game engine, but am having trouble designing a caching scheme.
The idea is that the ResourceManager has a soft target for the total memory used by all the game's resources combined. Other classes will create resource objects, which will be in an unloaded state, and pass them to the ResourceManager. The ResourceManager then decides when to load / unload the given resources, keeping the soft limit in mind.
When a resource is needed by another class a request is sent to the ResourceManager for it, (either using a string id or a unique identifier). If the resource is loaded, then a read-only reference to the resource is passed to the calling function, (wrapped in a referenced counted weak_ptr). If the resource isn't loaded, then the manager will mark the object to be loaded at the next opportunity, (usually at the end of drawing the frame).
Note that, although my system does do some reference counting, it only counts when the resource is being read, (so the reference count may be 0, but an entity might still be keeping track of it's uid).
It is also possible to mark resources for loading well in advance of first use. Here is a bit a sketch of the classes I am using:
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 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
ResourceGuard &getResource(ResourceId resourceId)
{
// Calls a private method, pretend it exits
T *temp = dynamic_cast (_getResource(resourceId));
assert(temp != NULL);
return (ResourceGuard(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();
};
The trouble is, to keep the total data usage hovering around / under the soft limit, the manager will have to have a smart way of determining which objects to unload.
I'm thinking of using some kind of priority system, (eg. Temporary Priority, Frequently Used Priority, Permanent Priority), combined with the time of the last dereference, and the size of the resource, to determine when to remove it. But I can't think of a decent scheme to use, or the right data-structures required to quickly manage them.
Could someone who has implemented a system like this give an overview of how their's worked. Is there an obvious design pattern I am missing out on? Have I made this too complicated? Ideally I need an efficient, and hard to abuse system. Any ideas?
Answer
I'm not sure if this pertains to your question 100% but a few advice tips are the following:
- Wrap your resources in a handle. Your resources should be split into two: their description (usually in XML) and actual data. The engine should load ALL resource descriptions at the start of the game and create all handles for them. When a component requests a resource the handle is returned. That way functions can proceed as normal (they can still request the size etc..). Now what if you haven't loaded the resource yet? Make a 'null resource' that is used to replace any resource that is attempted to be drawn but hasn't been loaded yet.
There's a bunch more. I recently read this book "Game Engine Design and Implementation" and a has very nice section where it goes and designs a resource manager class.
Without the ResourceHandle and Memory Budget functionality here is what the book recommends:
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, 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 > _resources; //Map of form
private:
};
Notice that the SetScope functionality refers to a Scene-Layered Engine Design where the ScopeLevel refers to the Scene#. Once a scene has been entered/exited, all resources according to that scope are loaded and any not in the global scope are unloaded.
No comments:
Post a Comment