EnTT: 资源管理 resource management
整理 EnTT resource、resource handle、loader、resource cache 和不同类型资源的管理方式。
EnTT 的资源管理系统提供了一种轻量级的通用缓存方案, 以针对特定应用程序进行调整, 例如, 启动时加载所有内容、请求时加载、预测加载等等
资源 Resource
资源可以是游戏中的 图片、音频、视频 或任何其他类型的数据, 例如, 以下是一个简单的资源结构
struct my_resource {
const int value;
};这代表一个仅包含整数值的资源类型
资源句柄 Resource Handle
在 EnTT 的资源管理系统中, 资源不会直接返回给调用者, 而是被封装在资源句柄 entt::resource 中, 这种设计模式与 Flyweight 享元模式 类似, 提供了一种高效的资源管理方法
EnTT 选择 entt::resource<T> 作为资源句柄, 而不是直接返回 std::shared_ptr<T>, 主要基于以下几个原因:
-
避免标准库
shared_ptr限制
C++ 标准库的类无法被特化 (specialization 通常是 未定义行为*), EnTT 允许resource<T>针对特定资源类型进行特化, 而std::shared_ptr<T>无法做到这一点 -
提供额外功能
entt::resource<T>继承了std::shared_ptr<T>的大部分功能, 并在其基础上扩展了一些特性, 使资源管理更加灵活 -
增强管理能力
资源句柄允许更细粒度的控制, 例如自动缓存清理、弱引用支持等
加载器 Loader
加载器是一个可调用对象, 负责从外部输入创建资源
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);
}
};operator() 允许使用任意参数加载资源, 并返回 std::shared_ptr<my_resource> 类型的资源对象, 可以重载 operator() 以支持不同参数列表构造资源
EnTT 允许加载器直接将参数转发给资源构造函数, 用户可以完全控制加载逻辑, 实现不同的资源加载策略, 加载器通常返回 std::shared_ptr<ResourceType>, 但这并非强制要求, 只要返回值可以用于构造 resource<T> 句柄即可
EnTT 支持 标签派发 Tag Dispatching, 使得加载器可以根据不同的 标记 tag 采用不同的加载逻辑
// 示例
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)...);
}
}
// 实例
struct my_loader {
using result_type = std::shared_ptr<my_resource>;
struct from_disk_tag {};
struct from_network_tag {};
result_type operator()(from_disk_tag, const std::string& path) {
std::cout << "Loading resource from disk: " << path << std::endl;
return std::make_shared<my_resource>(42); // 假设从磁盘加载
}
result_type operator()(from_network_tag, const std::string& url) {
std::cout << "Downloading resource from: " << url << std::endl;
return std::make_shared<my_resource>(99); // 假设从网络加载
}
};使用示例
int main() {
using my_cache = entt::resource_cache<my_resource, my_loader>;
my_cache cache;
my_loader::from_disk_tag disk_tag;
my_loader::from_network_tag network_tag;
// 加载资源
auto handle1 = cache.load(1, disk_tag, "resource.dat");
auto handle2 = cache.load(2, network_tag, "http://example.com/resource");
return 0;
}资源缓存 Cache
资源缓存负责管理资源的存储与访问, EnTT 提供了 entt::resource_cache<T, Loader> 类模板负责 加载资源、存储资源, 并根据需要返回 资源句柄
using my_cache = entt::resource_cache<my_resource, my_loader>;
// 创建缓存对象
my_cache cache{};资源缓存的作用
- 管理同种类型不同 ID 的资源 (如音频、模型、纹理)
- 控制资源生命周期 (决定何时加载、释放)
- 提高性能 (避免重复加载相同资源)
缓存的内部实现本质上是一个 哈希映射 map, 因此, 它提供了用户期望从 映射 中获得的大多数功能, 例如 empty 或 size 等等
- Key:
entt::id_type(资源唯一 ID) - Value: 由
Loader返回的对象 (通常是std::shared_ptr<T>)
entt::resource_cache<my_resource, my_loader> cache{};my_resource是资源类型, 例如纹理、音效等my_loader是加载器, 负责创建my_resource实例
缓存类 支持迭代和索引, 类似于 标准映射 map
for(auto [id, res]: cache) {
std::cout << "Resource ID: " << id << " Value: " << res->value << std::endl;
}id是entt::id_type(资源标识符)res是entt::resource<T>(资源句柄)
可以使用 哈希字符串 访问资源
if(entt::resource<my_resource> res = cache["resource/id"_hs]; res) {
std::cout << "Resource loaded: " << res->value << std::endl;
}_hs是 字符串哈希标记, 用于生成entt::id_typeres是 资源句柄, 需要检查是否有效 (if(res))
资源加载
缓存类 没有 emplace 方法, 而是提供:
load(id, args...): 仅在资源未加载时才加载force_load(id, args...): 无论是否存在, 都强制加载
使用 load()
auto ret = cache.load("resource/id"_hs, 42); // 加载 ID 为 "resource/id" 的资源
// 是否是新加载的资源
const bool loaded = ret.second;
// 获取资源句柄
entt::resource<my_resource> res = ret.first->second;ret.first是std::pair<iterator, bool>, 其中iterator->second是 资源句柄ret.second表示资源是否 新创建 (true) 还是 已存在 (false)
使用 force_load()
cache.force_load("resource/id"_hs, 99); // 强制重新加载资源无论资源是否存在, 都会重新加载
资源删除
缓存支持手动移除资源
cache.discard("resource/id"_hs); // 删除 ID 为 "resource/id" 的资源资源被 discard 后, 所有 entt::resource<T> 句柄仍然可以访问已加载的资源, 但缓存不再持有它
资源句柄的有效性
entt::resource<T> 可能会变成 无效句柄 invalid handle, 主要有以下情况:
- 资源从缓存中
discard但仍有句柄存在 - 资源
Loader可能返回 空指针nullptr - 用户代码逻辑错误
检查句柄是否有效
entt::resource<my_resource> res = cache.handle("resource/id"_hs);
if (res) {
std::cout << "Resource valid: " << res->value << std::endl;
} else {
std::cerr << "Invalid resource handle!" << std::endl;
}由于 缓存 无法控制 加载器, 并且资源也不一定需要可以转换为 布尔值, 因此这些句柄可能无效, 这通常意味着用户逻辑中存在错误, 但也可能是预期事件
不同类型的资源
entt::resource_cache<T, Loader> 仅管理 同一种类型 (T) 但不同 ID 的资源 如果需要管理 不同类型 的资源, 则需要 多个 entt::resource_cache, 分别对应不同的资源类型
using TextureCache = entt::resource_cache<Texture, TextureLoader>;
using AudioCache = entt::resource_cache<Audio, AudioLoader>;
TextureCache textureCache;
AudioCache audioCache;总结例子
int main() {
my_cache cache;
// 加载资源
auto handle = cache.load(1, 42); // 使用 my_loader 加载资源
std::cout << "Resource value: " << handle->value << std::endl;
// 获取已加载的资源
auto found = cache.handle(1);
if (found) {
std::cout << "Found cached resource: " << found->value << std::endl;
}
// 移除资源
cache.discard(1);
return 0;
}