Resources revisitedthor::ResourceCache is a very sophisticated resource manager: it detects when the user wants to load a resource twice and tracks lifetime of all resources as well as itself using
shared_ptr and
weak_ptr. It allows for different strategies regarding error handling (null pointer or exception) as well as resource release (automatic or explicit).
On the other side, this comes with certain complexity. Due to the lack of an alternative resource system in Thor, many people have seen
ResourceCache as a general-purpose resource manager, which it is definitely not. A very common use case in games consists of loading resources initially and using them while the game is running. For these cases, not a lot of functionality is needed. A simple class like the one discussed in our book already does the job pretty well.
Over the years, I have spent considerable thought into resource managing systems, and it's something I find rather difficult to get right, especially when you want to cover lots of different needs. I have also not come across many generic solutions so far (as it is often the case with game-related functionality: most people write code specific to their own needs). I originally planned to add another class in addition to the existing
ResourceCache -- this was mainly due to my assumption that because of apparently very different semantics, it would be very hard to combine both. Yesterday, I recognized that the main difference is the ownership, other than that there are many commonalities. Therefore,
ResourceCache will cease to exist in its current form and make room for a better approach.
The class template
ResourceHolder is an attempt to provide a more flexible
and simpler way of handling resources. Resources can be addressed by user-defined IDs. The basic use case looks as follows:
namespace res = thor::Resources;
enum struct TextureType { Grass, Rock };
// Loading
thor::ResourceHolder<sf::Texture, TextureType> textures;
textures.acquire(TextureType::Grass, res::fromFile<sf::Texture>("grass.png"));
textures.acquire(TextureType::Rock, res::fromFile<sf::Texture>("rock.png"));
// Usage
sf::Sprite sprite;
sprite.setTexture(textures[TextureType::Grass]);
Easy, eh? In the basic case, we just have centralized ownership semantics: the
ResourceHolder object stores everything, and people can get references to the resources using
operator[]. Now to the flexibility part: it is also possible to have shared-ownership semantics as before with
ResourceCache. In this case, a reference counter makes sure that the last user cleans up. This implies that there is at least one shared pointer referring to the resource at any time. A third template argument specifies the ownership model, in this case
RefCounted.
// Loading and storing in shared pointers
thor::ResourceHolder<sf::Texture, TextureType, res::RefCounted> textures;
std::shared_ptr<sf::Texture> grass = textures.acquire(TextureType::Grass, ...);
std::shared_ptr<sf::Texture> rock = textures.acquire(TextureType::Rock, ...);
// Later, get another shared pointer, either by copying existing one or directly from holder
std::shared_ptr<sf::Texture> rock2 = rock;
std::shared_ptr<sf::Texture> rock3 = textures[TextureType::Rock];
// Now rock, rock2 and rock3 point to the same resource.
// As soon as all of them go out of scope, the resource is released.
// Keep shared pointer alive while in use
sprite.setTexture(*rock2);
The basic functionality is there, I need to perform some further tests and design some special cases. There are also some interesting features one could still add:
- If user provides no ID, infer it from the way resources are loaded
- Store loader initially, load only on first access
- More ownership models
- ...