过了好一阵子,私以为对 ECS 有了些许了解,在我看来 ECS 是一种 DOD(面向数据设计)的实现方法。而跟诸多数据库一样,entt 提供的是一种紧凑组织、高效遍历的数据框架,并加上各种模板元使代码组织简洁美观。
本文会讨论内存组织方式与遍历的方法。
registry 成员
1. pools
dense_map<id_type, std::unique_ptr<base_type>, identity>
其中的 id_type
来自 type_hash<Component>::value()
,所有 Component 唯一。
通过模板泪将 Type 传入后,
PRETTY_FUNCTION
也会随之变化。使用 constexpr 就可以静态地算出一个 Type 的 hash。其本质是函数签名字符串的hash。
2. assure<>()
该函数会通过传入的 Comp,在 pools
中创建对应的 component 的专属内存块,并且要求是退化后的类型 (std::decay
)。
内存块类型为 sigh_storage_mixin<basic_storage<...>>
,别名 storage_type<Component>
。只需关注 basic_storage
类。后续有空会额外写一篇文章讨论稀疏集(sparse set)与storage之间的映射关系。
basic_view
本质上是一个 Component 池访问器,用于遍历拥有给定组件集合的实体。
遍历期间如下操作不会使遍历失效:
- 使用遍历中的 Component 新增实体。
- 修改迭代器指向的 Entity(比如移除 Entity 中的 正在被遍历的 Components)。
- 销毁遍历中的 Entity。
除此之外对 pools 的改动,均将使遍历过程失效,并且发生空悬等 Undefined Behavior。
构造过程
由 entt::registry::view
生成一个 basic_view 对象。其成员只有三个对象:
1 | // 需要遍历的pool引用 |
构造函数参数为若干个 pool,使用模板元来区分 Excluded Component。view 为需遍历Comp中,entity数量最小的 pool 引用。
view 构造的方式比较奇特:
1 | view { |
此处的 std::min
匹配的模板是 algorithm 中的针对初始化表的最小值函数。
1 | // file <algorithm> |
遍历过程
each
函数是操作 Components 的入口:
1 | template<std::size_t Comp, typename Func, std::size_t... Index> |
为啥遍历的是 std::get<Comp>(pools)->each()
而非 basic_view::view
❓
答:因为需要遍历函数涉及的所有组件。函数关联的组件是整个 basic_view 的子集,不一定包含了 basic_view::view
。
因此设计好用于遍历的 view 至关重要,性能消耗会来自于对集合的筛选,包括运行时检测等开销。
Scheduler
entt 本体并没有提供任何调度器。