前文已经说过,如果不对 稀疏集 的空洞进行回收,那么将造成其 稀疏数组 过于稀疏的问题,所以我们要想办法填补这些空隙
具体来说,就是在新增实体的时候,优先使用这些空洞位置 (可以认为是被销毁的实体),也就是说,需要复用 被销毁实体 的 标识符,将空洞处的 标识符 再次赋予新的实体使用
隐式链表
我们一般使用 隐式链表 结构来存储所有的空洞位置,这种结构如下
- 使用
next存储当前可以使用的第一个空洞位置的 索引 - 而空洞位置中直接存储下一个空洞位置的 索引
- 额外使用一个
available存储当前存在的空洞总数
Implicit Free List
next + available + sparse holes
可以发现这种 链表 结构是直接借助了 稀疏数组 本身存储的,而不是一个独立的结构,所以称它为 隐式链表
在这种结构下,创建实体 和 销毁实体 需要遵从特殊的步骤
创建实体
- 检查
available是否大于 0 - 如果有空闲标识符,使用
next指向的索引 - 更新
next为链表中的下一个空闲位置 - 减少
available计数
销毁实体
- 将当前
next中的值存储到被销毁 实体 的位置中 - 更新
next指向新销毁的位置 - 增加
available的计数
Destroy Entity
destroy(E6) pushes one slot to the head
slot[E6] = old nextnext = E6available++版本号
看起来很完美,但是这种方案任然存在一个问题,由于被销毁位置存储的是下一个空洞位置的索引我们无法 通过标识符位置检查实体是否已销毁 (因为无法分辨其中存储的索引是真的指向有效的 密集数组 还是 稀疏数组 中的下一个空洞) 如果想通过标识符位置检查实体是否已销毁,需要追溯链表,但隐式链表是单向的,无法轻松实现
解决方案是,同时存储 版本号,例如
entities[1] = 0x00010000; // 实体部分为 0x0001,版本部分为 0x0000
entities[2] = 0x00020001; // 实体部分为 0x0002,版本部分为 0x0001假设我们要验证标识符 0x00010001 是否有效
- 使用实体部分
0x0001找到其在entities数组中的位置(第 1 个位置) - 检查存储的 版本 部分是否等于 标识符 中的 版本 部分
- 如果版本部分不一致,说明该 标识符 已经失效
使用版本号可以直接通过实体数组中的版本部分进行验证,无需额外的数据结构来记录已销毁实体,这是因为 标识符 id 本身同时存储 实体编号 和 版本号,例如对于 32bit 的 标识符 可以使用前 16bit 存储 实体编号,后 16bit 存储 版本号,当然可以根据需要来决定哪一部分具体使用多少 bit 位
Generation Version
index reuse is checked by generation bits
所以每次销毁实体时版本号递增,确保新标识符与旧标识符区分开,验证时通过比较标识符中的版本部分与实体数组中存储的版本部分是否一致,即可快速判断有效性 (按位运算)