oecs-sharp/src/OECS/SparseSet.cs

159 lines
4.4 KiB
C#
Raw Normal View History

using System.Runtime.CompilerServices;
namespace OECS;
/// <summary>
/// Non-generic interface for sparse set operations that don't require
/// knowing the component type at compile time (e.g., entity destruction).
/// </summary>
internal interface ISparseSet
{
void Remove(Entity entity);
bool Contains(Entity entity);
int Count { get; }
}
/// <summary>
/// A sparse set storing components of type <typeparamref name="T"/>.
///
/// Uses a dense array (packed, no holes) and a sparse array (maps entity ID
/// to dense index) for O(1) add, remove, and lookup. Swap-remove keeps
/// removals cheap at the cost of iteration order instability.
/// </summary>
internal class SparseSet<T> : ISparseSet where T : struct
{
private const int DefaultCapacity = 64;
private T[] _dense;
private Entity[] _denseEntities;
private int[] _sparse;
private int _count;
public SparseSet(int initialCapacity = DefaultCapacity)
{
_dense = new T[initialCapacity];
_denseEntities = new Entity[initialCapacity];
_sparse = new int[initialCapacity];
Array.Fill(_sparse, -1);
}
/// <summary>
/// Number of components currently stored.
/// </summary>
public int Count => _count;
/// <summary>
/// The dense array of component values (first <see cref="Count"/> elements are valid).
/// </summary>
public T[] Dense => _dense;
/// <summary>
/// The dense array of entity IDs, parallel to <see cref="Dense"/>.
/// </summary>
public Entity[] DenseEntities => _denseEntities;
/// <summary>
/// Adds a component for the given entity. Replaces if already present.
/// </summary>
public void Add(Entity entity, T component)
{
EnsureSparseCapacity(entity.Id);
int index = _sparse[entity.Id];
if (index != -1)
{
// Already exists — replace in place.
_dense[index] = component;
return;
}
EnsureDenseCapacity(_count + 1);
index = _count;
_dense[index] = component;
_denseEntities[index] = entity;
_sparse[entity.Id] = index;
_count++;
}
/// <summary>
/// Removes the component for the given entity. No-op if not present.
/// </summary>
public void Remove(Entity entity)
{
if (entity.Id >= (uint)_sparse.Length)
return;
int index = _sparse[entity.Id];
if (index == -1)
return;
// Swap-remove: move the last element into the removed slot.
int lastIndex = _count - 1;
if (index != lastIndex)
{
_dense[index] = _dense[lastIndex];
_denseEntities[index] = _denseEntities[lastIndex];
_sparse[_denseEntities[index].Id] = index;
}
_dense[lastIndex] = default!;
_denseEntities[lastIndex] = default;
_sparse[entity.Id] = -1;
_count--;
}
/// <summary>
/// Returns a reference to the component for the given entity.
/// Throws if the entity does not have this component.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T Get(Entity entity)
{
int index = _sparse[entity.Id];
return ref _dense[index];
}
/// <summary>
/// Returns true if the entity has this component.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Contains(Entity entity)
{
return entity.Id < (uint)_sparse.Length && _sparse[entity.Id] != -1;
}
/// <summary>
/// Removes all components.
/// </summary>
public void Clear()
{
Array.Clear(_dense, 0, _count);
for (int i = 0; i < _count; i++)
{
_sparse[_denseEntities[i].Id] = -1;
}
_count = 0;
}
private void EnsureSparseCapacity(uint entityId)
{
if (entityId >= (uint)_sparse.Length)
{
int newSize = Math.Max((int)entityId + 1, _sparse.Length * 2);
Array.Resize(ref _sparse, newSize);
Array.Fill(_sparse, -1, _sparse.Length - (newSize - _sparse.Length), newSize - _sparse.Length);
}
}
private void EnsureDenseCapacity(int required)
{
if (required > _dense.Length)
{
int newSize = Math.Max(required, _dense.Length * 2);
Array.Resize(ref _dense, newSize);
Array.Resize(ref _denseEntities, newSize);
}
}
}