Implement core entity management features

Added `Archetype` struct with chunk management and disposal.
Added `BitSet` class for managing collections of bits.
Added `Class1.cs` as a placeholder for graphics functionality.
Added `ComponentData` struct and `ComponentPool` class for component management.
Added `ComponentRegistry` for efficient component registration.
Added `EntityChangeQueue` as a placeholder for future changes.
Added `Helpers.ttinclude` and `QueryRefComponent.tt` for code generation.
Added `Signature` struct for managing component signatures.
Added `World` struct to manage the game world and entities.
Added `QueryRefComponent` delegates for querying entities.

Changed `Archetype.cs` to implement `IDisposable`.
Changed `AssemblyInfo.cs` to update global using directives.
Changed `Chunk.cs` to introduce `ChunkCollection` for chunk management.
Changed `Component.cs` to refine component management methods.
Changed `Entity.cs` to improve properties and methods.
Changed `Ghost.Entities.csproj` to update project properties.
Changed `Program.cs` to demonstrate entity creation and querying.
Changed `World.Query.cs` to facilitate querying with components.
This commit is contained in:
2025-05-21 11:46:48 +09:00
parent 56a21bab2b
commit 0cf3104a6a
30 changed files with 1702 additions and 240 deletions

View File

@@ -0,0 +1,7 @@
global using EntityID = System.UInt32;
global using GenerationID = System.UInt16;
global using WorldID = System.UInt16;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.Engine")]

View File

