Resources Tutorial
Welcome to the tutorial about the Resources module of the Thor Library. Resources are heavyweight objects that can be loaded from the application, such as images, fonts, sound files, etc. In SFML, there exist five classes with resource semantics:
sf::Image
sf::Font
sf::Shader
sf::SoundBuffer
sf::Music
Note that sf::Music
is a special case; it behaves quite distinctly from the rest. Music is streamed and thus not completely loaded into memory. It has no real data, so shared access makes not much sense.
The Thor Resources module provides an interface that simplifies loading and access to these resources. Basically, the module consists of three widely independent parts:
Resource keys
Keys are IDs to access the resources. They contain loading information for the corresponding resource. A resource key for an sf::Image
which should be loaded from a file “image.jpg” is declared like this (given we write using thor::Resources::ImageKey;
for simplicity):
ImageKey key = ImageKey::FromFile("image.jpg");
These static factory methods indirect to the loading functions in sf::Image
, namely Create()
, LoadFromFile()
and LoadFromMemory()
, and they have the same signature. This relation applies for the other resource classes in SFML, too.
As mentioned, resource keys represent identifiers for resource access. Keys that are initialized identically refer to the same resource. Analogously, keys being initialized with different parameters (or even different factory functions) are always distinct, which means they refer to different resources. Note that resource keys do neither store nor reference the resource. The only thing they have is the abstract knowledge about how to load resources and how to differ between them.
Resource managers
The class template thor::ResourceManager
is the core of the resource management system. It maps keys to resources and returns pointers to access them. But before, it has to load the resources into memory, what can be done using the resource keys:
ImageKey key = ImageKey::FromFile("image.jpg");
thor::ResourceManager<sf::Image> mgr;
mgr.Acquire(key);
The Acquire()
function loads the resource according to the way in which the resource key was initialized. In the upper example, the image is loaded from a file called “image.jpg”. The next time we acquire the image using the same key, the ResourceManager
recognizes that the corresponding resource has already been allocated and does not load it again. But when we use a different key (initialized with other parameters or another factory function), we address another resource, therefore it is not cached. To make it clear:
thor::ResourceManager<sf::Image> mgr; mgr.Acquire(key); // Loads a new resource mgr.Acquire(key2); // Loads a new resource mgr.Acquire(key); // No loading (resource already stored)
In case a resource cannot be loaded (e.g. wrong filename), the method Acquire()
throws an exception of type thor::ResourceLoadingException
.
Resource pointers
We have now seen how to load resources, but how can we actually use them? Here comes the third component of the Resources module into play: The class template ResourcePtr
. This smart-pointer offers a safe and elegant way to access the resources as soon as they are loaded. It is returned by the Acquire()
function.
thor::Resources::ImageKey key = …; thor::ResourceManager<sf::Image> mgr; thor::ResourcePtr<sf::Image> image = mgr.Acquire(key);
The smart pointer ResourcePtr
has the following properties:
- Pointer-like behavior through overloaded dereferencing operators
*
and->
to access the resource class. - Shared-ownership semantics. If you copy
ResourcePtr
instances, you get multiple smart pointers that point to the same resource. The instances are lightweight objects, they can be passed around or stored in STL containers without the high cost of copying the resource itself. - Strong reference semantics. As long as a
ResourcePtr
refers to a resource, the latter cannot be released. This prevents accidental destruction of resources that are in use. Note that there is an exception: If theResourceManager
containing the resource is destroyed, all referringResourcePtr
instances are set to NULL (at least, there are no dangling pointers).
When you want to access loaded resources without possibly allocating new ones, Search()
is the function of your choice. It never loads resources. In case nothing is found, a null pointer is returned.
Releasing resources
When you allocate resources, you also have to deallocate them. Or maybe not you alone, the ResourceManager
helps you doing this task. To allow reasonable deallocation, the resource class must support RAII, hence its destructor must take care of cleaning its own resources up. But this is normally no problem when you program in C++. Every SFML resource class does this automatically.
All resources stored in a resource manager are released in the destructor ~ResourceManager()
– unless you release them before. Without further configurations, resources are kept in memory until you explicitly release them or the resource manager is destroyed. This behavior is quite meaningful, as it saves time to reallocate resources that are temporarily unused. On the other hand, there are situations which require a preferably low resource allocation. Here, resources are released as soon as they become unused. The Thor library supports both release strategies; they reside as ExplicitRelease
and AutoRelease
in namespace thor::Resources
.
Now you still don’t know how to release resources! Well, that’s simple, just call the Release()
function:
… // as always
mgr.Release(key);
If the corresponding resource is not used at the moment of the call, it is immediately released. Otherwise, things get more complex. As mentioned above, ResourcePtr
instances keep their resources alive, so the resource is not released. However, the ResourceManager
will take care of releasing it ASAP – this happens the moment that the last ResourcePtr
loses ownership of the resource.
Const correctness [advanced]
It is often desirable that resources are not altered after they have been initialized. ResourcePtr
is powerful enough to imitate the const-correct behavior of raw pointers. That is to say, you can add const qualifiers, but not remove them.
thor::ResourcePtr<sf::Image> ptr; thor::ResourcePtr<const sf::Image> cptr; ptr = ptr; // ok cptr = cptr; // ok cptr = ptr; // ok, adding const-qualifier ptr = cptr; // not okay, removing const-qualifier
So when you use pointers to constant resources, you prevent accidental modification through ResourcePtr
. If you are very radical, you can even declare the template argument of ResourceManager
const! Like this, you have no possibility to change resources once they have been loaded.
thor::ResourceManager<const sf::Image> mgr; thor::ResourcePtr<const sf::Image> cptr = mgr.Acquire(key); // ok thor::ResourcePtr<sf::Image> ptr = mgr.Acquire(key); // not ok
Different management strategies [advanced]
The resource manager allows tweaking some behavior. This concerns the following actions, which concern Acquire()
calls in the future:
- When a resource cannot be acquired, throw exception or return null pointer? This can be configured via
SetLoadingFailureStrategy()
. - When the last resource pointer loses ownership of a resource, automatically release it or only release on explicit request? Use
SetReleaseStrategy()
to choose the reaction.
Custom resources [advanced]
Having your own resource class, the following steps are required to make it compatible with thor::ResourceManager
:
- Your resource class shall support RAII, but neither copyability nor default-constructibility is required.
- You need to define a key class that conforms the requirements in the documentation (second template parameter of
thor::ResourceManager
).
That’s it.