oecs-sharp/OECS/EntityAllocator.cs

111 lines
3.2 KiB
C#
Raw Normal View History

namespace OECS;
/// <summary>
/// Manages entity ID allocation and recycling.
///
/// Uses a free-list for recycled IDs and a bump allocator for new IDs.
/// Each ID slot tracks a version byte that increments on recycle to
/// prevent use-after-free bugs.
/// </summary>
internal class EntityAllocator
{
private const int DefaultCapacity = 1024;
private const uint FirstValidId = 2; // 0 = Null, 1 = Singleton
private byte[] _versions;
private readonly Stack<uint> _freeIds;
private uint _nextId;
public EntityAllocator(int initialCapacity = DefaultCapacity)
{
_versions = new byte[initialCapacity];
_freeIds = new Stack<uint>();
_nextId = FirstValidId;
}
/// <summary>
/// Allocates a new entity. Reuses a free ID if available, otherwise
/// bumps the allocator.
/// </summary>
public Entity Allocate()
{
uint id;
if (_freeIds.Count > 0)
{
id = _freeIds.Pop();
}
else
{
id = _nextId++;
EnsureCapacity(id);
// First time this ID is used — start at version 1.
// 0 means "never alive" and is used by the sentinel.
_versions[id] = 1;
}
return new Entity(id, _versions[id]);
}
/// <summary>
/// Frees an entity ID, making it available for reuse.
/// Increments the version to invalidate any outstanding references.
/// </summary>
public void Free(Entity entity)
{
uint id = entity.Id;
EnsureCapacity(id);
// Increment version (wrap at 255 back to 1; 0 means "never alive").
_versions[id]++;
if (_versions[id] == 0)
_versions[id] = 1;
_freeIds.Push(id);
}
/// <summary>
/// Registers the singleton entity so that <see cref="IsAlive"/> returns true for it.
/// Called once by <see cref="World"/> when the first singleton accessor is used.
/// </summary>
public void RegisterSingleton(Entity singleton)
{
EnsureCapacity(singleton.Id);
_versions[singleton.Id] = (byte)singleton.Version;
}
/// <summary>
/// Reserves a specific entity ID and version for deserialization.
/// Advances <see cref="_nextId"/> past the reserved ID so future
/// allocations don't collide.
/// </summary>
public void Reserve(Entity entity)
{
uint id = entity.Id;
EnsureCapacity(id);
_versions[id] = (byte)entity.Version;
if (id >= _nextId)
_nextId = id + 1;
}
/// <summary>
/// Returns true if the entity's version matches the current version for its ID.
/// </summary>
public bool IsAlive(Entity entity)
{
uint id = entity.Id;
if (entity.IsNull) return false;
if (id == 1) return true; // Singleton is always alive.
if (id >= _versions.Length) return false;
return _versions[id] == entity.Version && _versions[id] != 0;
}
private void EnsureCapacity(uint id)
{
if (id >= _versions.Length)
{
int newSize = Math.Max((int)id + 1, _versions.Length * 2);
Array.Resize(ref _versions, newSize);
}
}
}