混合 mixin
mixin 是面向对象编程中常用的设计模式, 其本质是一个 “可插入模块”, 通过组合的方式, 将特定功能 “混入” 到一个类中, 而无需直接继承或重写原类, 也就是说在不改变原类的核心逻辑的情况下为其增加新功能
EnTT 中每个组件的存储默认带有一个 mixin, 用于支持 信号 机制, 这使得用户可以实现 依赖监听 和 反应式系统 reactive systems
组件信号监听
信号 机制允许对组件的构造、销毁等事件进行监听,从而支持基于依赖关系的逻辑设计
on_construct 方法可用于监听某组件类型的构造事件, 它返回一个 sink 对象, 用于管理监听器的连接和断开
// connects a free function
registry.on_construct<position>().connect<&my_free_function>();
// connects a member function
registry.on_construct<position>().connect<&my_class::member>(instance);
// disconnects a free function
registry.on_construct<position>().disconnect<&my_free_function>();
// disconnects a member function
registry.on_construct<position>().disconnect<&my_class::member>(instance);与之类似, on_destroy 和 on_update 可用于绑定组件实例销毁和更新的监听器, 因为 C++ 的运行机制, 被连接到 on_update 的监听器只会在调用 replace, emplace_or_replace 或 patch 之后被调用
监听器
监听器的函数签名被定义为
void(entt::registry &, entt::entity);这意味着任何作为监听器的函数都必须符合以下两个参数
entt::registry &
提供对注册表的引用, 允许访问和操作组件或实体, 监听器可以通过这个引用与其他组件交互或查询实体状态entt::entity
提供触发事件的实体标识符, 监听器可以使用此标识符获取或修改与实体关联的组件
监听标识符
默认情况下, 信号监听是全局的, 一个组件类型的事件会触发所有绑定的监听器, 通过为信号监听指定一个标识符, 可以实现更细粒度的事件监听和管理, 使得可以区分同类型组件的信号, 例如, 通过标识符可以为不同场景或逻辑分离组件的事件监听
// 使用标识符可以将事件处理逻辑隔离
registry.on_construct<position>("main"_hs).connect<&main_handler>();
registry.on_construct<position>("other"_hs).connect<&other_handler>();
// 标识符监听器可以单独断开,不影响其他标识符的监听器
registry.on_construct<position>("other"_hs).disconnect<&my_free_function>();注意事项
监听器 为组件的构造, 更新和销毁提供了灵活的事件处理机制, 然而, 为了避免潜在问题, 使用监听器时需要注意事件触发的 时机 和可能的 限制
事件触发时机
- 构造事件: 在组件被 创建后 触发, 此时, 组件已经完全初始化并关联到实体
- 更新事件: 在组件被 更新后 触发, 此时, 组件的状态已经被修改, 可以用于执行额外的逻辑
- 销毁事件: 在组件被 销毁前 触发, 此时, 组件仍然可用, 适合用于清理工作
限制
为了避免 未定义行为 UB, 以下规则必须遵守
- 避免在监听器内 连接 或 断开 其他监听器
- 在 构造 或 更新 事件的监听器中, 不允许移除相关组件
- 在 销毁 事件的监听器中, 避免增加或移除组件
销毁事件监听器的主要目的应当是清理, 而不是做别的
实体生命周期 Entity Lifecycle
除了对 组件 进行监听之外, 也可以对 实体 本身进行监听, 同样支持构造, 销毁, 更新信号, 并且 监听器 的函数签名也是相同的
监听器标识符
在对 实体 的行为监听中, 监听器标识符 不受支持, 无法通过类似 组件 监听的方式为 实体 信号指定不同的 “命名空间” 或分组逻辑, 如果提供了, 也会被忽略
// 标识符将被忽略
registry.on_construct<entt::entity>("example"_hs).connect<&entity_handler>();
// 等价于下面的代码
registry.on_construct<entt::entity>().connect<&entity_handler>();更新监听
实体的更新事件的监听和组件的不太一样, 因为实体并不会被真正的 更新, 实体只是被创建并最终被释放, 所以 更新事件 在这里用于发布有关某个实体的 通知, 它可以通过 patch 方法手动触发, 不会被自动触发
registry.on_update<entt::entity>().connect([](entt::registry ®, entt::entity entity) {
std::cout << "Entity " << static_cast<int>(entity) << " updated.\n";
});
// 触发更新信号
registry.patch<entt::entity>(entity);销毁监听
实体 销毁事件 的监听也需要特别注意, 实体销毁信号是在移除所有组件 之后 触发的
监听器的断开
监听器 的断开与 存储类 的销毁顺序相关, 而存储类的销毁顺序是随机的, 没有明确的保证, 这种随机性可能导致一些潜在问题, 因此需要特别注意资源清理和监听器的安全断开
在销毁 注册表 时, 其管理的存储类会被逐一销毁, 但销毁的顺序没有保证, 完全随机, 如果某个监听器试图访问已被销毁的存储类 (如组件池或实体池), 可能导致未定义行为
为了避免这些问题, 建议在销毁注册表之前明确清理组件和实体, 使用 clear 方法清理所有组件和实体, 这将强制删除所有组件和实体, 而不销毁存储类, 可以保证存储类仍然有效, 监听器可以安全地访问相关资源, 如此一来, 存储池 在完全销毁之前仍然存在, 监听器 可以根据需要对访问的对象进行有效性检查
registry.clear(); // 清理所有组件和实体
// 确保监听器在访问组件、实体或存储池之前,进行有效性检查
if (registry.valid(entity) && registry.all_of<position>(entity)) {
auto &pos = registry.get<position>(entity);
std::cout << "Position: (" << pos.x << ", " << pos.y << ")\n";
}