2026-07-18 19:03:37 +08:00
|
|
|
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);
|
2026-07-18 19:33:05 +08:00
|
|
|
bool Contains(Entity entity);
|
2026-07-18 19:03:37 +08:00
|
|
|
int Count { get; }
|
2026-07-18 21:06:57 +08:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Returns the dense entity array (first <see cref="Count"/> elements are valid).
|
|
|
|
|
/// Used by serialization to enumerate entities without knowing the component type.
|
|
|
|
|
/// </summary>
|
|
|
|
|
Entity[] GetDenseEntities();
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Returns the component at the given dense index as an object.
|
|
|
|
|
/// Used by serialization to extract component values.
|
|
|
|
|
/// </summary>
|
|
|
|
|
object GetComponentAt(int denseIndex);
|
2026-07-18 19:03:37 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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)
|
|
|
|
|
{
|
2026-07-18 20:07:43 +08:00
|
|
|
if (entity.Id >= (uint)_sparse.Length)
|
|
|
|
|
ThrowEntityNotPresent(entity);
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
int index = _sparse[entity.Id];
|
2026-07-18 20:07:43 +08:00
|
|
|
if (index == -1)
|
|
|
|
|
ThrowEntityNotPresent(entity);
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
return ref _dense[index];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 20:07:43 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Tries to get the component value for the given entity.
|
|
|
|
|
/// Returns true and copies the value to <paramref name="value"/> if present.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool TryGet(Entity entity, out T value)
|
|
|
|
|
{
|
|
|
|
|
if (entity.Id >= (uint)_sparse.Length)
|
|
|
|
|
{
|
|
|
|
|
value = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int index = _sparse[entity.Id];
|
|
|
|
|
if (index == -1)
|
|
|
|
|
{
|
|
|
|
|
value = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
value = _dense[index];
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ThrowEntityNotPresent(Entity entity)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Entity {entity} does not have component of type {typeof(T).Name}.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
/// <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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:06:57 +08:00
|
|
|
Entity[] ISparseSet.GetDenseEntities() => _denseEntities;
|
|
|
|
|
|
|
|
|
|
object ISparseSet.GetComponentAt(int denseIndex) => _dense[denseIndex]!;
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
private void EnsureSparseCapacity(uint entityId)
|
|
|
|
|
{
|
|
|
|
|
if (entityId >= (uint)_sparse.Length)
|
|
|
|
|
{
|
2026-07-18 19:58:23 +08:00
|
|
|
int oldSize = _sparse.Length;
|
2026-07-18 19:03:37 +08:00
|
|
|
int newSize = Math.Max((int)entityId + 1, _sparse.Length * 2);
|
|
|
|
|
Array.Resize(ref _sparse, newSize);
|
2026-07-18 19:58:23 +08:00
|
|
|
Array.Fill(_sparse, -1, oldSize, newSize - oldSize);
|
2026-07-18 19:03:37 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|