feature/archetype-ecs #1
@@ -1,3 +1,4 @@
|
||||
using Ghost.Core;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
@@ -5,25 +6,120 @@ using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.ArcEntities;
|
||||
|
||||
public unsafe struct Archetype : IDisposable
|
||||
internal unsafe struct Chuck : IDisposable
|
||||
{
|
||||
public const int CHUNK_SIZE = 16384; // 16 KB
|
||||
|
||||
private UnsafeArray<byte> _data;
|
||||
private int _count;
|
||||
private int _capacity;
|
||||
|
||||
public int Count
|
||||
{
|
||||
get => _count;
|
||||
set => _count = value;
|
||||
}
|
||||
|
||||
public int Capacity => _capacity;
|
||||
|
||||
public Chuck(int size, int capacity)
|
||||
{
|
||||
_data = new UnsafeArray<byte>(size, Allocator.Persistent);
|
||||
_capacity = capacity;
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public byte* GetUnsafePtr()
|
||||
{
|
||||
return (byte*)_data.GetUnsafePtr();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_data.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal struct Edge
|
||||
{
|
||||
public int componentID;
|
||||
public Identifier<Archetype> targetArchetype;
|
||||
}
|
||||
|
||||
internal unsafe struct Archetype : IIdentifierType, IDisposable
|
||||
{
|
||||
private struct ComponentMemoryLayout
|
||||
{
|
||||
public int offset;
|
||||
public int size;
|
||||
}
|
||||
|
||||
private readonly Identifier<Archetype> _id;
|
||||
private readonly Identifier<World> _worldID;
|
||||
|
||||
private UnsafeBitSet _signature;
|
||||
private UnsafeList<Chuck> _chunks;
|
||||
private UnsafeArray<int> _offsets;
|
||||
private UnsafeArray<ComponentMemoryLayout> _layouts;
|
||||
private UnsafeArray<int> _componentIDToOffset;
|
||||
|
||||
private UnsafeList<Edge> _edgesAdd;
|
||||
private UnsafeList<Edge> _edgesRemove;
|
||||
|
||||
private int _entityCapacity;
|
||||
private int _maxComponentID;
|
||||
private int _entityIdsOffset;
|
||||
|
||||
public UnsafeBitSet Signature => _signature;
|
||||
public int EntityCapacity => _entityCapacity;
|
||||
public int ChunkCount => _chunks.Count;
|
||||
|
||||
public Archetype(ReadOnlySpan<ComponentInfo> components)
|
||||
public Archetype(Identifier<Archetype> id, Identifier<World> worldID, ReadOnlySpan<int> componentIds)
|
||||
{
|
||||
_id = id;
|
||||
_worldID = worldID;
|
||||
|
||||
if (componentIds.IsEmpty)
|
||||
{
|
||||
_signature = new UnsafeBitSet(1, Allocator.Persistent, AllocationOption.Clear);
|
||||
_chunks = new UnsafeList<Chuck>(4, Allocator.Persistent);
|
||||
CalculateLayout(components);
|
||||
|
||||
_edgesAdd = new UnsafeList<Edge>(4, Allocator.Persistent);
|
||||
_edgesRemove = new UnsafeList<Edge>(4, Allocator.Persistent);
|
||||
|
||||
_signature.ClearAll();
|
||||
|
||||
_entityCapacity = Chuck.CHUNK_SIZE / sizeof(Entity);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private void CalculateLayout(ReadOnlySpan<ComponentInfo> components)
|
||||
var highestComponentID = 0;
|
||||
for (var i = 0; i < componentIds.Length; i++)
|
||||
{
|
||||
if (componentIds[i] > highestComponentID)
|
||||
{
|
||||
highestComponentID = componentIds[i];
|
||||
}
|
||||
}
|
||||
|
||||
_signature = new UnsafeBitSet(highestComponentID, Allocator.Persistent, AllocationOption.Clear);
|
||||
_chunks = new UnsafeList<Chuck>(4, Allocator.Persistent);
|
||||
|
||||
_edgesAdd = new UnsafeList<Edge>(4, Allocator.Persistent);
|
||||
_edgesRemove = new UnsafeList<Edge>(4, Allocator.Persistent);
|
||||
|
||||
var pComponents = stackalloc ComponentInfo[componentIds.Length];
|
||||
for (var i = 0; i < componentIds.Length; i++)
|
||||
{
|
||||
_signature.SetBit(componentIds[i]);
|
||||
pComponents[i] = ComponentRegister.s_registeredComponents[componentIds[i]];
|
||||
}
|
||||
|
||||
CalculateLayout(new Span<ComponentInfo>(pComponents, componentIds.Length));
|
||||
}
|
||||
|
||||
private void CalculateLayout(Span<ComponentInfo> components)
|
||||
{
|
||||
var entitySize = sizeof(Entity);
|
||||
var entityAlign = (int)MemoryUtility.AlignOf<Entity>();
|
||||
@@ -42,14 +138,12 @@ public unsafe struct Archetype : IDisposable
|
||||
|
||||
_maxComponentID = maxComponentID;
|
||||
_entityCapacity = Chuck.CHUNK_SIZE / bytesPerEntity;
|
||||
_offsets = new UnsafeArray<int>(components.Length, Allocator.Persistent);
|
||||
_layouts = new UnsafeArray<ComponentMemoryLayout>(components.Length, Allocator.Persistent);
|
||||
_componentIDToOffset = new UnsafeArray<int>(_maxComponentID + 1, Allocator.Persistent);
|
||||
|
||||
_componentIDToOffset.AsSpan().Fill(-1);
|
||||
|
||||
var sortedComponents = new ComponentInfo[components.Length];
|
||||
components.CopyTo(sortedComponents);
|
||||
Array.Sort(sortedComponents, (a, b) => b.alignment.CompareTo(a.alignment));
|
||||
components.Sort((a, b) => b.alignment.CompareTo(a.alignment));
|
||||
|
||||
while (_entityCapacity > 0)
|
||||
{
|
||||
@@ -57,15 +151,16 @@ public unsafe struct Archetype : IDisposable
|
||||
var fits = true;
|
||||
|
||||
currentOffset = (currentOffset + entityAlign - 1) & ~(entityAlign - 1);
|
||||
currentOffset += _entityCapacity * entitySize;
|
||||
|
||||
_entityIdsOffset = currentOffset;
|
||||
currentOffset += _entityCapacity * entitySize;
|
||||
|
||||
var tempOffsets = stackalloc int[components.Length];
|
||||
|
||||
for (var i = 0; i < sortedComponents.Length; i++)
|
||||
for (var i = 0; i < components.Length; i++)
|
||||
{
|
||||
var size = sortedComponents[i].size;
|
||||
var align = sortedComponents[i].alignment;
|
||||
var size = components[i].size;
|
||||
var align = components[i].alignment;
|
||||
|
||||
currentOffset = (currentOffset + align - 1) & ~(align - 1);
|
||||
tempOffsets[i] = currentOffset;
|
||||
@@ -80,10 +175,15 @@ public unsafe struct Archetype : IDisposable
|
||||
|
||||
if (fits)
|
||||
{
|
||||
for (var i = 0; i < sortedComponents.Length; i++)
|
||||
for (var i = 0; i < components.Length; i++)
|
||||
{
|
||||
_offsets[i] = tempOffsets[i];
|
||||
_componentIDToOffset[sortedComponents[i].id] = tempOffsets[i];
|
||||
_layouts[i] = new ComponentMemoryLayout
|
||||
{
|
||||
offset = tempOffsets[i],
|
||||
size = components[i].size
|
||||
};
|
||||
|
||||
_componentIDToOffset[components[i].id] = tempOffsets[i];
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -93,7 +193,42 @@ public unsafe struct Archetype : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
internal ref Chuck GetChunkReference(int index)
|
||||
public void AllocateEntity(out int chunkIndex, out int rowIndex)
|
||||
{
|
||||
for (var i = 0; i < _chunks.Count; i++)
|
||||
{
|
||||
var chunk = _chunks[i];
|
||||
if (chunk.Count < _entityCapacity)
|
||||
{
|
||||
rowIndex = chunk.Count;
|
||||
chunk.Count++;
|
||||
chunkIndex = i;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Need to allocate a new chunk
|
||||
var newChunk = new Chuck(Chuck.CHUNK_SIZE, _entityCapacity);
|
||||
_chunks.Add(newChunk);
|
||||
|
||||
rowIndex = 0;
|
||||
newChunk.Count++;
|
||||
chunkIndex = _chunks.Count - 1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetEntity(int chunkIndex, int rowIndex, Entity entity)
|
||||
{
|
||||
var chunk = _chunks[chunkIndex];
|
||||
var chunkBase = chunk.GetUnsafePtr();
|
||||
var pEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * rowIndex);
|
||||
|
||||
MemoryUtility.MemCpy(&entity, pEntity, (nuint)sizeof(Entity));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ref Chuck GetChunkReference(int index)
|
||||
{
|
||||
return ref _chunks[index];
|
||||
}
|
||||
@@ -109,37 +244,99 @@ public unsafe struct Archetype : IDisposable
|
||||
return _componentIDToOffset[componentId];
|
||||
}
|
||||
|
||||
public void RemoveEntity(int chunkIndex, int rowIndex)
|
||||
public ResultStatus RemoveEntity(int chunkIndex, int rowIndex)
|
||||
{
|
||||
var chunk = _chunks[chunkIndex];
|
||||
if (chunkIndex < 0 || chunkIndex >= _chunks.Count)
|
||||
{
|
||||
return ResultStatus.InvalidArgument;
|
||||
}
|
||||
|
||||
ref var chunk = ref _chunks[chunkIndex];
|
||||
int lastIndex = chunk.Count - 1;
|
||||
|
||||
// 1. If we are NOT removing the very last entity, we must swap.
|
||||
// If we are NOT removing the very last entity, we must swap.
|
||||
if (rowIndex != lastIndex)
|
||||
{
|
||||
// A. We are moving the 'last' entity into the 'row' spot.
|
||||
// We need to know WHO that last entity is so we can update the lookup map.
|
||||
var chunkBase = chunk.GetUnsafePtr();
|
||||
var pLastEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * lastIndex);
|
||||
var pRowEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * rowIndex);
|
||||
|
||||
var pLastEntity = chunk.GetUnsafePtr() + _entityIdsOffset + (sizeof(Entity) * lastIndex);
|
||||
var lastEntity = *(Entity*)pLastEntity;
|
||||
|
||||
// B. Now we can update the map
|
||||
// World.UpdateLocation(lastEntity.ID, newIndex: rowIndex);
|
||||
|
||||
// C. Perform the memory copy (Swap components)
|
||||
for (var i = 0; i <= _offsets.Count; i++)
|
||||
var wroldResult = World.GetWorld(_worldID);
|
||||
if (wroldResult.Status != ResultStatus.Success)
|
||||
{
|
||||
var offset = _offsets[i];
|
||||
var compSize = ComponentRegister.GetComponentInfo(i).size;
|
||||
return wroldResult.Status;
|
||||
}
|
||||
|
||||
var pRow = chunk.GetUnsafePtr() + offset + (compSize * rowIndex);
|
||||
var pLast = chunk.GetUnsafePtr() + offset + (compSize * lastIndex);
|
||||
var result = wroldResult.Value.EntityManager.UpdateEntityLocation(*(Entity*)pLastEntity, _id, chunkIndex, rowIndex);
|
||||
if (result != ResultStatus.Success)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
MemoryUtility.MemCpy(pLast, pRow, (nuint)compSize);
|
||||
// Only operate the swap back after the update is succeed.
|
||||
MemoryUtility.MemCpy(pLastEntity, pRowEntity, (nuint)sizeof(Entity));
|
||||
|
||||
for (var i = 0; i <= _layouts.Count; i++)
|
||||
{
|
||||
var layout = _layouts[i];
|
||||
|
||||
var pRow = chunk.GetUnsafePtr() + layout.offset + (layout.size * rowIndex);
|
||||
var pLast = chunk.GetUnsafePtr() + layout.offset + (layout.size * lastIndex);
|
||||
|
||||
MemoryUtility.MemCpy(pLast, pRow, (nuint)layout.size);
|
||||
}
|
||||
}
|
||||
|
||||
chunk.Count--;
|
||||
return ResultStatus.Success;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void AddEdgeAdd(int componentID, Identifier<Archetype> targetArchetype)
|
||||
{
|
||||
_edgesAdd.Add(new Edge
|
||||
{
|
||||
componentID = componentID,
|
||||
targetArchetype = targetArchetype
|
||||
});
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Result<int, ResultStatus> GetEdgeAdd(int componentID)
|
||||
{
|
||||
for (var i = 0; i < _edgesAdd.Count; i++)
|
||||
{
|
||||
if (_edgesAdd[i].componentID == componentID)
|
||||
{
|
||||
return Result.Create(_edgesAdd[i].targetArchetype.value, ResultStatus.Success);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Create(-1, ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void AddEdgeRemove(int componentID, Identifier<Archetype> targetArchetype)
|
||||
{
|
||||
_edgesRemove.Add(new Edge
|
||||
{
|
||||
componentID = componentID,
|
||||
targetArchetype = targetArchetype
|
||||
});
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Result<int, ResultStatus> GetEdgeRemove(int componentID)
|
||||
{
|
||||
for (var i = 0; i < _edgesRemove.Count; i++)
|
||||
{
|
||||
if (_edgesRemove[i].componentID == componentID)
|
||||
{
|
||||
return Result.Create(_edgesRemove[i].targetArchetype.value, ResultStatus.Success);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Create(-1, ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -164,13 +361,19 @@ public unsafe struct Archetype : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_componentIDToOffset.Dispose();
|
||||
|
||||
foreach (var chunk in _chunks)
|
||||
if (_chunks.IsCreated)
|
||||
{
|
||||
foreach (ref var chunk in _chunks)
|
||||
{
|
||||
chunk.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_signature.Dispose();
|
||||
_chunks.Dispose();
|
||||
_componentIDToOffset.Dispose();
|
||||
_layouts.Dispose();
|
||||
_edgesAdd.Dispose();
|
||||
_edgesRemove.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
global using EntityID = System.Int32;
|
||||
global using GenerationID = System.UInt32;
|
||||
global using WorldID = System.UInt16;
|
||||
global using GenerationID = System.Int32;
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.ArcEntities;
|
||||
|
||||
public struct ComponentInfo
|
||||
{
|
||||
// public FixedText64 stableName; // Do we actually need this?
|
||||
public int size;
|
||||
public int alignment;
|
||||
public int id;
|
||||
@@ -15,34 +13,48 @@ public struct ComponentInfo
|
||||
internal static unsafe class ComponentTypeID<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
public static readonly int value = ComponentRegister.s_nextComponentTypeID++;
|
||||
public static readonly int value = ComponentRegister.GetOrRegisterComponent<T>();
|
||||
}
|
||||
|
||||
internal static class ComponentRegister
|
||||
{
|
||||
internal static int s_nextComponentTypeID = 0;
|
||||
private static int s_nextComponentTypeID = 0;
|
||||
private static Dictionary<IntPtr, int> s_typeHandleToID = new();
|
||||
|
||||
internal static List<ComponentInfo> s_registeredComponents = new();
|
||||
internal static Dictionary<string, int> s_nameToRuntimeID = new();
|
||||
|
||||
internal unsafe static int GetOrRegisterComponent<T>()
|
||||
where T : unmanaged
|
||||
{
|
||||
var typeId = ComponentTypeID<T>.value;
|
||||
while (s_registeredComponents.Count <= typeId)
|
||||
var typeHandle = typeof(T).TypeHandle.Value;
|
||||
|
||||
lock (s_registeredComponents)
|
||||
{
|
||||
s_registeredComponents.Add(default);
|
||||
if (s_typeHandleToID.TryGetValue(typeHandle, out int existingID))
|
||||
{
|
||||
return existingID;
|
||||
}
|
||||
|
||||
if (s_registeredComponents[typeId].size == 0)
|
||||
{
|
||||
s_registeredComponents[typeId] = new ComponentInfo
|
||||
int newID = s_nextComponentTypeID++;
|
||||
string stableName = typeof(T).FullName ?? typeof(T).Name;
|
||||
|
||||
var info = new ComponentInfo
|
||||
{
|
||||
// stableName = new FixedText64(stableName),
|
||||
size = sizeof(T),
|
||||
alignment = (int)MemoryUtility.AlignOf<T>(),
|
||||
id = typeId
|
||||
id = newID,
|
||||
};
|
||||
}
|
||||
|
||||
return typeId;
|
||||
while (s_registeredComponents.Count <= newID) s_registeredComponents.Add(default);
|
||||
s_registeredComponents[newID] = info;
|
||||
|
||||
s_typeHandleToID[typeHandle] = newID;
|
||||
s_nameToRuntimeID[stableName] = newID;
|
||||
|
||||
return newID;
|
||||
}
|
||||
}
|
||||
|
||||
internal static ComponentInfo GetComponentInfo(int typeId)
|
||||
@@ -50,38 +62,3 @@ internal static class ComponentRegister
|
||||
return s_registeredComponents[typeId];
|
||||
}
|
||||
}
|
||||
|
||||
internal unsafe struct Chuck : IDisposable
|
||||
{
|
||||
public const int CHUNK_SIZE = 16384; // 16 KB
|
||||
|
||||
private UnsafeArray<byte> _data;
|
||||
private int _count;
|
||||
private int _capacity;
|
||||
|
||||
public int Count
|
||||
{
|
||||
get => _count;
|
||||
set => _count = value;
|
||||
}
|
||||
|
||||
public int Capacity => _capacity;
|
||||
|
||||
public Chuck(int size, int capacity)
|
||||
{
|
||||
_data = new UnsafeArray<byte>(size, Allocator.Persistent);
|
||||
_capacity = capacity;
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public byte* GetUnsafePtr()
|
||||
{
|
||||
return (byte*)_data.GetUnsafePtr();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_data.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
110
Ghost.ArcEntities/EntityCommandBuffer.cs
Normal file
110
Ghost.ArcEntities/EntityCommandBuffer.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
|
||||
namespace Ghost.ArcEntities;
|
||||
|
||||
public unsafe class EntityCommandBuffer : IDisposable
|
||||
{
|
||||
private enum CommandType
|
||||
{
|
||||
CreateEntity,
|
||||
DestroyEntity,
|
||||
AddComponent,
|
||||
RemoveComponent,
|
||||
SetComponent,
|
||||
}
|
||||
|
||||
private struct Command
|
||||
{
|
||||
public UnsafeArray<byte> data;
|
||||
public CommandType type;
|
||||
public Entity entity;
|
||||
public int componentTypeID;
|
||||
}
|
||||
|
||||
private readonly EntityManager _entityManager;
|
||||
private UnsafeList<Command> _commands;
|
||||
|
||||
public EntityCommandBuffer(EntityManager entityManager)
|
||||
{
|
||||
_entityManager = entityManager;
|
||||
_commands = new UnsafeList<Command>(32, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent);
|
||||
}
|
||||
|
||||
public void CreateEntity()
|
||||
{
|
||||
var command = new Command
|
||||
{
|
||||
type = CommandType.CreateEntity,
|
||||
data = default,
|
||||
entity = default,
|
||||
componentTypeID = -1
|
||||
};
|
||||
|
||||
_commands.Add(command);
|
||||
}
|
||||
|
||||
public void DestroyEntity(Entity entity)
|
||||
{
|
||||
var command = new Command
|
||||
{
|
||||
type = CommandType.DestroyEntity,
|
||||
data = default,
|
||||
entity = entity,
|
||||
componentTypeID = -1
|
||||
};
|
||||
|
||||
_commands.Add(command);
|
||||
}
|
||||
|
||||
public void AddComponent<T>(Entity entity, T component)
|
||||
where T : unmanaged
|
||||
{
|
||||
var data = new UnsafeArray<byte>(sizeof(T), Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent);
|
||||
MemoryUtility.MemCpy(&component, data.GetUnsafePtr(), (nuint)sizeof(T));
|
||||
|
||||
var command = new Command
|
||||
{
|
||||
type = CommandType.AddComponent,
|
||||
data = data,
|
||||
entity = entity,
|
||||
componentTypeID = ComponentTypeID<T>.value
|
||||
};
|
||||
|
||||
_commands.Add(command);
|
||||
}
|
||||
|
||||
public void RemoveComponent<T>(Entity entity)
|
||||
where T : unmanaged
|
||||
{
|
||||
var command = new Command
|
||||
{
|
||||
type = CommandType.RemoveComponent,
|
||||
data = default,
|
||||
entity = entity,
|
||||
componentTypeID = ComponentTypeID<T>.value
|
||||
};
|
||||
|
||||
_commands.Add(command);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
foreach (ref var command in _commands)
|
||||
{
|
||||
command.data.Dispose();
|
||||
}
|
||||
|
||||
_commands.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (ref var command in _commands)
|
||||
{
|
||||
command.data.Dispose();
|
||||
}
|
||||
|
||||
_commands.Dispose();
|
||||
}
|
||||
}
|
||||
161
Ghost.ArcEntities/EntityManager.cs
Normal file
161
Ghost.ArcEntities/EntityManager.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using Ghost.Core;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
namespace Ghost.ArcEntities;
|
||||
|
||||
public class EntityManager : IDisposable
|
||||
{
|
||||
private struct EntityLocation
|
||||
{
|
||||
public Identifier<Archetype> archetypeID;
|
||||
public int chunkIndex;
|
||||
public int rowIndex;
|
||||
}
|
||||
|
||||
private World _world;
|
||||
private UnsafeSlotMap<EntityLocation> _entityLocations;
|
||||
|
||||
internal EntityManager(World world)
|
||||
{
|
||||
_world = world;
|
||||
_entityLocations = new UnsafeSlotMap<EntityLocation>(16, Allocator.Persistent);
|
||||
}
|
||||
|
||||
public Entity CreateEntity()
|
||||
{
|
||||
// Put into empty archetype
|
||||
ref var emptyArchetype = ref _world.GetArchetypeReference(World.EmptyArchetypeID);
|
||||
emptyArchetype.AllocateEntity(out var chunkIndex, out var rowIndex);
|
||||
|
||||
var id = _entityLocations.Add(new EntityLocation
|
||||
{
|
||||
archetypeID = World.EmptyArchetypeID,
|
||||
chunkIndex = chunkIndex,
|
||||
rowIndex = rowIndex
|
||||
}, out var generation);
|
||||
|
||||
var entity = new Entity(id, generation);
|
||||
emptyArchetype.SetEntity(chunkIndex, rowIndex, entity);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
public ResultStatus RemoveEntity(Entity entity)
|
||||
{
|
||||
if (!_entityLocations.TryGetElementAt(entity.ID, entity.Generation, out var location))
|
||||
{
|
||||
return ResultStatus.NotFound;
|
||||
}
|
||||
|
||||
ref var archetype = ref _world.GetArchetypeReference(location.archetypeID);
|
||||
var r = archetype.RemoveEntity(location.chunkIndex, location.rowIndex);
|
||||
if (r != ResultStatus.Success)
|
||||
{
|
||||
return r;
|
||||
}
|
||||
|
||||
if (!_entityLocations.Remove(entity.ID, entity.Generation))
|
||||
{
|
||||
return ResultStatus.NotFound;
|
||||
}
|
||||
|
||||
return ResultStatus.Success;
|
||||
}
|
||||
|
||||
internal ResultStatus UpdateEntityLocation(Entity entity, Identifier<Archetype> newArchetypeID, int newChunkIndex, int newRowIndex)
|
||||
{
|
||||
ref var location = ref _entityLocations.GetElementReferenceAt(entity.ID, entity.Generation, out var exist);
|
||||
if (!exist)
|
||||
{
|
||||
return ResultStatus.NotFound;
|
||||
}
|
||||
|
||||
location.archetypeID = newArchetypeID;
|
||||
location.chunkIndex = newChunkIndex;
|
||||
location.rowIndex = newRowIndex;
|
||||
|
||||
return ResultStatus.Success;
|
||||
}
|
||||
|
||||
public ResultStatus AddComponent<T>(Entity entity)
|
||||
where T : unmanaged
|
||||
{
|
||||
// Find current location
|
||||
ref var location = ref _entityLocations.GetElementReferenceAt(entity.ID, entity.Generation, out var exist);
|
||||
if (!exist)
|
||||
{
|
||||
return ResultStatus.NotFound;
|
||||
}
|
||||
|
||||
// Build new archetype signature
|
||||
ref var archetype = ref _world.GetArchetypeReference(location.archetypeID);
|
||||
var currentSignature = archetype.Signature;
|
||||
|
||||
var compID = ComponentTypeID<T>.value;
|
||||
|
||||
var largestComponentID = Math.Max(currentSignature.Count, compID);
|
||||
var length = UnsafeBitSet.RequiredLength(largestComponentID + 1);
|
||||
|
||||
Span<uint> bits = stackalloc uint[length];
|
||||
bits.Clear();
|
||||
|
||||
var newSignature = new SpanBitSet(bits);
|
||||
|
||||
var i = 0;
|
||||
var compCount = 0;
|
||||
do
|
||||
{
|
||||
i = currentSignature.NextSetBit(i);
|
||||
if (i == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
newSignature.SetBit(i);
|
||||
compCount++;
|
||||
} while (true);
|
||||
|
||||
compCount++;
|
||||
newSignature.SetBit(compID);
|
||||
|
||||
// Find or create new archetype
|
||||
var newSignatureHash = newSignature.GetHashCode();
|
||||
var newArcID = _world.GetArchetypeIDBySignatureHash(newSignatureHash);
|
||||
if (newArcID.IsNotValid)
|
||||
{
|
||||
// Create new archetype
|
||||
Span<int> componentTypeIDs = stackalloc int[compCount];
|
||||
componentTypeIDs[0] = compID;
|
||||
|
||||
do
|
||||
{
|
||||
i = currentSignature.NextSetBit(i);
|
||||
if (i == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
componentTypeIDs[--compCount] = i;
|
||||
} while (true);
|
||||
|
||||
_world.CreateArchetype(componentTypeIDs, newSignatureHash);
|
||||
}
|
||||
|
||||
ref var newArchetype = ref _world.GetArchetypeReference(newArcID);
|
||||
newArchetype.AllocateEntity(out var newChunkIndex, out var newRowIndex);
|
||||
newArchetype.SetEntity(newChunkIndex, newRowIndex, entity);
|
||||
|
||||
// Update location
|
||||
location.archetypeID = newArcID;
|
||||
location.chunkIndex = newChunkIndex;
|
||||
location.rowIndex = newRowIndex;
|
||||
|
||||
return ResultStatus.Success;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_entityLocations.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ public unsafe class EntityQuery<T1, T2>
|
||||
|
||||
private List<ArchetypeCache> _cache = new();
|
||||
|
||||
public void AddMatchingArchetype(Archetype archetype)
|
||||
internal void AddMatchingArchetype(Archetype archetype)
|
||||
{
|
||||
// We look up the offsets ONCE when the archetype is registered
|
||||
int off1 = archetype.GetOffset(ComponentTypeID<T1>.value);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Ghost.Core;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.ArcEntities;
|
||||
|
||||
public partial class World
|
||||
{
|
||||
private static List<World> s_worlds = new(4);
|
||||
private static Queue<WorldID> s_freeWorldSlots = new();
|
||||
private static List<World?> s_worlds = new(4);
|
||||
private static Queue<Identifier<World>> s_freeWorldSlots = new();
|
||||
|
||||
internal static Identifier<Archetype> EmptyArchetypeID => new Identifier<Archetype>(0);
|
||||
|
||||
public static int WorldCount => s_worlds.Count - s_freeWorldSlots.Count;
|
||||
|
||||
@@ -15,41 +20,60 @@ public partial class World
|
||||
{
|
||||
if (s_freeWorldSlots.TryDequeue(out var index))
|
||||
{
|
||||
s_worlds[index] = new World(index, entityCapacity);
|
||||
s_worlds[index.value] = new World(index, entityCapacity);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (s_worlds.Count >= WorldID.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException("Maximum number of worlds reached");
|
||||
}
|
||||
|
||||
index = (WorldID)s_worlds.Count;
|
||||
index = new Identifier<World>(s_worlds.Count);
|
||||
s_worlds.Add(new World(index, entityCapacity));
|
||||
}
|
||||
|
||||
return s_worlds[index];
|
||||
return s_worlds[index.value]!;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static World GetWorld(int index)
|
||||
public static Result<World, ResultStatus> GetWorld(Identifier<World> id)
|
||||
{
|
||||
return s_worlds[index];
|
||||
if (id.value < 0 || id.value >= s_worlds.Count)
|
||||
{
|
||||
return Result.Create(default(World)!, ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
var world = s_worlds[id.value];
|
||||
if (world is null)
|
||||
{
|
||||
return Result.Create(default(World)!, ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
return Result.Create(world, ResultStatus.Success);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class World : IDisposable, IEquatable<World>
|
||||
public partial class World : IIdentifierType, IDisposable, IEquatable<World>
|
||||
{
|
||||
private readonly WorldID _id;
|
||||
private readonly Identifier<World> _id;
|
||||
|
||||
private bool _isDisposed = false;
|
||||
private UnsafeList<Archetype> _archetypes;
|
||||
private UnsafeHashMap<int, Identifier<Archetype>> _archetypeLookup; // Signature Hash to Archetype ID
|
||||
private EntityManager _entityManager;
|
||||
|
||||
public WorldID ID => _id;
|
||||
private bool _disposed = false;
|
||||
|
||||
private World(WorldID id, int entityCapacity)
|
||||
public Identifier<World> ID => _id;
|
||||
public EntityManager EntityManager => _entityManager;
|
||||
|
||||
private World(Identifier<World> id, int entityCapacity)
|
||||
{
|
||||
_id = id;
|
||||
|
||||
_archetypes = new UnsafeList<Archetype>(16, Allocator.Persistent);
|
||||
_archetypeLookup = new UnsafeHashMap<int, Identifier<Archetype>>(16, Allocator.Persistent);
|
||||
|
||||
_entityManager = new EntityManager(this);
|
||||
|
||||
// Create the empty archetype
|
||||
CreateArchetype(ReadOnlySpan<int>.Empty, 0);
|
||||
}
|
||||
|
||||
~World()
|
||||
@@ -57,19 +81,32 @@ public partial class World : IDisposable, IEquatable<World>
|
||||
Dispose();
|
||||
}
|
||||
|
||||
internal Identifier<Archetype> CreateArchetype(ReadOnlySpan<int> componentTypeIDs, int signatureHash)
|
||||
{
|
||||
var arcID = new Identifier<Archetype>(_archetypes.Count);
|
||||
_archetypes.Add(new Archetype(arcID, _id, componentTypeIDs));
|
||||
_archetypeLookup.Add(signatureHash, arcID);
|
||||
return arcID;
|
||||
}
|
||||
|
||||
internal ref Archetype GetArchetypeReference(Identifier<Archetype> id)
|
||||
{
|
||||
return ref _archetypes[id.value];
|
||||
}
|
||||
|
||||
internal Identifier<Archetype> GetArchetypeIDBySignatureHash(int signatureHash)
|
||||
{
|
||||
if (_archetypeLookup.TryGetValue(signatureHash, out var arcID))
|
||||
{
|
||||
return arcID;
|
||||
}
|
||||
|
||||
return Identifier<Archetype>.Invalid;
|
||||
}
|
||||
|
||||
public bool Equals(World? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _id == other._id;
|
||||
return other is not null && _id == other._id;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
@@ -94,14 +131,17 @@ public partial class World : IDisposable, IEquatable<World>
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_archetypes.Dispose();
|
||||
_archetypeLookup.Dispose();
|
||||
|
||||
s_freeWorldSlots.Enqueue(_id);
|
||||
|
||||
_isDisposed = true;
|
||||
_disposed = true;
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Misaki.HighPerformance" Version="1.0.1" />
|
||||
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.2.5" />
|
||||
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.2.6" />
|
||||
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.2.6" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="10.0.0" />
|
||||
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.5" />
|
||||
|
||||
Reference in New Issue
Block a user