Skip to content

Commit

Permalink
ECS - optimize Entity.AddComponent<>() / RemoveComponent<>()
Browse files Browse the repository at this point in the history
  • Loading branch information
friflo committed Aug 16, 2024
1 parent 2ec34ab commit b23d6cc
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 49 deletions.
4 changes: 2 additions & 2 deletions src/ECS/Archetype/EntityStore.Archetype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ internal bool TryGetValue(ArchetypeKey searchKey, out ArchetypeKey archetypeKey)
return archSet.TryGetValue(searchKey, out archetypeKey);
}

private static Archetype GetArchetypeWith(EntityStoreBase store, Archetype current, int structIndex)
internal static Archetype GetArchetypeWith(EntityStoreBase store, Archetype current, int structIndex)
{
var key = store.searchKey;
key.SetWith(current, structIndex);
Expand All @@ -89,7 +89,7 @@ private static Archetype GetArchetypeWith(EntityStoreBase store, Archetype curre
return archetype;
}

private static Archetype GetArchetypeWithout(EntityStoreBase store, Archetype archetype, int structIndex)
internal static Archetype GetArchetypeWithout(EntityStoreBase store, Archetype archetype, int structIndex)
{
var key = store.searchKey;
key.SetWithout(archetype, structIndex);
Expand Down
4 changes: 3 additions & 1 deletion src/ECS/Archetype/EntityStore.Mutation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace Friflo.Engine.ECS;

public partial class EntityStoreBase
{
/*
// ------------------------------------ add / remove component ------------------------------------
#region add / remove component
internal static bool AddComponent<T>(
Expand Down Expand Up @@ -99,7 +100,8 @@ internal static bool RemoveComponent<T>(
return true;
}
#endregion

*/

// ------------------------------------ add / remove entity Tag ------------------------------------
#region add / remove tags

Expand Down
9 changes: 4 additions & 5 deletions src/ECS/Archetype/EntityStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,10 @@ public abstract partial class EntityStoreBase
[Browse(Never)] internal int singleIndex; // 4
[Browse(Never)] internal bool shrinkArchetypes; // 1

private InternBase internBase; // 88
internal InternBase internBase; // 88
/// <summary>Contains state of <see cref="EntityStoreBase"/> not relevant for application development.</summary>
/// <remarks>Declaring internal state fields in this struct remove noise in debugger.</remarks>
// MUST be private by all means
private struct InternBase {
internal struct InternBase {
internal long archetypesCapacity; // 16 - sum of all Archetype capacities
internal double shrinkRatio; // 8
// --- delegates
Expand Down Expand Up @@ -191,13 +190,13 @@ internal static ArgumentException IdOutOfRangeException(EntityStore store, int i
return new ArgumentException($"id: {id}. expect in [0, current max id: {store.nodes.Length - 1}]");
}

private static ArgumentException AddRelationException(int id, int structIndex) {
internal static ArgumentException AddRelationException(int id, int structIndex) {
var componentType = Static.EntitySchema.components[structIndex];
var type = componentType.Name;
return new ArgumentException($"relation component must be added with: entity.{nameof(RelationExtensions.AddRelation)}(new {type}()); id: {id}");
}

private static ArgumentException RemoveRelationException(int id, int structIndex) {
internal static ArgumentException RemoveRelationException(int id, int structIndex) {
var componentType = Static.EntitySchema.components[structIndex];
var type = componentType.Name;
var keyType = componentType.RelationKeyType.Name;
Expand Down
21 changes: 3 additions & 18 deletions src/ECS/Base/Types/ComponentType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,31 +94,16 @@ internal ComponentType(string componentKey, int structIndex, Type indexType, Typ
}

internal override bool RemoveEntityComponent(Entity entity) {
int archIndex = 0;
ref var node = ref entity.store.nodes[entity.Id];
if (node.IsAlive(entity.Revision)) {
return EntityStoreBase.RemoveComponent<T>(entity.Id, ref node.archetype, ref node.compIndex, ref archIndex, StructIndex);
}
throw EntityStoreBase.EntityArgumentNullException(entity, nameof(entity));
return entity.RemoveComponent<T>();
}

internal override bool AddEntityComponent(Entity entity) {
int archIndex = 0;
ref var node = ref entity.store.nodes[entity.Id];
if (node.IsAlive(entity.Revision)) {
return EntityStoreBase.AddComponent<T>(entity.Id, StructIndex, ref node.archetype, ref node.compIndex, ref archIndex, default);
}
throw EntityStoreBase.EntityArgumentNullException(entity, nameof(entity));
return entity.AddComponent<T>(default);
}

internal override bool AddEntityComponentValue(Entity entity, object value) {
int archIndex = 0;
var componentValue = (T)value;
ref var node = ref entity.store.nodes[entity.Id];
if (node.IsAlive(entity.Revision)) {
return EntityStoreBase.AddComponent(entity.Id, StructIndex, ref node.archetype, ref node.compIndex, ref archIndex, componentValue);
}
throw EntityStoreBase.EntityArgumentNullException(entity, nameof(entity));
return entity.AddComponent(componentValue);
}

internal override StructHeap CreateHeap() {
Expand Down
13 changes: 5 additions & 8 deletions src/ECS/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ namespace Friflo.Engine.ECS;
/// </remarks>
[CLSCompliant(true)]
[StructLayout(LayoutKind.Explicit)]
public readonly struct Entity : IEquatable<Entity>
public readonly partial struct Entity : IEquatable<Entity>
{
// ------------------------------------ general properties ------------------------------------
#region general properties
Expand Down Expand Up @@ -424,13 +424,10 @@ public bool TryGetComponent<T>(out T result) where T : struct, IComponent
/// <returns>true - component is newly added to the entity.<br/> false - component is updated.</returns>
/// <remarks>Note: Use <see cref="EntityUtils.AddEntityComponent"/> as non generic alternative</remarks>
public bool AddComponent<T>() where T : struct, IComponent {
int archIndex = 0;
ref var node = ref store.nodes[Id];
if (node.IsAlive(Revision)) {
return EntityStoreBase.AddComponent<T>(Id, StructInfo<T>.Index, ref node.archetype, ref node.compIndex, ref archIndex, default);
}
throw EntityNullException();
return AddComponent<T>(default);
}

/*
/// <summary>
/// Add the given <paramref name="component"/> to the entity.<br/>
/// If the entity contains a component of the same type it is updated.<br/>
Expand Down Expand Up @@ -458,7 +455,7 @@ public bool RemoveComponent<T>() where T : struct, IComponent {
return EntityStoreBase.RemoveComponent<T>(Id, ref node.archetype, ref node.compIndex, ref archIndex, StructInfo<T>.Index);
}
throw EntityNullException();
}
}*/
#endregion


Expand Down
104 changes: 104 additions & 0 deletions src/ECS/Entity/Mutation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Ullrich Praetz - https://github.com/friflo. All rights reserved.
// See LICENSE file in the project root for full license information.

using Friflo.Engine.ECS.Index;

// ReSharper disable once CheckNamespace
namespace Friflo.Engine.ECS;

public readonly partial struct Entity
{
/// <summary>
/// Add the given <paramref name="component"/> to the entity.<br/>
/// If the entity contains a component of the same type it is updated.<br/>
/// See <a href="https://friflo.gitbook.io/friflo.engine.ecs/examples/general#component">Example.</a>
/// </summary>
/// <returns>true - component is newly added to the entity.<br/> false - component is updated.</returns>
public bool AddComponent<T>(in T component) where T : struct, IComponent
{
int id = Id;
var localStore = store;
ref var node = ref localStore.nodes[id];
var arch = node.archetype;
if (arch == null || Revision != node.revision) {
throw EntityNullException();
}
int structIndex = StructInfo<T>.Index;
ComponentChangedAction action;
bool added;
if (StructInfo<T>.IsRelation) {
throw EntityStoreBase.AddRelationException(id, structIndex);
}
int localCompIndex = node.compIndex;
var oldHeap = (StructHeap<T>)arch.heapMap[structIndex];
StructHeap<T> newHeap;
if (oldHeap != null) {
// --- case: archetype contains the component type => archetype remains unchanged
oldHeap.componentStash = oldHeap.components[localCompIndex];
added = false;
action = ComponentChangedAction.Update;
if (StructInfo<T>.HasIndex) StoreIndex.UpdateIndex(localStore, id, component, oldHeap);
newHeap = oldHeap;
goto AssignComponent;
}
// --- case: archetype doesn't contain component type => change entity archetype
var newArchetype = EntityStoreBase.GetArchetypeWith(localStore, arch, structIndex);
localCompIndex = Archetype.MoveEntityTo(arch, id, localCompIndex, newArchetype);
node.compIndex = localCompIndex;
node.archetype = arch = newArchetype;
added = true;
action = ComponentChangedAction.Add;
if (StructInfo<T>.HasIndex) StoreIndex.AddIndex(localStore, id, component);
newHeap = (StructHeap<T>)arch.heapMap[structIndex];

AssignComponent: // --- assign passed component value
newHeap.components[localCompIndex] = component;
// Send event. See: SEND_EVENT notes
var componentAdded = localStore.internBase.componentAdded;
if (componentAdded == null) {
return added;
}
componentAdded.Invoke(new ComponentChanged (localStore, id, action, structIndex, oldHeap));
return added;
}

/// <summary>Remove the component of the given type from the entity.</summary>
/// <returns>true if entity contained a component of the given type before</returns>
/// <remarks>
/// Executes in O(1)<br/>
/// <remarks>Note: Use <see cref="EntityUtils.RemoveEntityComponent"/> as non generic alternative</remarks>
/// </remarks>
public bool RemoveComponent<T>() where T : struct, IComponent
{
var id = Id;
int structIndex = StructInfo<T>.Index;
var localStore = store;
ref var node = ref localStore.nodes[id];
var arch = node.archetype;
if (arch == null || Revision != node.revision) {
throw EntityNullException();
}
if (StructInfo<T>.IsRelation) {
throw EntityStoreBase.RemoveRelationException(id, structIndex);
}
int localCompIndex = node.compIndex;
var heap = (StructHeap<T>)arch.heapMap[structIndex];
if (heap == null) {
return false;
}
heap.componentStash = heap.components[localCompIndex];
if (StructInfo<T>.HasIndex) StoreIndex.RemoveIndex(localStore, id, heap);
var newArchetype = EntityStoreBase.GetArchetypeWithout(localStore, arch, structIndex);

// --- change entity archetype
node.archetype = newArchetype;
node.compIndex = Archetype.MoveEntityTo(arch, id, localCompIndex, newArchetype);
// Send event. See: SEND_EVENT notes
var componentRemoved = localStore.internBase.componentRemoved;
if (componentRemoved == null) {
return true;
}
componentRemoved.Invoke(new ComponentChanged (localStore, id, ComponentChangedAction.Remove, structIndex, heap));
return true;
}
}
12 changes: 5 additions & 7 deletions src/ECS/RawEntity/RawEntityStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See LICENSE file in the project root for full license information.

using System;
using System.Diagnostics.CodeAnalysis;

// Hard rule: this file MUST NOT use type: Entity

Expand Down Expand Up @@ -135,21 +136,18 @@ public ref T GetEntityComponent<T>(int id)
return ref heap.components[entity.compIndex];
}

[ExcludeFromCodeCoverage]
public bool AddEntityComponent<T>(int id, in T component)
where T : struct, IComponent
{
ref var entity = ref entities[id];
var archetype = archs[entity.archIndex];
var structIndex = StructInfo<T>.Index;
return AddComponent(id, structIndex, ref archetype, ref entity.compIndex, ref entity.archIndex, component);
throw new NotImplementedException($"{nameof(RawEntityStore)} will be removed");
}

[ExcludeFromCodeCoverage]
public bool RemoveEntityComponent<T>(int id)
where T : struct, IComponent
{
ref var entity = ref entities[id];
var archetype = archs[entity.archIndex];
return RemoveComponent<T>(id, ref archetype, ref entity.compIndex, ref entity.archIndex, StructInfo<T>.Index);
throw new NotImplementedException($"{nameof(RawEntityStore)} will be removed");
}
#endregion

Expand Down
14 changes: 7 additions & 7 deletions src/Tests/ECS/Entity/Test_Entity_NRE.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,20 @@ public static void Test_Entity_NRE_Component()
var schema = EntityStore.GetEntitySchema();
var componentType = schema.ComponentTypeByType[typeof(Position)];

var ane = Throws<ArgumentException>(() => {
nre = Throws<NullReferenceException>(() => {
EntityUtils.RemoveEntityComponent(entity, componentType);
});
AreEqual(expectArg, ane!.Message);
AreEqual(expect, nre!.Message);

ane = Throws<ArgumentException>(() => {
nre = Throws<NullReferenceException>(() => {
EntityUtils.AddEntityComponent(entity, componentType);
});
AreEqual(expectArg, ane!.Message);
AreEqual(expect, nre!.Message);

ane = Throws<ArgumentException>(() => {
nre = Throws<NullReferenceException>(() => {
EntityUtils.AddEntityComponentValue(entity, componentType, new Position());
});
AreEqual(expectArg, ane!.Message);
AreEqual(expect, nre!.Message);

// --- tags
nre = Throws<NullReferenceException>(() => {
Expand All @@ -106,7 +106,7 @@ public static void Test_Entity_NRE_Component()
});
AreEqual(expect, nre!.Message);

ane = Throws<ArgumentException>(() => {
var ane = Throws<ArgumentException>(() => {
store.CloneEntity(entity);
});
AreEqual(expectArg, ane!.Message);
Expand Down
2 changes: 1 addition & 1 deletion src/Tests/ECS/Raw/Test_RawEntities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Tests.ECS.Raw {
public static class Test_RawEntities
{
/// <summary>Similar to <see cref="Test_StructComponent.Test_9_RemoveComponent"/></summary>
[Test]
// [Test]
public static void Test_RawEntities_Components()
{
var store = new RawEntityStore();
Expand Down

0 comments on commit b23d6cc

Please sign in to comment.