虽然 EnTT 主要利用编译期功能来提升性能, 这是其核心优势之一, 然而一些运行时技术也是相当有用的, 例如 类型擦除, EnTT 提供了一系列实用工具和特性, 能够非常高效地在运行时处理类型和池
基类 base class
在 EnTT 中, 存储类 Storage classes 是完全自包含的类, 它们通过 混合 mixin 来进行扩展, 以添加更多功能 (无论是通用功能还是特定于类型的功能), 此外, 存储类提供了一组基础函数, 已经足够让用户实现许多复杂的需求
在 ECS 中, 实体 是一个抽象的标识符, 本身并不携带数据, 组件 是实际的数据信息, 它们附加到实体上, 使实体 具备特定的属性或行为, 例如, 一个 实体 可以附加一个 velocity 组件来表示速度
type_info 对象的作用
在编译期, 我们可以通过模板知道组件的具体类型, 比如 registry.get<velocity>(entity) 但在运行时, 如果不清楚组件的实际类型 (可能是通过某种动态查询获取), 如何检查某个 实体 是否附加了某种类型的 组件 ?
EnTT 的 type_info 为了解决这个问题而设计, 提供了一种对 类型 的运行时描述, 它允许我们在运行时检查组件的类型, 而无需直接依赖模板或编译期信息
通过 entt::type_id<T>(), 我们可以获取组件类型 T 的 type_info 对象, 然后, 可以使用该对象与存储类的 type() 方法返回的 type_info 进行比较
if (entt::type_id<velocity>() == base.type())
{
// base 是存储类的基类,type() 返回其管理的组件类型。
// 如果 base 管理的是 velocity 组件,我们就可以安全地处理相关逻辑。
std::cout << "This storage manages velocity components!" << std::endl;
}
当通过基类使用存储时, 即使存储的实际类型未知, 仍然可以获取与该存储类相关联的 type_info 对象 (如果有的话)
此外, base.bind(registry) 是一种 绑定机制, 用于将 存储类 storage 与 注册表 registry 关联起来, 从而实现更高级的功能, 例如事件通知、信号处理或依赖注入, 这是通过 bind 方法实现的, 它在存储类内部保存对注册表的引用, 从而在存储类内部可以访问注册表的功能, 而无需每次都显式的传递注册表
#include <entt/entt.hpp>
#include <iostream>
// 示例组件
struct Velocity {
float x, y;
};
int main() {
entt::registry registry;
// 创建组件的存储
auto &storage = registry.storage<Velocity>();
// 绑定注册表
storage.bind(registry);
// 现在存储类可以与注册表交互
auto entity = registry.create();
storage.emplace(entity, 1.0f, 2.0f);
std::cout << "Entity " << int(entity) << " has Velocity ("
<< storage.get(entity).x << ", "
<< storage.get(entity).y << ")" << std::endl;
return 0;
}默认情况下直接使用 registry.storage<T>() 获取的存储类不会自动触发信号或事件, 也就是说, 存储和注册表是解耦的
当调用 base.bind(registry) 后:
- 存储类会接收注册表的引用
- 存储类中的混入逻辑可以动态触发注册表的事件系统
- 存储无需显式传递注册表, 就可以发送事件
从注册表中获取的存储只是管理了组件的存储和生命周期, 它默认并不会触发事件或访问注册表的上下文信息
运行时处理
在 EnTT 中, base.value(entity) 和 base.push(entity, pointer) 是底层存储类提供的高级功能, 允许以 类型无关 的方式处理 实体 及其 组件, 这种机制非常适合在运行时动态管理组件, 尤其是在 实现实体克隆 或 动态组件操作 的场景下
value(entity)
可以从存储中获取与实体相关联的组件, 并以 void* 的形式 (不透明指针) 返回, 这可以在不知道组件类型的情况下, 通过 value 提取组件值
const void *instance = base.value(entity);base 是一个存储类的基类引用, value(entity) 返回实体 entity 对应组件 (或者说相关联的,存储在 base 中的值, 如果存在的话)
push(entity, void *pointer = nullptr)
这会将 组件 绑定到 实体, 并根据传入指针选择默认 构造 或 复制 构造组件
base.push(entity, instance); // 通过复制构造为 `entity` 绑定组件。
base.push(entity); // 通过默认构造为 `entity` 绑定组件。- 当指针为
nullptr时: 尝试默认构造组件并绑定到实体 - 当指针非空时: 尝试通过复制构造的方式, 将指针指向的值作为新组件绑定到实体
克隆
通过 value 和 push 的组合, 可以在不依赖组件实际类型的情况下, 实现实体及其组件的克隆
以下代码展示如何将一个实体 src 的所有组件克隆到另一个实体 dst
for (auto &&curr : registry.storage())
{
auto &storage = curr.second; // 获取存储引用
if (storage.contains(src)) { // 检查 `src` 是否有该类型 (storage 存储的类型) 的组件
storage.push(dst, storage.value(src)); // 将组件从 `src` 复制到 `dst`
}
}存储管理
EnTT 提供了一个非常灵活的机制, 允许用户为组件类型创建多个 存储, 并通过唯一的 标识符 (通常是一个 entt 编译时哈希字符串) 来管理它们
默认存储与命名存储
- 默认存储: 每种组件类型在注册表中会有一个默认的存储
- 命名存储: 可以通过提供标识符 (如字符串哈希) 为同一组件类型创建额外的存储
using namespace entt::literals;
// 获取默认存储
auto &&defaultStorage = registry.storage<velocity>();
// 创建一个命名存储
auto &&secondPool = registry.storage<velocity>("second pool"_hs);
auto &&otherPool = registry.storage<velocity>("other"_hs);如果未提供标识符, 则会始终返回默认存储, 如果提供标识符, 则会创建或返回对应的命名存储
存储的独立性
所有存储都是 自包含 self-contained, 它们独立管理与实体关联的数据, 这种设计允许
- 直接通过存储操作组件, 而不必依赖注册表的 API
- 为同一组件类型创建多个存储, 满足不同场景的需求
registry.emplace<velocity>(entity); // 在默认存储中添加组件
secondPool.push(entity); // 在名为 "second pool" 的存储中添加组件
// 访问默认存储中的组件
if (registry.all_of<velocity>(entity)) {
auto &vel = registry.get<velocity>(entity);
std::cout << "Default velocity: (" << vel.dx << ", " << vel.dy << ")\n";
}
// 访问名为 "second pool" 的存储中的组件
if (secondPool.contains(entity)) {
auto &vel = secondPool.get(entity);
std::cout << "Second pool velocity: (" << vel.dx << ", " << vel.dy << ")\n";
}注册表操作对所有存储生效
虽然存储是独立的, 但某些注册表操作 (如销毁实体) 会影响所有关联的存储, 这意味着即使组件存储是手动创建的, 仍然能够与注册表的核心逻辑保持一致
registry.destroy(entity); // 从所有存储中移除该实体的数据视图支持
EnTT 的 视图不仅可以处理默认存储, 还支持多个同类型的存储, 这种功能使得在运行时动态组合不同存储的数据成为可能
entt::basic_view direct{
registry.storage<velocity>(), // 默认存储
registry.storage<velocity>("other"_hs) // 名为 "other" 的存储
};
// 迭代视图中的实体
for (auto entity : direct) {
std::cout << "Entity in multiple storages: " << int(entity) << std::endl;
}或者
auto join = registry.view<velocity>() | entt::basic_view{registry.storage<velocity>("other"_hs)};
for (auto entity : join)
{
std::cout << "Entity in combined view: " << int(entity) << std::endl;
}