以 C# 容器为例,哈希表通常对应 Dictionary<int, T>,红黑树可以对应 SortedDictionary<int, T>。两者都是通用容器,而稀疏集利用了 ECS 的额外条件:Entity ID 是非负整数,System 不要求按 ID 顺序遍历,并且 Entity ID 的上界可以由 World 控制。
稀疏集并不是在所有方面都更好。它的 sparse 数组大小取决于 Entity ID 的范围。如果只创建了几个 Entity,ID 却已经增长到几千万,内存占用会非常不划算。因此实际 ECS 通常会限制 World 的 Entity 容量,并用空闲列表复用 Entity ID。
C# 实现
下面是一份简化的 Component 存储。构造时需要传入 World 支持的 Entity 容量,避免某个异常 ID 让 sparse 无限制扩张。稠密数组按需扩容,因此新增为均摊 O(1),删除和查询为 O(1);发生扩容的那次新增仍然需要复制数组。
示例使用 where T : struct,让 Component 值直接连续存放在数组中。如果允许 class Component,连续排列的只是对象引用,对象本身仍可能分散在托管堆上。
publicSparseSet(int entityCapacity) { if (entityCapacity < 0) thrownew ArgumentOutOfRangeException(nameof(entityCapacity));
_sparse = newint[entityCapacity]; }
publicint Count => _count;
public ReadOnlySpan<int> Entities => _denseEntities.AsSpan(0, _count);
public Span<T> Components => _denseComponents.AsSpan(0, _count);
publicboolContains(int entity) { return TryGetDenseIndex(entity, out _); }
publicvoidAdd(int entity, T component) { if ((uint)entity >= (uint)_sparse.Length) { thrownew ArgumentOutOfRangeException( nameof(entity), "Entity ID is outside the configured capacity."); }
if (Contains(entity)) thrownew InvalidOperationException( $"Entity {entity} already has {typeof(T).Name}.");
publicref T Get(int entity) { if (!TryGetDenseIndex(entity, outint denseIndex)) { thrownew KeyNotFoundException( $"Entity {entity} does not have {typeof(T).Name}."); }
returnref _denseComponents[denseIndex]; }
publicboolRemove(int entity) { if (!TryGetDenseIndex(entity, outint denseIndex)) returnfalse;
int lastIndex = --_count;
if (denseIndex != lastIndex) { int lastEntity = _denseEntities[lastIndex];