Skip to content

Commit

Permalink
ECS - recycle entity ids
Browse files Browse the repository at this point in the history
  • Loading branch information
friflo committed Jul 29, 2024
1 parent 06c1241 commit a3cca7b
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 25 deletions.
10 changes: 7 additions & 3 deletions src/ECS/Batch/StackArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ namespace Friflo.Engine.ECS;

internal struct StackArray<T>
{
#region properties
internal int Count => count;
public override string ToString() => $"Count: {count}";
#endregion

#region fields
private T[] items; // 8
private int count; // 4
#endregion

private T[] items;
private int count;

internal StackArray(T[] items) {
this.items = items;
}
Expand Down
5 changes: 4 additions & 1 deletion src/ECS/CommandBuffer/CommandBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,10 @@ public int CreateEntity()
if (intern.returnedBuffer) {
throw CannotReuseCommandBuffer();
}
var id = intern.store.NewIdInterlocked();
int id;
lock (intern.componentCommandTypes) {
id = intern.store.NewId();
}
var count = intern.entityCommandCount;

if (count == intern.entityCommands.Length) {
Expand Down
40 changes: 20 additions & 20 deletions src/ECS/Entity/Store/NodeTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Text;
using System.Threading;
using Friflo.Engine.ECS.Collections;
using Friflo.Engine.ECS.Index;
using static Friflo.Engine.ECS.NodeFlags;
Expand Down Expand Up @@ -456,9 +455,18 @@ protected internal override void UpdateEntityCompIndex(int id, int compIndex)
private void NewIds(int[] ids, int start, int count)
{
var localNodes = nodes;
int n = 0;
for (; n < count; n++)
{
if (!intern.recycleIds.TryPop(out int id)) {
break;
}
localNodes[id].flags = Created; // mark created. So id is not used twice by loop below
ids[n + start] = id;
}
int max = localNodes.Length;
int sequenceId = intern.sequenceId;
for (int n = 0; n < count; n++)
for (; n < count; n++)
{
for (; ++sequenceId < max;)
{
Expand All @@ -471,34 +479,25 @@ private void NewIds(int[] ids, int start, int count)
}
intern.sequenceId = sequenceId;
}
/// <summary> Note! Sync implementation with <see cref="NewIdInterlocked"/> and <see cref="NewIds"/>. </summary>

/// <summary> Note! Sync implementation with <see cref="NewIds"/>. </summary>
internal int NewId()
{
var localNodes = nodes;
int max = localNodes.Length;
int id = ++intern.sequenceId;
for (; id < max;)
var localNodes = nodes;
int id;
while (intern.recycleIds.TryPop(out id))
{
if ((localNodes[id].flags & Created) != 0) {
id = ++intern.sequenceId;
continue;
}
break;
return id;
}
return id;
}

/// <summary> Same as <see cref="NewId"/> but thread safe for <see cref="CommandBuffer"/>. </summary>
internal int NewIdInterlocked()
{
var localNodes = nodes;
int max = localNodes.Length;
int id = Interlocked.Increment(ref intern.sequenceId);
int max = localNodes.Length;
id = ++intern.sequenceId;
for (; id < max;)
{
if ((localNodes[id].flags & Created) != 0) {
id = Interlocked.Increment(ref intern.sequenceId);
id = ++intern.sequenceId;
continue;
}
break;
Expand All @@ -510,6 +509,7 @@ internal int NewIdInterlocked()
internal void DeleteNode(Entity entity)
{
int id = entity.Id;
intern.recycleIds.Push(id);
entityCount--;
ref var node = ref nodes[id];
if (node.isOwner != 0) {
Expand Down
2 changes: 2 additions & 0 deletions src/ECS/EntityStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ public sealed partial class EntityStore : EntityStoreBase
private struct Intern {
internal readonly PidType pidType; // 4 - pid != id / pid == id
internal int sequenceId; // 4 - incrementing id used for next new entity
internal StackArray<int> recycleIds; // 16 - contains id of deleted entities
//
internal SignalHandler[] signalHandlerMap; // 8
internal List<SignalHandler> signalHandlers; // 8
Expand All @@ -138,6 +139,7 @@ internal Intern(PidType pidType)
{
this.pidType = pidType;
sequenceId = Static.MinNodeId - 1;
recycleIds = new StackArray<int>(Array.Empty<int>());
signalHandlerMap = Array.Empty<SignalHandler>();
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/Tests/ECS/Entity/Test_Entity_Tree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,13 @@ public static void Test_Entity_Tree_DeleteEntity()
{
var store = new EntityStore(PidType.RandomPids);
var root = store.CreateEntity(1);
// create / delete to resize recycleIds buffer. Enable AssertNoAlloc() below
var child = store.CreateEntity(2);
child.DeleteEntity();

root.AddComponent(new EntityName("root"));
store.SetStoreRoot(root);
var child = store.CreateEntity(2);
child = store.CreateEntity(2);
var childPid = child.Pid;
AreEqual(2, store.PidToId(childPid));
AreEqual(childPid, store.IdToPid(2));
Expand Down
66 changes: 66 additions & 0 deletions src/Tests/ECS/Entity/Test_StructHeap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,72 @@ public static void Test_StructHeap_CreateEntities_with_pids()
Assert.AreEqual(count, type.Count);
}

[Test]
public static void Test_StructHeap_CreateEntity_RecycleIds()
{
var store = new EntityStore();
var entity1 = store.CreateEntity(1);
var entity2 = store.CreateEntity(2);
entity1.DeleteEntity();
entity2.DeleteEntity();

entity2 = store.CreateEntity();
entity1 = store.CreateEntity();

Assert.AreEqual(2, entity2.Id);
Assert.AreEqual(1, entity1.Id);

entity1.DeleteEntity();
entity2.DeleteEntity();

var type = store.GetArchetype(default);
var entities = type.CreateEntities(3);

Assert.AreEqual(3, entities.Count);
Assert.AreEqual("{ 2, 1, 3 }", entities.Debug()); // recycle: 2, 1 new id: 3

// --- cover case: recycled id is created manually
entities[0].DeleteEntity(); // id: 2
entity2 = store.CreateEntity(2);
Assert.AreEqual(2, entity2.Id);

var entity4 = store.CreateEntity();
Assert.AreEqual(4, entity4.Id);
}

[Test]
public static void Test_StructHeap_CreateEntity_RecycleIds_Perf()
{
int repeat = 10; // 1_000_000;
int count = 100;
// Entity count: 100, repeat: 1000000, duration: 4279 ms
var store = new EntityStore();
var entities = new Entity[count];
var sw = new Stopwatch();
sw.Start();
var start = 0L;
for (int i = 0; i < repeat; i++)
{
for (int n = 0; n < count; n++) {
entities[n] = store.CreateEntity();
}
for (int n = 0; n < count; n++) {
entities[n].DeleteEntity();
}
if (i == 0) {
start = Mem.GetAllocatedBytes();
} else {
Mem.AssertNoAlloc(start);
}
}
for (int n = 0; n < count; n++) {
Mem.AreEqual(count - n, entities[n].Id);
}
Console.WriteLine($"Entity count: {count}, repeat: {repeat}, duration: {sw.ElapsedMilliseconds} ms");
Mem.AreEqual(0, store.Count);
Mem.AreEqual(128, store.Capacity);
}

[Test]
public static void Test_StructHeap_CreateEntity_Perf()
{
Expand Down

0 comments on commit a3cca7b

Please sign in to comment.