Resources Tutorial
Welcome to the tutorial about the Resources module of the Thor Library. Resources, sometimes called assets, are heavyweight objects that can be loaded from the application, such as images, fonts, sound files, etc. In SFML, there exist the following classes with typical resource semantics:
sf::Image
sf::Texture
sf::Font
sf::Shader
sf::SoundBuffer
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.
Typically, resources are accessed from different places in the application, requiring certain organization in terms of code. The Thor Resources module provides an interface that simplifies loading and access to resources. Besides SFML resources, Thor also allows you to integrate your own resource types.
Resource loader
As implied by the name, a resource loader knows how to load a resource. It offers a way of passing loading information around and can be invoked to actually load resources when needed.
A resource loader for an sf::Texture
which should be loaded from a file “image.jpg” is declared like this:
thor::ResourceLoader<sf::Texture> key = thor::Resources::fromFile<sf::Texture>("image.jpg");
The functionfromFile()
indirects to the loading function sf::Texture::loadFromFile()
and has the same signature. Thor provides a variety of predefined loaders that correspond to their SFML counterparts.
Resource holder
The class template thor::ResourceHolder
is the core of the resource management system. It provides a central point to load, access and release resources within your application. The class is based on resource IDs: identifiers that represent names for your resources. Such identifiers can be strings, enums or any types that can act as keys in associative maps. Whenever you access resources, you do so through identifiers.
The template parameters define the resource type and the ID type, respectively. For example, the following defines a resource holder for texture resources and string IDs:
thor::ResourceHolder<sf::Texture, std::string> holder;
Resources can be loaded through the acquire()
function. The first parameter is the ID, which you want to associate with the resource. The second parameter is the resource loader, which determines how the resource is loaded. In the next example, the texture is loaded from a file called “image.jpg”. It is stored using the identifier “myID”.
holder.acquire("myID", thor::Resources::fromFile<sf::Texture>("image.jpg"));
Once the resource is loaded, it can be accessed using the ID. You can either use acquire()
directly, or operator[]
at a later time. Both return a reference to the resource.
sf::Texture& texture = holder.acquire("myID", ...); sf::Texture& texture = holder["myID"];
If you don't need the resource anymore, you can destroy it with the release()
method, the counterpart to acquire()
. If you don't release resources manually, they will be released in the ResourceHolder
destructor. Make sure that a resource is no longer used after it is released.
holder.release("myID");
Error handling
All kinds of errors related to ResourceHolder
are signaled through exceptions. The primary error source is loading: a file may not exist, data can be corrupt, a network connection breaks down, etc. When acquire()
fails to load a resource, it throws a ResourceLoadingException
which can be handled at runtime.
thor::ResourceHolder<...> holder; try { holder.acquire("firstID", ...); holder.acquire("secondID", ...); ... } catch (thor::ResourceLoadingException& e) { std::cout << "Error: " << e.what() << std::endl; }
This allows you to load many resources without checking every single call to acquire()
. You can even have your try-catch block far from the place where resources are loaded; this is often advisable because the local code doesn't know how to deal with such errors and has to propagate them.
All the methods that access a resource through an ID fail if that ID is invalid. Invalid means the ResourceHolder
stores no resource corresponding to that ID. This can occur if you simply mistype, or if the requested resource has not been loaded, or if it has been released again. In such cases, a different exception is thrown: ResourceAccessException
. Exceptions of this type should occur far less frequently. Usually, their occurrence hints a logic error (bug) in your program — a correctly written application should know which resources it loads and is able to access.
Ownership models
The ResourceHolder
facility aims to be reusable in a variety of different scenarios. The pattern, where resources are stored centrally and accessed by many clients through references, will be referred to as centralized ownership model (accessible through thor::Resources::CentralOwner
). It is very simple and very efficient. The lifetime of resources is tied to that of their owning resource holder. Once the latter is destroyed, resources suffer the same fate. Even with this simple model, it is possible to build fairly complex scenarios by using multiple ResourceHolder
instances with different scopes. Imagine a game that has several levels: there are resources that need to be available more or less all the time, such as textures or sounds needed to represent the player and his actions. Others are specific to a level and can be loaded once a level is entered and unloaded when it is left. A level class could simply keep a ResourceHolder
as a member and load the resources in its constructor, and RAII would take care of releasing them automatically in the level's destructor.
Now imagine a different scenario: you are writing a GUI framework with loads of different graphical themes and fonts. If two buttons use the same font, you don't want that font to be loaded twice. And of course, you don't want to load a font if it is never used in any button. Similarly, to keep resource allocation low, you would like to release fonts as soon as they are no longer used by any button. In short: keep every resource around only once and exactly as long as it's used. To implement this manually, you would have to track resources, count how often they are referenced, and check the reference counter every time a button is destroyed.
Thor does all this for you. It addresses the above-mentioned scenario by providing a new ownership model, the reference-counted ownership (thor::Resources::RefCounted
). What this means is that ResourceHolder
is now merely a manager, but no longer the owner of resources. Instead, the clients share ownership among each other. In this model, resources are no longer handed out as references (R&
), but as shared pointers (std::shared_ptr<R>
objects), which fit this scenario perfectly.
A third template parameter is used to enable this ownership model:
namespace res = thor::Resources;
thor::ResourceHolder<sf::Font, std::string, res::RefCounted> holder;
Now, we can work with the resource holder as usual, just that we deal with shared pointers instead of references. Keep in mind that you must store a shared pointer directly at the acquire()
call — because by definition, the resource is only kept alive as long as it is used, and if it is not used directly after loading, then it will be released immediately. The following example shows three users of the font, all of which refer to the same resource.
std::shared_ptr<sf::Font> user1 = holder.acquire("Fancy", res::fromFile<sf::Font>("MyFancyFont.ttf")); std::shared_ptr<sf::Font> user2 = user1; // Share from existing pointer std::shared_ptr<sf::Font> user3 = holder["Fancy"]; // Access directly through ResourceHolder
Now each std::shared_ptr
instance can be stored in a Button
class. This keeps the font alive, as long as at least one button refers to it. When all buttons are destroyed, so is the shared font. Note that with reference-counted ownership, ResourceHolder::release()
does not really release the resource. It only removes it from the resource holder, so that the ID can be reused. There is usually no reason to call release()
here, as the shared pointers take care of releasing the resource automatically.
Customizing resource loading
By default, attempts to assign a resource ID more than once are prevented. That is, when you call ResourceHolder::acquire()
with an ID that you have used in the same resource holder before (i.e. the ID is known), you'll have to fight with ResourceAccessException
s. This behavior is embodied as AssumeNew
, because it assumes every resource being loaded is new. AssumeNew
is one of several enumerators in an enum thor::Resources::KnownIdStrategy
.
Now imagine the case where you want to load a resource, and when the requested resource has already been loaded, you just want that resource back. You don't care whether the ID has already been assigned to the same resource before. This is possible by using the Reuse
strategy, which is particularly useful if you want to keep loading from different places symmetric. In the previous example with the buttons and fonts, you typically don't know which button is the first to request the resource, and you don't particularly care either.
thor::Resources::Reuse
as a third argument to acquire()
. In the following example, the second call recognizes that a font with ID “Fancy” has been requested before, and just returns that one instead of loading it again. The result is exactly the same as if you copied the shared_ptr
or accessed the resource through ResourceHolder::operator[]
.
std::shared_ptr<sf::Font> user1 = holder.acquire("Fancy", res::fromFile<sf::Font>("MyFancyFont.ttf"), res::Reuse); std::shared_ptr<sf::Font> user2 = holder.acquire("Fancy", res::fromFile<sf::Font>("MyFancyFont.ttf"), res::Reuse);
There are other cases, where you simply want to assign an ID to a new resource, possibly overwriting previous assignments. The thor::Resources::Reload
strategy can be used to enforce a loading of the resource, even if the ID is already there.
Const correctness [advanced]
It is often desirable that resources are not altered after they have been initialized. ResourceHolder
propagates its const-qualification: a const-qualified holder only returns handles (references or shared pointers), through which the resource cannot be modified.
ResourceHolder<R, ...> holder; const ResourceHolder<R, ...>& cref = holder; R& a = holder["myID"]; // ok const R& b = holder["myID"]; // ok R& c = cref["myID"]; // forbidden (would remove const) const R& d = cref["myID"]; // ok
Thus, it is recommended that clients, who access resources only to use them passively, are given const-references to resource holders. Like this, you can ensure that resources are only loaded, unloaded and possibly changed in one central place.
Custom resources [advanced]
Having your own resource class, the following steps are required to make it compatible with Thor:
- Your resource class shall support RAII, but neither copyability nor default-constructibility is required.
- You need to provide resource keys that are able to load the resource.
A short example shows how to integrate an existing class called Mesh
to Thor. First, let's see what's given by the external mesh API:
// Custom resource class class Mesh {}; // Factory function returning a pointer to the resource Mesh* createMesh(const char* filename);
On user side, we now implement a functor that is able to load such a mesh. Thor requires this functor to return a std::unique_ptr
to the resource, which shall be nullptr
in case of loading failure.
// Functor to load mesh struct MeshLoader { MeshLoader(const char* filename) : filename(filename) { } std::unique_ptr<Mesh> operator() () const { return std::unique_ptr<Mesh>(createMesh(filename)); } const char* filename; };
Now we are able to create a Thor resource loader from the loading functor and a string ID which is unique for the same resource (here the filename):
thor::ResourceLoader<Mesh> loadMesh(const char* filename) { return thor::ResourceLoader<Mesh>(MeshLoader(filename), filename); }
Afterwards, we can directly use Mesh
with Thor:
int main() { thor::ResourceHolder<Mesh, std::string> holder; holder.acquire("myID", loadMesh("mesh.obj")); }
Note that if we use lambda expressions, we don't need the functor, so the code to integrate the Mesh
class is very short:
thor::ResourceLoader<Mesh> loadMesh(const char* filename) { return thor::ResourceLoader<Mesh>( [=] () { return std::unique_ptr<Mesh>(createMesh(filename)); }, filename); }