Thanks a lot, everyone : )
Noizex, I understood the implementation but the key-identifier part. While I understood that std::strings can be a bit of a power consuming thing to compare, how would I do it with integers?
Well, example:
A game object gets created, it knows a file path of its texture. Now it requests the asset-manager if this has been allocated already. The asset-manager would iterate through a list of collected assets with all their <type>s.
What I thought about is to compare the strings with each other, as every asset-item knows its original path. Storing std::strings is not that lovely for such an undertaking, especially comparing them. If I would use something like you suggested, I could simply compare numbers, is that right?
The new game object would simply send the type it is looking for together with an ID, maybe send the file-path as well, to directly allocate if it does not exist.
But how would I define what asset would get what number? Or is there something better?
I think you're confusing several concepts here and complicate unnecessarily. When you load the resource you want to supply whatever ID you need to use to identify this resource and its version on a medium that you store it. It may be file path, it may be just a name or numeric id, it may be something more complex like a struct that contains both, filename and version. It doesn't matter what type it will be, as long as your loader knows how to deal with it. One thing matters - type should be hashable because you want to store references to your resources in a unique_map or similar container for fast lookup. You don't want iterate over a vector and look up for the resource comparing strings, as you suggest above.
So for resource key just use whatever type is most convenient for you. Strings are perfectly fine for this, as they're hashable. You can make it all compile-time even with some C++ magic, but that's probably only needed if you do a lot of lookups on thousands of resources.
In my case, I don't ask manager each time I need the resource (so, each frame just before rendering), I use a simple refcount and don't return plain pointer but a handle that can be resolved to a pointer (this also acts as a proxy and allows returning placeholder when doing async load). This resolving doesn't use a map but rather a vector, and a handle keeps index to the resource in that vector (this is the main container for my resources, map just holds references in case the key-lookup is needed through Get<T> method). This requires designing a good policy of when the handle is valid, what happens when resources in vector need to be moved to other indices and so on, but otherwise is fast where it's needed and allows easy lookup when it's needed.
In terms of code it looks a bit like this snippet:
struct Model {
Resource<Shader> shader;
Resource<Texture> texture;
Resource<Mesh> mesh;
void LoadResources(ResourceManager& resources)
{
# Here it does the std::unique_map lookup on the Store class (mind that Store class is provided by default
# but also can be user-defined with completly different storage implementation, so this unique_map is just
# what is the usual case)
shader = resources.Get<Shader>(ShaderKey("base_shader", E_NormalMapped | E_Lighting));
texture = resources.Get<Texture>("texture.dds");
mesh = resource.Get<Mesh>("mesh.iqm");
}
}
Now a bit on what happens when you Get<T>:
- Find appropriate meta data for resource type T - meta data contains load/store/retrieve function callbacks, Store object and Loader object
- Call Store->Retrieve(key) to check if resource exists in the cache
- If resource exists, Store returns Resource<T> handle to it and increases refcount
- If resource doesn't exist Store returns empty handle
- If handle is empty, proceed to loading the object, so Loader->Load(key) is called
- Depending on the loader, it either fetches resource from somewhere, or creates it using factory class, this all happens inside Loader and other classes like Store or ResourceManager itself don't know anything about it. Loader just has to return the resource (this time it's unique_ptr) or nullptr
- When loader returns and the handle is not empty, it gets stored into Store, increasing refcount to 1 (so the store callback is called with the unique_ptr, which is then moved into the Store, as the store is the owning entity for resources)
- Handle is returned to user
Later on, when I actually need to use the resource I do it as if it were a normal pointer to resource it holds:
RenderModel(mesh->GetVertexBuffer(), mesh->GetIndexBuffer(), texture->GetId(), shader.get());
When -> is called, the handle does the call on the owning Store (to which it holds a pointer) like:
m_Owner->Get(m_ResourceIndex);
where m_ResourceIndex is index into resource vector. This way it can return placeholder, or do all sort of things, because it has this indirection which you don't get if you point straight to Resource* pointer.
So to rendering system I usually pass straight numbers of resources (GLuints in my case), but sometimes I need resource object itself like in Shader case (I need to call few more methods and Shader keeps the state for this, like caching uniforms per shader type). This is why last argument is passed as shader.get(), which returns plain pointer to Shader*, just like std::unique_ptr and similar smart pointers. There is a policy that resource manager will never free any resource during rendering phase, so passing raw pointer and holding to it for a while (during the journey of RenderTask through queues, sorting, and into actual render call) is fine because we guarantee that the resource will stay alive during that time.
It's still a naive implementation so there may be some problems, but it worked for me so far and it's quite flexible so I hope it helps you a bit and not adds more confusion :)