feature/archetype-ecs #1

Merged
Misaki merged 23 commits from feature/archetype-ecs into develop 2025-12-17 08:27:32 +00:00
8 changed files with 614 additions and 124 deletions
Showing only changes of commit 948fae4401 - Show all commits

View File

@@ -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();
}
}

View File

@@ -1,4 +1,3 @@
global using EntityID = System.Int32;
global using GenerationID = System.UInt32;
global using WorldID = System.UInt16;
global using GenerationID = System.Int32;

View File

@@ -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();
}
}

View 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();
}
}

View 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();
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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" />