Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork967
Resource management
Resource management is usually one of the most critical parts of a game.Solutions are often tuned to the particular application. There exist severalapproaches and all of them are perfectly fine as long as they fit therequirements of the piece of software in which they are used.
Examples are loading everything on start, loading on request, predictiveloading, and so on.
EnTT
does not pretend to offer aone-fits-all solution for the differentcases.
Instead, the library comes with a minimal, general purpose resource cache thatmight be useful in many cases.
Resource, loader and cache are the three main actors for the purpose.
Theresource is an image, an audio, a video or any other type:
structmy_resource {constint value; };
Theloader is a callable type, the aim of which is to load a specificresource:
structmy_loaderfinal {using result_type = std::shared_ptr<my_resource>; result_typeoperator()(int value)const {// ...return std::make_shared<my_resource>(value); }};
Its function operator can accept any arguments and should return a value of thedeclared result type (std::shared_ptr<my_resource>
in the example).
A loader can also overload its function call operator to make it possible toconstruct the same or another resource from different lists of arguments.
Finally, a cache is a specialization of a class template tailored to a specificresource and (optionally) a loader:
using my_cache = entt::resource_cache<my_resource, my_loader>;// ...my_cache cache{};
The class is designed to create different caches for different resource typesand to manage each one independently in the most appropriate way.
As a (very) trivial example, audio tracks can survive in most of the scenes ofan application while meshes can be associated with a single scene only, thendiscarded when a player leaves it.
Resources are not returned directly to the caller. Instead, they are wrapped inaresource handle, an instance of theentt::resource
class template.
For those who know theflyweight design pattern already, that is exactly whatit is. To all others, this is the time to brush up on some notions instead.
A shared pointer could have been used as a resource handle. In fact, the defaultimplementation mostly maps the interface of its standard counterpart and onlyadds a few things on top of it.
However, the handle inEnTT
is designed as a standalone class template. Thisis due to the fact that specializing a class in the standard library is oftenundefined behavior while having the ability to specialize the handle for one,more or all resource types could help over time.
A loader is responsible forloading resources (quite obviously).
By default, it is just a callable object that forwards its arguments to theresource itself. That is, apassthrough type. All the work is demanded to theconstructor(s) of the resource itself.
Loaders also are fully customizable as expected.
A custom loader is a class with at least one function call operator and a membertype namedresult_type
.
The loader is not required to return a resource handle. As long asreturn_type
is suitable for constructing a handle, that is fine.
When using the default handle, it expects a resource type which is convertibleto or suitable for constructing anstd::shared_ptr<Type>
(whereType
is theactual resource type).
In other terms, the loader should return shared pointers to the given resourcetype. However, this is not mandatory. Users can easily get around thisconstraint by specializing both the handle and the loader.
A cache forwards all its arguments to the loader if required. This means thatloaders can also support tag dispatching to offer different loading policies:
structmy_loader {using result_type = std::shared_ptr<my_resource>;structfrom_disk_tag{};structfrom_network_tag{};template<typename Args> result_typeoperator()(from_disk_tag, Args&&... args) {// ...return std::make_shared<my_resource>(std::forward<Args>(args)...); }template<typename Args> result_typeoperator()(from_network_tag, Args&&... args) {// ...return std::make_shared<my_resource>(std::forward<Args>(args)...); }}
This makes the whole loading logic quite flexible and easy to extend over time.
The cache is the class that is asked toconnect the dots.
It loads the resources, stores them aside and returns handles as needed:
entt::resource_cache<my_resource, my_loader> cache{};
Under the hood, a cache is nothing more than a map where the key value has typeentt::id_type
while the mapped value is whatever type its loader returns.
For this reason, it offers most of the functionalities a user would expect froma map, such asempty
orsize
and so on. Similarly, it is an iterable typethat also supports indexing by resource id:
for(auto [id, res]: cache) {// ...}if(entt::resource<my_resource> res = cache["resource/id"_hs]; res) {// ...}
Please, refer to the inline documentation for all the details about the otherfunctions (such ascontains
orerase
).
Set aside the part of the API that this classshares with a map, it also addssomething on top of it in order to address the most common requirements of aresource cache.
In particular, it does not have anemplace
member function which is replacedbyload
andforce_load
instead (where the former loads a new resource onlyif not present while the second triggers a forced loading in any case):
auto ret = cache.load("resource/id"_hs);// true only if the resource was not already presentconstbool loaded = ret.second;// takes the resource handle pointed to by the returned iteratorentt::resource<my_resource> res = ret.first->second;
Note that the hashed string is used for convenience in the example above.
Resource identifiers are nothing more than integral values. Therefore, plainnumbers as well as non-class enum value are accepted.
It is worth mentioning that the iterators of a cache as well as its indexingoperators return resource handles rather than instances of the mapped type.
Since the cache has no control over the loader and a resource is not required toalso be convertible to bool, these handles can be invalid. This usually means anerror in the user logic, but it may also be anexpected event.
It is therefore recommended to verify handles validity with a check in debug(for example, when loading) or an appropriate logic in retail.