Skip to content

Commit 8b4790e

Browse files
Merge pull request #3 from JasonXuDeveloper/claude/ecs-framework-multicore-011CUoqcaWaToLFchjwugUUG
Build ECS framework for multi-core servers
2 parents 54be878 + 1cf99b6 commit 8b4790e

37 files changed

Lines changed: 3680 additions & 2025 deletions

EasyEcs.Core/Archetype.cs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
using System;
2+
using System.Runtime.CompilerServices;
3+
using EasyEcs.Core.Components;
4+
5+
namespace EasyEcs.Core;
6+
7+
/// <summary>
8+
/// Archetype stores entities with the same component configuration.
9+
/// Uses tombstone pattern (-1) for safe iteration during modifications.
10+
/// </summary>
11+
internal class Archetype
12+
{
13+
public Tag ComponentMask;
14+
public int[] EntityIds;
15+
public int Count; // Total slots (including tombstones)
16+
public int AliveCount; // Living entities only
17+
18+
private const int Tombstone = -1;
19+
20+
public Archetype(in Tag componentMask, int initialCapacity = 1024)
21+
{
22+
ComponentMask = componentMask;
23+
EntityIds = new int[initialCapacity];
24+
Count = 0;
25+
AliveCount = 0;
26+
}
27+
28+
/// <summary>
29+
/// Add an entity to this archetype.
30+
/// Reuses tombstone slots first, then appends.
31+
/// </summary>
32+
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
33+
public void Add(int entityId)
34+
{
35+
// Try to find a tombstone slot for reuse
36+
var span = EntityIds.AsSpan(0, Count);
37+
int emptySlot = span.IndexOf(Tombstone);
38+
39+
if (emptySlot >= 0)
40+
{
41+
// Reuse tombstone slot
42+
EntityIds[emptySlot] = entityId;
43+
AliveCount++;
44+
}
45+
else
46+
{
47+
// No tombstones available, append to end
48+
if (Count >= EntityIds.Length)
49+
{
50+
// Grow array (doubling strategy)
51+
Array.Resize(ref EntityIds, EntityIds.Length * 2);
52+
}
53+
54+
EntityIds[Count++] = entityId;
55+
AliveCount++;
56+
}
57+
}
58+
59+
/// <summary>
60+
/// Remove an entity from this archetype.
61+
/// Marks the slot as tombstone (-1) for safe iteration.
62+
/// </summary>
63+
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
64+
public void Remove(int entityId)
65+
{
66+
// Find and tombstone the entity
67+
var span = EntityIds.AsSpan(0, Count);
68+
int idx = span.IndexOf(entityId);
69+
70+
if (idx >= 0)
71+
{
72+
EntityIds[idx] = Tombstone;
73+
AliveCount--;
74+
75+
// Optional: Compact if heavily fragmented (>50% tombstones)
76+
// This prevents memory waste while keeping iteration safe
77+
if (AliveCount > 0 && AliveCount < Count / 2)
78+
{
79+
Compact();
80+
}
81+
}
82+
}
83+
84+
/// <summary>
85+
/// Compact the array by removing all tombstones.
86+
/// Only called when heavily fragmented (>50% dead slots).
87+
/// </summary>
88+
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
89+
public void Compact()
90+
{
91+
int writeIdx = 0;
92+
var span = EntityIds.AsSpan(0, Count);
93+
94+
for (int readIdx = 0; readIdx < Count; readIdx++)
95+
{
96+
if (span[readIdx] != Tombstone)
97+
{
98+
EntityIds[writeIdx++] = EntityIds[readIdx];
99+
}
100+
}
101+
102+
Count = writeIdx;
103+
// AliveCount should already match Count after compaction
104+
}
105+
106+
/// <summary>
107+
/// Get a read-only span of entity IDs for iteration.
108+
/// Includes tombstones - iterator must skip -1 values.
109+
/// </summary>
110+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
111+
public ReadOnlySpan<int> GetEntitySpan() => new ReadOnlySpan<int>(EntityIds, 0, Count);
112+
113+
/// <summary>
114+
/// Get fragmentation ratio (percentage of tombstones).
115+
/// </summary>
116+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
117+
public float GetFragmentation()
118+
{
119+
return Count > 0 ? 1.0f - (float)AliveCount / Count : 0f;
120+
}
121+
}

