资源管理
目录
简介
资源管理通常是游戏开发中最关键的部分之一。解决方案通常针对特定应用程序进行调优。存在多种方法,只要它们符合所使用软件的需求,所有这些方法都是完全可行的。
示例包括启动时加载所有内容、按需加载、预取加载等。
EnTT 并不试图为不同情况提供 万能 解决方案。
相反,该库提供了一个最小化的、通用的 resource cache,在许多情况下可能很有用。
Resource、Loader 与 Cache
Resource、loader 和 cache 是为此目的的三个主要参与者。
Resource 是一张图像、一段音频、一段视频或任何其他类型:
struct my_resource { const int value; };
Loader 是一种可调用类型,其目的是加载特定的 resource:
struct my_loader final {
using result_type = std::shared_ptr<my_resource>;
result_type operator()(int value) const {
// ...
return std::make_shared<my_resource>(value);
}
};
其函数调用运算符可以接受任何参数,并应返回声明的 result_type 类型的值(示例中为 std::shared_ptr<my_resource>)。
Loader 还可以重载其函数调用运算符,以便能够根据不同的参数列表构造相同或另一个 resource。
最后,cache 是一个针对特定 resource 和(可选的)loader 定制的类模板特化:
using my_cache = entt::resource_cache<my_resource, my_loader>;
// ...
my_cache cache{};
该类旨在为不同类型的 resource 创建不同的 cache,并以最合适的方式独立管理每一个。
作为一个(非常)简单的示例,音频轨道可以在应用程序的大多数场景中存活,而网格 (meshes) 可能仅与单个场景关联,然后在玩家离开时被丢弃。
Resource Handle
Resource 不会直接返回给调用者。相反,它们被包装在 resource handle 中,即 entt::resource 类模板的实例。
对于已经了解 享元设计模式 (flyweight design pattern) 的人来说,这正是它所实现的。对于其他人来说,现在是复习一些概念的时候了。
本可以使用 shared pointer 作为 resource handle。事实上,默认实现主要映射了其标准对应物的接口,仅在其之上添加了一些内容。
然而,EnTT 中的 handle 被设计为一个独立的类模板。这是由于在标准库中特化类通常是未定义行为,而能够为一个、多个或所有 resource 类型特化 handle 可能随着时间的推移带来帮助。
Loaders
Loader 负责 加载 resource(相当明显)。
默认情况下,它只是一个可调用对象,将其参数转发给 resource 本身。也就是说,一个 直通类型 (passthrough type)。所有工作都交由 resource 本身的构造函数完成。
正如预期的那样,loader 也是完全可定制的。
自定义 loader 是一个至少具有一个函数调用运算符和名为 result_type 的成员类型的类。
Loader 不需要返回 resource handle。只要 return_type 适合构造 handle,那就没问题。
当使用默认 handle 时,它期望一个 resource 类型,该类型可转换为或适合构造 std::shared_ptr<Type>(其中 Type 是实际的 resource 类型)。
换句话说,loader 应返回指向给定 resource 类型的 shared pointer。然而,这不是强制性的。用户可以通过特化 handle 和 loader 轻松绕过此约束。
如果需要,cache 会将其所有参数转发给 loader。这意味着 loader 也可以支持标签分发 (tag dispatching) 以提供不同的加载策略:
struct my_loader {
using result_type = std::shared_ptr<my_resource>;
struct from_disk_tag{};
struct from_network_tag{};
template<typename... Args>
result_type operator()(from_disk_tag, Args&&... args) {
// ...
return std::make_shared<my_resource>(std::forward<Args>(args)...);
}
template<typename... Args>
result_type operator()(from_network_tag, Args&&... args) {
// ...
return std::make_shared<my_resource>(std::forward<Args>(args)...);
}
}
这使得整个加载逻辑相当灵活,并且易于随时间扩展。
Cache 类
Cache 是被要求 连接各个点 的类。
它加载 resource,将它们存储在一旁,并在需要时返回 handle:
entt::resource_cache<my_resource, my_loader> cache{};
在底层,cache 不过是一个映射 (map),其中键值类型为 entt::id_type,而映射值是 loader 返回的任何类型。
因此,它提供了用户期望从 map 获得的大部分功能,例如 empty 或 size 等。同样,它是一个可迭代类型,也支持按 resource id 索引:
for(auto [id, res]: cache) {
// ...
}
if(entt::resource<my_resource> res = cache["resource/id"_hs]; res) {
// ...
}
有关其他函数(如 contains 或 erase)的所有详细信息,请参阅内联文档。
除了此类与 map 共享 的那部分 API 之外,它还在此基础上添加了一些内容,以满足 resource cache 最常见的要求。
特别是,它没有 emplace 成员函数,而是用 load 和 force_load 取而代之(前者仅在 resource 不存在时加载新 resource,而后者在任何情况下都会触发强制加载):
auto ret = cache.load("resource/id"_hs);
// 仅当 resource 之前不存在时为 true
const bool loaded = ret.second;
// 获取返回的迭代器指向的 resource handle
entt::resource<my_resource> res = ret.first->second;
请注意,在上面的示例中,hashed string 是为了方便而使用的。
Resource 标识符不过是整数值。因此,普通数字以及非类 enum 值都被接受。
值得一提的是,cache 的迭代器及其索引运算符返回的是 resource handle,而不是映射类型的实例。
由于 cache 无法控制 loader,并且 resource 也不要求可转换为 bool,因此这些 handle 可能无效。这通常意味着用户逻辑中的错误,但也可能是 预期的 事件。
因此,建议在 debug 中(例如加载时)通过检查验证 handle 的有效性,或在零售版本中使用适当的逻辑。