@@ -0,0 +1,213 @@
using Misaki.HighPerformance.Unsafe.Collections;
using Misaki.HighPerformance.Unsafe.Helpers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Ghost.Entities;
internal unsafe struct ChunkCollection : IDisposable
{
private UnsafeArray<Chunk> _chunkStorage;
private int _count;
private int _capacity;
public readonly int Count => _count;
public readonly int Capacity => _capacity;
public readonly ref Chunk this[int index] => ref _chunkStorage[index];
public ChunkCollection(int capacity)
{
_chunkStorage = new(capacity, Allocator.Persistent);
_count = 0;
_capacity = capacity;
}
public void Add(Chunk chunk)
{
_chunkStorage[_count] = chunk;
_count++;
}
public void EnsureCapacity(int newCapacity)
{
if (newCapacity <= _capacity)
{
return;
}
_chunkStorage.Resize(newCapacity);
}
public void TrimExcess()
{
if (_count == _capacity)
{
return;
}
_chunkStorage.Resize(_count);
}
public void Clear()
{
for (var i = 0; i < _count; i++)
{
_chunkStorage[i].Clear();
}
_count = 0;
_capacity = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span<Chunk> AsSpan()
{
return _chunkStorage.AsSpan();
}
public void Dispose()
{
for (var i = 0; i < _count; i++)
{
_chunkStorage[i].Dispose();
}
_chunkStorage.Dispose();
_count = 0;
_capacity = 0;
}
}
internal struct Chunk : IDisposable
{
public UnsafeArray<Entity> entities;
public UnsafeArray<UnsafeArray<byte>> components;
// The component lookup array is used to quickly find the index of a component in the components array.
// Mapping component ID to component index in the components array.
private UnsafeArray<int> _componentLookup;
private int _count;
private readonly int _capacity;
private bool _isDisposed;
public readonly int Count => _count;
public readonly int Capacity => _capacity;
public Chunk(int capacity, Span<ComponentData> data) : this(capacity, data, Component.ToLookupArray(data, Allocator.Persistent))
{
}
public Chunk(int capacity, Span<ComponentData> data, UnsafeArray<int> lookup)
{
_count = 0;
_capacity = capacity;
entities = new((int)capacity, Allocator.Persistent);
components = new(data.Length, Allocator.Persistent);
_componentLookup = lookup;
for (var i = 0; i < data.Length; i++)
{
var component = data[i];
components[component.id] = new UnsafeArray<byte>(capacity * (int)component.sizeInByte, Allocator.Persistent);
}
}
public int Add(Entity entity)
{
var index = _count;
entities[index] = entity;
_count++;
return index;
}
public unsafe bool Remove(int index)
{
if (index < 0 || index >= _count)
{
return false;
}
var lastIndex = _count--;
entities[index] = entities[lastIndex];
for (var i = 0; i < components.Count; i++)
{
var componentArray = UnsafeUtilities.ReadArrayElementUnsafe<UnsafeArray<byte>>(components.GetUnsafePtr(), i);
var componentSize = componentArray->Count / _capacity;
var removedComponent = UnsafeUtilities.ReadArrayElementUnsafe<byte>(componentArray->GetUnsafePtr(), index * componentSize);
var lastComponent = UnsafeUtilities.ReadArrayElementUnsafe<byte>(componentArray->GetUnsafePtr(), lastIndex * componentSize);
MemoryUtilities.MemCpy(removedComponent, lastComponent, (nuint)componentSize);
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly int IndexOf<T>()
where T : unmanaged, IComponent
{
var id = Component<T>.data.id;
Debug.Assert(id != -1 && id < _componentLookup.Count, $"Index is out of bounds, component {typeof(T)} with id {id} does not exist in this chunk.");
return _componentLookup[id];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly bool Has<T>()
where T : unmanaged, IComponent
{
var id = Component<T>.data.id;
return id < _componentLookup.Count && _componentLookup[id] != -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly unsafe UnsafeArray<T> GetArrayOf<T>()
where T : unmanaged, IComponent
{
var index = IndexOf<T>();
var componentArray = components[index];
return UnsafeUtilities.CastArray<byte, T>(componentArray);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly unsafe ref T GetComponent<T>(int index)
where T : unmanaged, IComponent
{
var componentArray = GetArrayOf<T>();
return ref componentArray[index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Entity GetEntity(int index)
{
return entities[index];
}
public void Clear()
{
_count = 0;
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
entities.Dispose();
_componentLookup.Dispose();
for (var i = 0; i < components.Count; i++)
{
components[i].Dispose();
}
components.Dispose();
_isDisposed = true;
}
}

View File

@@ -0,0 +1,116 @@
using Ghost.Entities.Helpers;
using Ghost.Entities.Registries;
using Misaki.HighPerformance.Unsafe.Collections;
using Misaki.HighPerformance.Unsafe.Helpers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Entities;
public interface IComponent
{
}
[SkipLocalsInit]
internal readonly record struct ComponentData
{
public readonly int id;
public readonly int sizeInByte;
public ComponentData(int id, int sizeInByte)
{
this.id = id;
this.sizeInByte = sizeInByte;
}
}
internal static class Component
{
internal static int GetTotalSize(in Span<ComponentData> datas)
{
var size = 0;
foreach (var type in datas)
{
var typeSize = type.sizeInByte;
typeSize = typeSize != 1 ? typeSize : 0; // Ignore tag components
size += typeSize;
}
return size;
}
internal static unsafe UnsafeArray<int> ToLookupArray(in Span<ComponentData> datas, Allocator allocator)
{
var max = 0;
foreach (var data in datas)
{
var componentId = data.id;
if (componentId >= max)
{
max = componentId;
}
}
// Create lookup table where the component ID points to the component index.
var array = new UnsafeArray<int>(max + 1, allocator);
array.AsSpan().Fill(-1);
for (var index = 0; index < datas.Length; index++)
{
ref var type = ref datas[index];
var componentId = type.id;
array[componentId] = index;
}
return array;
}
internal static int GetHashCode(Span<ComponentData> components)
{
// Search for the highest id to determine how much uints we need for the stack.
var highestId = 0;
foreach (ref var cmp in components)
{
if (cmp.id > highestId)
{
highestId = cmp.id;
}
}
// Allocate the stack and set bits to replicate a bitset
var length = BitSet.RequiredLength(highestId + 1);
Span<uint> stack = stackalloc uint[length];
var spanBitSet = new SpanBitSet(stack);
foreach (ref var type in components)
{
var x = type.id;
spanBitSet.SetBit(x);
}
return GetHashCode(stack);
}
public static int GetHashCode(Span<uint> span)
{
var hashCode = new HashCode();
hashCode.AddBytes(MemoryMarshal.AsBytes(span));
return hashCode.ToHashCode();
}
}
internal static class Component<T>
where T : unmanaged, IComponent
{
public static readonly ComponentData data;
public static readonly Signature signature;
static Component()
{
data = ComponentRegistry.GetOrAdd<T>();
signature = new Signature(data);
}
}

View File

@@ -0,0 +1,19 @@
using Misaki.HighPerformance.Unsafe.Collections;
namespace Ghost.Entities.Core;
internal partial struct Archetype : IEquatable<Archetype>
{
public void AddInsertionEdge(Archetype archetype)
{
if (_insertionEdges.IsCreated)
{
_insertionEdges = new UnsafeHashSet<Archetype>(_BUCKET_SIZE, Allocator.Persistent);
}
_insertionEdges.Add(archetype);
}
public readonly bool Equals(Archetype other)
{
return signature.Equals(other.signature);
}
}

View File

@@ -0,0 +1,76 @@
using Ghost.Entities.Helpers;
using Misaki.HighPerformance.Unsafe.Collections;
using Misaki.HighPerformance.Unsafe.Helpers;
namespace Ghost.Entities.Core;
internal partial struct Archetype : IDisposable
{
private const int _BUCKET_SIZE = 16;
// The component ID to array index lookup array.
private UnsafeArray<int> _lookupArray;
public readonly int sizeOfBaseChunk;
public readonly int sizeOfChunk;
public readonly int entitiesPerChunk;
public int totalEntityCount;
public Signature signature;
// For fast lookups
public BitSet bitSet;
public ChunkCollection chunks;
private UnsafeHashSet<Archetype> _insertionEdges;
private UnsafeHashSet<Archetype> _deletionEdges;
public readonly UnsafeArray<int> LookupArray => _lookupArray;
internal Archetype(Signature signature, int sizeOfBaseChunk, int minimalEntityCountPerChunk)
{
this.signature = signature;
this.sizeOfBaseChunk = sizeOfBaseChunk;
var sizeOfTotalData = Component.GetTotalSize(signature.componentDatas.AsSpan());
sizeOfChunk = GetSizeOfChunk(sizeOfBaseChunk, minimalEntityCountPerChunk, sizeOfTotalData);
entitiesPerChunk = GetEntityCount(sizeOfChunk, sizeOfTotalData);
_lookupArray = Component.ToLookupArray(signature.componentDatas.AsSpan(), Allocator.Persistent);
bitSet = new BitSet();
for (var i = 0; i < signature.componentDatas.Count; i++)
{
bitSet.SetBit(signature.componentDatas[i].id);
}
chunks = new ChunkCollection(1);
AddNewChunk();
}
private unsafe static int GetEntityCount(int sizeOfChunk, int sizeOfTotalData)
{
return sizeOfChunk / (sizeof(Entity) + sizeOfTotalData);
}
private static unsafe int GetSizeOfChunk(int sizeOfBaseChunk, int entityCount, int sizeOfTotalData)
{
var entityBytes = (sizeof(Entity) + sizeOfTotalData) * entityCount;
return (int)Math.Ceiling((float)entityBytes / sizeOfBaseChunk) * sizeOfBaseChunk; // Calculates and rounds to a multiple of BaseSize to store the number of entities
}
public ref Chunk AddNewChunk()
{
chunks.EnsureCapacity(chunks.Count + 1);
chunks.Add(new Chunk(entitiesPerChunk, signature.componentDatas.AsSpan(), _lookupArray));
return ref chunks[^1];
}
public void Dispose()
{
_lookupArray.Dispose();
signature.Dispose();
bitSet.Dispose();
chunks.Dispose();
}
}

View File

@@ -0,0 +1,105 @@
using System.Runtime.CompilerServices;
namespace Ghost.Entities.Core;
/// <summary>
/// The <see cref="Slot"/> struct references an <see cref="Entity"/> entry within an <see cref="Archetype"/> using a reference to its <see cref="Chunk"/> and its index.
/// </summary>
[SkipLocalsInit]
internal record struct Slot
{
/// <summary>
/// The index of the <see cref="Entity"/> in the <see cref="Chunk"/>.
/// </summary>
public int index;
/// <summary>
/// The index of the <see cref="Chunk"/> in which the <see cref="Entity"/> is located.
/// </summary>
public int chunkIndex;
/// <summary>
/// Initializes a new instance of the <see cref="Slot"/> struct.
/// </summary>
/// <param name="index">The index of the <see cref="Entity"/> in the <see cref="Chunk"/>.</param>
/// <param name="chunkIndex">The index of the <see cref="Chunk"/> in which the <see cref="Entity"/> is located.</param>
public Slot(int index, int chunkIndex)
{
this.index = index;
this.chunkIndex = chunkIndex;
}
/// <summary>
/// Adds a plus operator for easy calculation of new <see cref="Slot"/>. Adds the positions of both <see cref="Slot"/>s.
/// </summary>
/// <param name="first">The first <see cref="Slot"/>.</param>
/// <param name="second">The second <see cref="Slot"/>.</param>
/// <returns>The result <see cref="Slot"/>.</returns>
public static Slot operator +(Slot first, Slot second)
{
return new Slot(first.index + second.index, first.chunkIndex + second.chunkIndex);
}
/// <summary>
/// Adds a plus plus operator for easy calculation of new <see cref="Slot"/>. Increases the index by one.
/// </summary>
/// <param name="slot">The <see cref="Slot"/>.</param>
/// <returns>The <see cref="Slot"/> with index increased by one..</returns>
public static Slot operator ++(Slot slot)
{
slot.index++;
return slot;
}
/// <summary>
/// Validates the <see cref="Slot"/>, moves the <see cref="Slot"/> if it is outside a <see cref="Chunk.Capacity"/> to match it.
/// </summary>
/// <returns></returns>
public void Wrap(int capacity)
{
// Result outside valid chunk, wrap into next one
if (index < capacity)
{
return;
}
// Index outside of its chunk, so we calculate how many times a chunk fit into the index for adjusting the chunkindex to that position.
// Floor since we do not need a rounded value since the index is within that chunk and not the next one.
chunkIndex += (int)Math.Floor(index / (float)capacity);
// After moving the chunk index we can simply take the rest and assign it as a index.
index %= capacity;
}
/// <summary>
/// Moves or shifts this <see cref="Slot"/> by one slot forward.
/// Ensures that the slots chunkindex updated properly once the end was reached.
/// </summary>
/// <param name="source">The <see cref="Slot"/> to shift by one.</param>
/// <param name="sourceCapacity">The capacity of the chunk the slot is in.</param>
/// <returns></returns>
public static Slot Shift(ref Slot source, int sourceCapacity)
{
source.index++;
source.Wrap(sourceCapacity);
return source;
}
/// <summary>
/// Moves or shifts the source <see cref="Slot"/> based on the destination <see cref="Slot"/> and calculates its new position.
/// Used for copy operations to predict where the source <see cref="Slot"/> will end up.
/// </summary>
/// <param name="source">The source <see cref="Slot"/>, from which we want to calculate where it lands..</param>
/// <param name="destination">The destination <see cref="Slot"/>, a reference point at which the copy or shift operation starts.</param>
/// <param name="sourceCapacity">The source <see cref="Chunk.Capacity"/>.</param>
/// <param name="destinationCapacity">The destination <see cref="Chunk.Capacity"/></param>
public static Slot Shift(in Slot source, int sourceCapacity, in Slot destination, int destinationCapacity)
{
var freeSpot = destination;
var resultSlot = source + freeSpot;
resultSlot.index += source.chunkIndex * (sourceCapacity - destinationCapacity);
resultSlot.Wrap(destinationCapacity);
return resultSlot;
}
}

View File

@@ -0,0 +1,93 @@
using System.Runtime.CompilerServices;
namespace Ghost.Entities;
[SkipLocalsInit]
public struct Entity : IEquatable<Entity>, IComparable<Entity>
{
private const EntityID _WORLD_INDEX_BITS = 4u;
private const EntityID _GENERATION_BITS = 8u;
private const EntityID _INDEX_BITS = sizeof(EntityID) * 8 - _WORLD_INDEX_BITS - _GENERATION_BITS;
private const EntityID _WORLD_INDEX_MASK = (1u << (int)_WORLD_INDEX_BITS) - 1;
private const EntityID _GENERATION_MASK = (1u << (int)_GENERATION_BITS) - 1;
private const EntityID _INDEX_MASK = (1u << (int)_INDEX_BITS) - 1;
private const EntityID _ID_MASK = EntityID.MaxValue;
private EntityID _id;
public readonly bool IsValid
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _id != _ID_MASK;
}
public readonly EntityID Index
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _id & _INDEX_MASK;
}
public readonly GenerationID Generation
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (GenerationID)(_id >> (int)_INDEX_BITS & _GENERATION_MASK);
}
public readonly WorldID WorldIndex
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (WorldID)(_id >> (int)(_INDEX_BITS + _GENERATION_BITS) & _WORLD_INDEX_MASK);
}
public void IncrementGeneration()
{
var generation = Generation + 1u;
if (generation >= _GENERATION_MASK)
{
throw new InvalidOperationException("Generation overflow");
}
_id = _id & ~(_GENERATION_MASK << (int)_INDEX_BITS) | generation << (int)_INDEX_BITS;
}
internal Entity(EntityID index, EntityID generation, EntityID worldIndex)
{
_id = worldIndex << (int)(_INDEX_BITS + _GENERATION_BITS) | generation << (int)_INDEX_BITS | index;
}
public readonly bool Equals(Entity other)
{
return _id == other._id;
}
public readonly int CompareTo(Entity other)
{
return _id.CompareTo(other._id);
}
public override readonly bool Equals(object? obj)
{
return obj is Entity other && Equals(other);
}
public override readonly int GetHashCode()
{
return _id.GetHashCode();
}
public static bool operator ==(Entity left, Entity right)
{
return left.Equals(right);
}
public static bool operator !=(Entity left, Entity right)
{
return !(left == right);
}
public override readonly string ToString()
{
return $"Entity {{ Index: {Index}, Generation: {Generation}, WorldIndex: {WorldIndex} }}";
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<Reference Include="Misaki.HighPerformance.Unsafe">
<HintPath>..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Unsafe\bin\Release\net9.0\Misaki.HighPerformance.Unsafe.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,604 @@
// Code from https://github.com/genaray/Arch/blob/master/src/Arch/Core/Utils/BitSet.cs
using Misaki.HighPerformance.Unsafe.Collections;
using Misaki.HighPerformance.Unsafe.Helpers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
namespace Ghost.Entities.Helpers;
// NOTE: Can this be replaced with `System.Collections.BitArray`?
// NOTE: If not, can it at least mirror that type's API?
/// <summary>
/// The <see cref="BitSet"/> class
/// represents a resizable collection of bits.
/// </summary>
public struct BitSet : IDisposable
{
private const int _BIT_SIZE = (sizeof(uint) * 8) - 1; // 31
private const int _INDEX_SIZE = 5; // log_2(BitSize + 1)
private static readonly int _padding = Vector<uint>.Count; // The padding used for vectorisation, the amount of uints required for being vectorized basically
/// <summary>
/// Determines the required length of an <see cref="BitSet"/> to hold the passed id or bit.
/// </summary>
/// <param name="id">The id or bit.</param>
/// <returns>A size of required <see cref="uint"/>s for the bitset.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int RequiredLength(int id)
{
return (id >> 5) + int.Sign(id & _BIT_SIZE);
}
/// <summary>
/// The bits from the bitset.
/// </summary>
private UnsafeArray<uint> _bits;
/// TODO: Update on ClearBit, however clearbit is only used in tests so its fine for now.
/// <summary>
/// The highest bit set.
/// </summary>
private int _highestBit;
/// TODO: Update on ClearBit, probably remove <see cref="_highestBit"/> in favor?
/// <summary>
/// The maximum <see cref="_bits"/>-index current in use.
/// </summary>
private int _max;
/// <summary>
/// Initializes a new instance of the <see cref="BitSet" /> class.
/// </summary>
public BitSet()
{
_bits = new UnsafeArray<uint>(_padding, Allocator.Persistent);
}
/// <summary>
/// Initializes a new instance of the <see cref="BitSet" /> class.
/// </summary>
public BitSet(params Span<uint> bits)
{
_bits = new UnsafeArray<uint>(bits.Length, Allocator.Persistent);
_bits.CopyFrom(bits);
}
/// <summary>
/// The highest uint index in use inside the <see cref="_bits"/>-array.
/// </summary>
public readonly int HighestIndex
{
get => _max;
}
/// <summary>
/// The highest bit set.
/// </summary>
public readonly int HighestBit
{
get => _highestBit;
}
/// <summary>
/// Returns the length of the bitset, how many int's it consists of.
/// </summary>
public readonly int Count
{
get => _bits.Count;
}
/// <summary>
/// Checks whether a bit is set at the index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>True if it is, otherwise false</returns>
public readonly bool IsSet(int index)
{
var b = index >> _INDEX_SIZE;
if (b >= _bits.Count)
{
return false;
}
return (_bits[b] & (1 << (index & _BIT_SIZE))) != 0;
}
/// <summary>
/// Sets a bit at the given index.
/// Resizes its internal array if necessary.
/// </summary>
/// <param name="index">The index.</param>
public void SetBit(int index)
{
var b = index >> _INDEX_SIZE;
if (b >= _bits.Count)
{
_bits.Resize((b + _padding) / _padding * _padding); // Round up to a multiply of Padding
}
// Track highest set bit
_highestBit = Math.Max(_highestBit, index);
_max = (_highestBit / (_BIT_SIZE + 1)) + 1;
_bits[b] |= 1u << (index & _BIT_SIZE);
}
/// <summary>
/// Clears the bit at the given index.
/// </summary>
/// <param name="index">The index.</param>
public readonly void ClearBit(int index)
{
var b = index >> _INDEX_SIZE;
if (b >= _bits.Count)
{
return;
}
_bits[b] &= ~(1u << (index & _BIT_SIZE));
}
/// <summary>
/// Sets all bits.
/// </summary>
public void SetAll()
{
var count = _bits.Count;
for (var i = 0; i < count; i++)
{
_bits[i] = 0xffffffff;
}
_highestBit = (_bits.Count * (_BIT_SIZE + 1)) - 1;
_max = (_highestBit / (_BIT_SIZE + 1)) + 1;
}
/// <summary>
/// Clears all set bits.
/// </summary>
public readonly void ClearAll()
{
_bits.Clear();
}
/// <summary>
/// Checks if all bits from this instance match those of the other instance.
/// </summary>
/// <param name="other">The other <see cref="BitSet"/>.</param>
/// <returns>True if they match, false if not.</returns>
[SkipLocalsInit]
public readonly bool All(BitSet other)
{
var min = Math.Min(Math.Min(Count, other.Count), _max);
if (!Vector.IsHardwareAccelerated || min < _padding)
{
// Bitwise and
for (var i = 0; i < min; i++)
{
var bit = _bits[i];
if ((bit & other._bits[i]) != bit)
{
return false;
}
}
// Handle extra bits on our side that might just be all zero.
for (var i = min; i < _max; i++)
{
if (_bits[i] != 0)
{
return false;
}
}
}
else
{
// Vectorized bitwise and
for (var i = 0; i < min; i += _padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
var otherVector = new Vector<uint>(other._bits.AsSpan()[i..]);
var resultVector = Vector.BitwiseAnd(vector, otherVector);
if (!Vector.EqualsAll(resultVector, vector))
{
return false;
}
}
// Handle extra bits on our side that might just be all zero.
for (var i = min; i < _max; i += _padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
if (!Vector.EqualsAll(vector, Vector<uint>.Zero)) // Vectors are not zero bits[0] != 0 basically
{
return false;
}
}
}
return true;
}
/// <summary>
/// Checks if any bits from this instance match those of the other instance.
/// </summary>
/// <param name="other">The other <see cref="BitSet"/>.</param>
/// <returns>True if they match, false if not.</returns>
public readonly bool Any(BitSet other)
{
var min = Math.Min(Math.Min(Count, other.Count), _max);
if (!Vector.IsHardwareAccelerated || min < _padding)
{
var bits = _bits.AsSpan();
var otherBits = other._bits.AsSpan();
// Bitwise and, return true since any is met
for (var i = 0; i < min; i++)
{
var bit = bits[i];
if ((bit & otherBits[i]) > 0)
{
return true;
}
}
// Handle extra bits on our side that might just be all zero.
for (var i = min; i < _max; i++)
{
if (bits[i] > 0)
{
return false;
}
}
}
else
{
// Vectorized bitwise and, return true since any is met
for (var i = 0; i < min; i += _padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
var otherVector = new Vector<uint>(other._bits.AsSpan()[i..]);
var resultVector = Vector.BitwiseAnd(vector, otherVector);
if (!Vector.EqualsAll(resultVector, Vector<uint>.Zero))
{
return true;
}
}
// Handle extra bits on our side that might just be all zero.
for (var i = min; i < _max; i += _padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
if (!Vector.EqualsAll(vector, Vector<uint>.Zero)) // Vectors are not zero bits[0] != 0 basically
{
return false;
}
}
}
return _highestBit <= 0;
}
/// <summary>
/// Checks if none bits from this instance match those of the other instance.
/// </summary>
/// <param name="other">The other <see cref="BitSet"/>.</param>
/// <returns>True if none match, false if not.</returns>
public readonly bool None(BitSet other)
{
var min = Math.Min(Math.Min(Count, other.Count), _max);
if (!Vector.IsHardwareAccelerated || min < _padding)
{
var bits = _bits.AsSpan();
var otherBits = other._bits.AsSpan();
// Bitwise and, return true since any is met
for (var i = 0; i < min; i++)
{
var bit = bits[i];
if ((bit & otherBits[i]) != 0)
{
return false;
}
}
}
else
{
// Vectorized bitwise and, return true since any is met
for (var i = 0; i < min; i += _padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
var otherVector = new Vector<uint>(other._bits.AsSpan()[i..]);
var resultVector = Vector.BitwiseAnd(vector, otherVector);
if (!Vector.EqualsAll(resultVector, Vector<uint>.Zero))
{
return false;
}
}
}
return true;
}
/// <summary>
/// Checks if exactly all bits from this instance match those of the other instance.
/// </summary>
/// <param name="other">The other <see cref="BitSet"/>.</param>
/// <returns>True if they match, false if not.</returns>
public readonly bool Exclusive(BitSet other)
{
var min = Math.Min(Math.Min(Count, other.Count), _max);
if (!Vector.IsHardwareAccelerated || min < _padding)
{
var bits = _bits.AsSpan();
var otherBits = other._bits.AsSpan();
// Bitwise xor, if both are not totally equal, return false
for (var i = 0; i < min; i++)
{
var bit = bits[i];
if ((bit ^ otherBits[i]) != 0)
{
return false;
}
}
// handle extra bits on our side that might just be all zero
for (var i = min; i < _max; i++)
{
if (bits[i] != 0)
{
return false;
}
}
}
else
{
// Vectorized bitwise xor, return true since any is met
for (var i = 0; i < min; i += _padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
var otherVector = new Vector<uint>(other._bits.AsSpan()[i..]);
var resultVector = Vector.Xor(vector, otherVector);
if (!Vector.EqualsAll(resultVector, Vector<uint>.Zero))
{
return false;
}
}
// Handle extra bits on our side that might just be all zero.
for (var i = min; i < _max; i += _padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
if (!Vector.EqualsAll(vector, Vector<uint>.Zero)) // Vectors are not zero bits[0] != 0 basically
{
return false;
}
}
}
return true;
}
/// <summary>
/// Creates a <see cref="Span{T}"/> to access the <see cref="_bits"/>.
/// </summary>
/// <returns>The hash.</returns>
public readonly Span<uint> AsSpan()
{
var max = (_highestBit / (_BIT_SIZE + 1)) + 1;
return _bits.AsSpan()[0..max];
}
/// <summary>
/// Copies the bits into a <see cref="Span{T}"/> and returns a slice containing the copied <see cref="_bits"/>.
/// </summary>
/// <param name="span">The <see cref="Span{T}"/> to copy into.</param>
/// <param name="zero">If true, it will zero the unused space from the <see cref="span"/>.</param>
/// <returns>The <see cref="Span{T}"/>.</returns>
public readonly Span<uint> AsSpan(Span<uint> span, bool zero = true)
{
// Copy everything that's possible from one to another
var length = Math.Min(Count, span.Length);
for (var index = 0; index < length; index++)
{
span[index] = _bits[index];
}
// Zero the rest space which was not overriden due to the copy.
for (var index = length; zero && index < span.Length; index++)
{
span[index] = 0;
}
return span[0..length];
}
/// <summary>
/// Calculates the hash, this is unique for the set bits. Two <see cref="BitSet"/> with the same set bits, result in the same hash.
/// </summary>
/// <returns>The hash.</returns>
public readonly override int GetHashCode()
{
return Component.GetHashCode(AsSpan());
}
/// <summary>
/// Prints the content of this instance.
/// </summary>
/// <returns>The string.</returns>
public readonly override string ToString()
{
// Convert uint to binary form for pretty printing
var binaryBuilder = new StringBuilder();
foreach (var bit in _bits)
{
binaryBuilder.Append(Convert.ToString((uint)bit, 2).PadLeft(32, '0')).Append(',');
}
binaryBuilder.Length--;
return $"{nameof(_bits)}: {binaryBuilder}, {nameof(Count)}: {Count}";
}
public void Dispose()
{
_bits.Dispose();
}
}
/// <summary>
/// The <see cref="SpanBitSet"/> struct
/// represents a non resizable collection of bits.
/// Used to set, check and clear bits on a allocated <see cref="BitSet"/> or on the stack.
/// </summary>
public readonly ref struct SpanBitSet
{
private const int _BIT_SIZE = (sizeof(uint) * 8) - 1; // 31
// NOTE: Is a byte not 8 bits?
private const int _BYTE_SIZE = 5; // log_2(BitSize + 1)
/// <summary>
/// The bits from the bitset.
/// </summary>
private readonly Span<uint> _bits;
/// <summary>
/// Initializes a new instance of the <see cref="BitSet" /> class.
/// </summary>
public SpanBitSet(Span<uint> bits)
{
_bits = bits;
}
/// <summary>
/// Checks whether a bit is set at the index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>True if it is, otherwise false</returns>
public bool IsSet(int index)
{
var b = index >> _BYTE_SIZE;
if (b >= _bits.Length)
{
return false;
}
return (_bits[b] & (1 << (index & _BIT_SIZE))) != 0;
}
/// <summary>
/// Sets a bit at the given index.
/// Resizes its internal array if necessary.
/// </summary>
/// <param name="index">The index.</param>
public void SetBit(int index)
{
var b = index >> _BYTE_SIZE;
if (b >= _bits.Length)
{
return;
}
_bits[b] |= 1u << (index & _BIT_SIZE);
}
/// <summary>
/// Clears the bit at the given index.
/// </summary>
/// <param name="index">The index.</param>
public void ClearBit(int index)
{
var b = index >> _BYTE_SIZE;
if (b >= _bits.Length)
{
return;
}
_bits[b] &= ~(1u << (index & _BIT_SIZE));
}
/// <summary>
/// Sets all bits.
/// </summary>
public void SetAll()
{
var count = _bits.Length;
for (var i = 0; i < count; i++)
{
_bits[i] = 0xffffffff;
}
}
/// <summary>
/// Clears all set bits.
/// </summary>
public void ClearAll()
{
_bits.Clear();
}
/// <summary>
/// Creates a <see cref="Span{T}"/> to access the <see cref="_bits"/>.
/// </summary>
/// <returns>The hash.</returns>
public Span<uint> AsSpan()
{
return _bits;
}
/// <summary>
/// Copies the bits into a <see cref="Span{T}"/> and returns a slice containing the copied <see cref="_bits"/>.
/// </summary>
/// <param name=""></param>
/// <returns>The hash.</returns>
public Span<uint> AsSpan(Span<uint> span, bool zero = true)
{
// Prevent exception because target array is to small for copy operation
var length = Math.Min(this._bits.Length, span.Length);
for (var index = 0; index < length; index++)
{
span[index] = _bits[index];
}
// Zero the rest space which was not overriden due to the copy.
for (var index = length; zero && index < span.Length; index++)
{
span[index] = 0;
}
return span[.._bits.Length];
}
/// <summary>
/// Calculates the hash, this is unique for the set bits. Two <see cref="BitSet"/> with the same set bits, result in the same hash.
/// </summary>
/// <returns>The hash.</returns>
public override int GetHashCode()
{
return Component.GetHashCode(AsSpan());
}
/// <summary>
/// Prints the content of this instance.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
// Convert uint to binary form for pretty printing
var binaryBuilder = new StringBuilder();
foreach (var bit in _bits)
{
binaryBuilder.Append(Convert.ToString((uint)bit, 2).PadLeft(32, '0')).Append(',');
}
binaryBuilder.Length--;
return $"{nameof(_bits)}: {string.Join(",", binaryBuilder)}";
}
}

View File

@@ -0,0 +1,7 @@
namespace Ghost.Entities.Helpers;
internal static class ThreadLocker
{
private static Lock? _worldLock;
public static Lock WorldLock => _worldLock ??= new();
}

View File

@@ -0,0 +1,22 @@
namespace Ghost.Entities.Registries;
internal static class ComponentRegistry
{
private static readonly Dictionary<Type, ComponentData> _hashCodeToComponentMap = new(64);
public static unsafe ComponentData GetOrAdd<T>()
where T : unmanaged, IComponent
{
var type = typeof(T);
if (_hashCodeToComponentMap.TryGetValue(type, out var data))
{
return data;
}
var id = (ushort)_hashCodeToComponentMap.Count;
data = new ComponentData(id, sizeof(T));
_hashCodeToComponentMap.Add(type, data);
return data;
}
}

View File

@@ -0,0 +1,6 @@
namespace Ghost.Entities.Services;
internal class EntityChangeQueue
{
// TODO: This class is not implemented yet.
}

View File

@@ -0,0 +1,53 @@
using Misaki.HighPerformance.Unsafe.Collections;
using Misaki.HighPerformance.Unsafe.Helpers;
namespace Ghost.Entities;
internal struct Signature : IDisposable, IEquatable<Signature>
{
public UnsafeArray<ComponentData> componentDatas;
private int _hashCode;
public Signature(params Span<ComponentData> components)
{
componentDatas = new UnsafeArray<ComponentData>(components.Length, Allocator.Persistent);
componentDatas.CopyFrom(components);
_hashCode = -1;
_hashCode = GetHashCode();
}
public bool Equals(Signature other)
{
return GetHashCode() == other.GetHashCode();
}
public override bool Equals(object? obj)
{
if (obj is Signature other)
{
return Equals(other);
}
return false;
}
public override int GetHashCode()
{
if (_hashCode != -1)
{
return _hashCode;
}
unchecked
{
_hashCode = Component.GetHashCode(componentDatas.AsSpan());
return _hashCode;
}
}
public void Dispose()
{
componentDatas.Dispose();
}
}