EasyEcs.Core/Commands/AddComponentCommand.cs

Lines changed: 0 additions & 23 deletions
This file was deleted.

EasyEcs.Core/Commands/AddSystemCommand.cs

Lines changed: 0 additions & 13 deletions
This file was deleted.

EasyEcs.Core/Commands/CommandBuffer.cs

Lines changed: 0 additions & 18 deletions
This file was deleted.

EasyEcs.Core/Commands/CreateEntityCommand.cs

Lines changed: 0 additions & 18 deletions
This file was deleted.

EasyEcs.Core/Commands/DeleteEntityCommand.cs

Lines changed: 0 additions & 16 deletions
This file was deleted.

EasyEcs.Core/Commands/ICommand.cs

Lines changed: 0 additions & 5 deletions
This file was deleted.

EasyEcs.Core/Commands/RemoveComponentCommand.cs

Lines changed: 0 additions & 17 deletions
This file was deleted.

EasyEcs.Core/Commands/RemoveSystemCommand.cs

Lines changed: 0 additions & 13 deletions
This file was deleted.

EasyEcs.Core/Components/ComponentRef.cs

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,83 @@
1+
using System;
2+
using System.Runtime.CompilerServices;
3+
using System.Runtime.InteropServices;
4+
15
namespace EasyEcs.Core.Components;
26

7+
/// <summary>
8+
/// Optimized component reference with packed ID+version and Unsafe access.
9+
/// 24 bytes total (down from 32+).
10+
/// </summary>
311
public readonly struct ComponentRef<T> where T : struct, IComponent
412
{
5-
private readonly int _index;
6-
private readonly byte _bitIndex;
13+
// Pack entity ID (lower 32 bits) + version (upper 32 bits)
14+
private readonly long _packed;
15+
private readonly ushort _componentIndex; // Supports up to 65536 component types
716
private readonly Context _context;
817

9-
public ComponentRef(int index, byte bitIndex, Context context)
18+
public ComponentRef(int entityId, int version, ushort componentIndex, Context context)
1019
{
11-
_index = index;
12-
_bitIndex = bitIndex;
20+
_packed = ((long)version << 32) | (uint)entityId;
21+
_componentIndex = componentIndex;
1322
_context = context;
1423
}
1524

16-
public ref T Value => ref ((T[])_context.Components[_bitIndex])[_index];
25+
private int EntityId
26+
{
27+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
28+
get => (int)_packed;
29+
}
30+
31+
private int Version
32+
{
33+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
34+
get => (int)(_packed >> 32);
35+
}
36+
37+
public ref T Value
38+
{
39+
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
40+
get
41+
{
42+
int id = EntityId;
43+
int version = Version;
44+
45+
// Validate version using Unsafe (zero bounds checks)
46+
ref int actualVersion = ref Unsafe.Add(
47+
ref MemoryMarshal.GetArrayDataReference(_context.EntityVersions),
48+
id);
49+
50+
if (actualVersion != version)
51+
ThrowEntityDestroyed();
52+
53+
// Lazy initialization: ensure component array exists
54+
// This handles cases where TagRegistry static indices are shared across contexts
55+
T[] componentArray = EnsureComponentArray();
56+
57+
return ref Unsafe.Add(
58+
ref MemoryMarshal.GetArrayDataReference(componentArray),
59+
id);
60+
}
61+
}
62+
63+
[MethodImpl(MethodImplOptions.NoInlining)]
64+
private T[] EnsureComponentArray()
65+
{
66+
// Fast path: component array already initialized
67+
if (_context.Components != null &&
68+
_componentIndex < _context.Components.Length &&
69+
_context.Components[_componentIndex] != null)
70+
{
71+
return Unsafe.As<T[]>(_context.Components[_componentIndex]);
72+
}
73+
74+
// Slow path: need to initialize (happens when static TypeBitIndex is shared across contexts)
75+
return _context.EnsureComponentArrayInitialized<T>(_componentIndex);
76+
}
77+
78+
[MethodImpl(MethodImplOptions.NoInlining)]
79+
private static void ThrowEntityDestroyed() =>
80+
throw new InvalidOperationException("Entity has been destroyed");
1781

1882
public static implicit operator T(ComponentRef<T> componentRef)
1983
{

0 commit comments

Comments
 (0)