| A Simple Fast Resource Manager using C++ and STL | |
|
A Simple Fast Resource Manager using C++ and STL
IntroductionResource management is a vital aspect of game development. Every game has resources of various types and an efficient resource management system can improve the overall performance of a game. In this article, we look at an implementation of a resource manager that can be set up with minimal effort, yet which provides noticeably better performance than more generic approaches. The Role of a Resource ManagerA resource manager manages various resources of a game. Meshes, sounds, scripts, shaders, textures can all be considered as resources, yet they have one thing in common: they are loaded from files before use. A resource manager can handle the mundane tasks associated with loading and accessing the resources. Ideally, the resource manager should be fast, efficient, should use minimum memory and also be easy to use at the same time. Design GoalsOur resource manager should satisfy the following goals:
ApproachTo achieve the goals above, we are using the following:
StructureThe Resource ClassTo enable consistent handling and initialization across various types of resources, we have used a common base class for all resources. For fast access, we have chosen to use unsigned integer handles for each resource. The resource class also has a reference count and members to hold the file name and path. The following figure shows the members of the class:
The ResourceManager ClassThe ResourceManager class manages different types of resources. We use a different ResourceManager instance for every type of resource, and thus the ResourceManager is a template class. The members are shown in the following diagram:
The ResourceManager class has four member variables. How it WorksResource TypesFor every type of resource, we need to derive a class from the base resource class. This derived class will contain member variables and methods that are needed for that specific resource type. For example, say we need to use resources of type Mesh. We would need to derive a Mesh class from the Resource class. In addition, we would require the constructor of the Mesh class to accept the parameters handle, name and (optional) path. The Mesh class would hold other members like mesh containers, frame lists, vertices, indices etc. and also methods to update and render the mesh. So the Mesh class may be something like this:
class Mesh :
public Resource
{
private:
Vertex * vertices;
public:
Mesh(const unsigned int handle, const std::string& name, const std::string& path = "./"):
Resource(handle, name, path)
{
//load mesh specific stuff
}
~Mesh()
{
//destroy mesh specific stuff
}
Render()
{
//do rendering here
}
};
ResourceManager CreationWe create a ResourceManager DestructionThe destructor of the ResourceManager class destroys all the resources the ResourceManager loaded and destroys the AccessThe
NULL if the resource is not found. Resources can also be accessed by the [] operator. It works the same way as the second overload of GetElement and is provided for easier access.
RemovalRemoval is done by the Remove method. It takes a handle and checks if removal is possible. If so, it gets the resource indexed by the handle. The reference count of the resource is then decremented. If the reference count reaches 0, the following code runs:
// If the resource is no longer being used then destroy it.
if( resource->GetRefCount() == 0 )
{
//add the handle to the stack of free handles
m_handles.push(handle);
//delete the resource
delete resource;
//mark the slot as NULL
(*m_list)[handle] = NULL;
}
We first save the handle onto the stack. This is done so that on the next insert, that handle will be used (i.e. the next insertion will use that slot in the m_list vector). If a vector is full when another element is being inserted, the vector resizes itself. The resize operation is costly and once a vector grows, it usually can’t shrink – even when elements are removed. Keeping the stack of handles, we “save” the slots in the vector that used to hold resources which were later removed. Using these slots for insertion will mean the vector will not grow while there is an unoccupied slot in it. Since the Remove method does not rely on the number of resources loaded or the number of slots available, it runs in O(1) time. We also set m_list[handle] to NULL so that if for some reason the handle is used to access a resource, NULL is returned.
InsertionInserting a resource is done by the Add method. Here’s the signature: unsigned int Add( const std::string& name, const std::string& path = "./" )The function takes the file location to load the resource from and checks if the specified resource has already been loaded. If it has, then the reference count of the resource is incremented and the handle of the resource is returned. If not, then a resource is created. In this case, a check is made to see if any handles are available on the m_handles stack. If so, that handle is used. If not, then a curent size of the vector serves as the handle and m_size is also incremented. The following code does this:
//Check if there is an available handle. If not, use new handle.
bool handleAvailable = !m_handles.empty();
unsigned int handle;
if(handleAvailable)
{
handle = m_handles.top();
m_handles.pop();
}
else handle = m_list->size();
After that, the callback function is called if it has been set by the user. If not, the constructor of the resource is called. Either way, a resource object is created. Next we check if a slot was available. If so, the resource is added to the vector at that index. If not, it is appended to the end of the vector.
if(handleAvailable) (*m_list)[handle] = resource; else m_list->push_back(resource);The Add function iterates through all of the elements of m_list to see if the element already exists. This means it runs in linear time. This is a performance bottleneck, but since resources are usually created at load time, we chose to go with this approach for the sake of simplicity. Another alternative would be to have an stl::map in the ResourceManager class that would map a filename to a handle. In such an approach, insertion would be reduced to logarithmic time (as GetElement’s first overload would run in logarithmic time), but the complexity of Remove would increase to logarithmic time from the current constant time.
UsageUsing the resource management system is fairly simple. The file sample.cpp has a basic application that uses the system. The first step to using the system to manage resources of a certain type is to create a class for that type. That is what the TestResource class is for. As mentioned before, this class must derive from the basic Resource class and provide a constructor that takes three arguments: the handle, the name of the file and (optional) the path to the file. If the resource class needs initializing anything else, we can do it in the constructor. In the example, the string
TestResource(const unsigned int handle, const string& name, const string& path = "./"):Resource(handle, name, path)
{
stringstream s;
s << "Hello handle: " << handle << " name: " << name <<
" path: " << path;
m_details = s.str();
}
After defining the class, the next step is to create a ResourceManager instance for the type. In our sample, this is done by:
//Create the resource manager for managing TestResources ResourceManager<TestResource> TestResourceManager;After that, we can add a few resources to the manager. The Add method returns the handle for each resource added. Our handles vector is used to hold the handles. We request resources from the ResourceManager by indexing into it by these handles. The following code shows the usage:
for(vectorHere, we have requested resources from the manager and called the ToString() method for each valid resource returned.
In the sample, we have added two resources with the same name and path (“Hello” and “./” respectively). Printing the The
|