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>, 主要基于以下几个原因:

  1. 避免标准库 shared_ptr 限制
    C++ 标准库的类无法被特化 (specialization 通常是 未定义行为*), EnTT 允许 resource<T> 针对特定资源类型进行特化, 而 std::shared_ptr<T> 无法做到这一点

  2. 提供额外功能
    entt::resource<T> 继承了 std::shared_ptr<T> 的大部分功能, 并在其基础上扩展了一些特性, 使资源管理更加灵活

  3. 增强管理能力
    资源句柄允许更细粒度的控制, 例如自动缓存清理、弱引用支持等

加载器 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{};

资源缓存的作用

  1. 管理同种类型不同 ID 的资源 (如音频、模型、纹理)
  2. 控制资源生命周期 (决定何时加载、释放)
  3. 提高性能 (避免重复加载相同资源)

缓存的内部实现本质上是一个 哈希映射 map, 因此, 它提供了用户期望从 映射 中获得的大多数功能, 例如 emptysize 等等

  • 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;
}
  • identt::id_type (资源标识符)
  • resentt::resource<T> (资源句柄)

可以使用 哈希字符串 访问资源

if(entt::resource<my_resource> res = cache["resource/id"_hs]; res) {
    std::cout << "Resource loaded: " << res->value << std::endl;
}
  • _hs字符串哈希标记, 用于生成 entt::id_type
  • res资源句柄, 需要检查是否有效 (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.firststd::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, 主要有以下情况:

  1. 资源从缓存中 discard 但仍有句柄存在
  2. 资源 Loader 可能返回 空指针 nullptr
  3. 用户代码逻辑错误

检查句柄是否有效

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;
}