Arch.Extended
Arch.Extended copied to clipboard
Relationship tracking uses Entity instead of EntityReference
One issue I have ran into is that as relationships use plain old Entity for tracking instead of EntityReference, it is difficult to handle what happens with transient objects.
Consider the following example: 1- Create entities A and B. 2- Create a "TransformChild" relationship from A to B. 3- Delete B.
If blindly iterating the TransformChild relationships of A, you will hit an AccessViolation if not checking for IsAlive. Using IsAlive works to prevent the AVE, but causes a further problem shown in this pattern 1- Create entities A and B. 2- Create a "TransformChild" relationship from A to B. 3- Delete B. 3- Create entity C
Due to C recycling the ID for B, as far as the system is concerned C is now a child of A.
My solution to this has been to add the idea of "Reciprocal relationships" as well as a custom Destroy function which is aware of them. In my reciprocal relationships feature, it works like this 1- Create entities A and B. 2- Create a "TransformChild" relationship from A to B. A TransformParent relationship is created from B to A 3- Destroy B with custom destroy method.
That method looks like this
public static class EcsUtilities
{
public static void SafeDestroyEntity(Entity entity)
{
World world = World.Worlds[entity.WorldId];
CleanRelationship<TransformParent, TransformChild>(entity);
CleanRelationship<LogicalParent, LogicalChild>(entity);
CleanRelationship<Relationship_MaterialInstanceHost, Relationship_MaterialInstance>(entity);
CleanRelationship<ScriptAttachedEntity, EntityScript>(entity);
RemoveDependents<TransformChild>(world, entity);
RemoveDependents<EntityScript>(world, entity);
DetachDependents<Relationship_MaterialInstance>(entity);
world.Destroy(entity);
}
private static void DetachDependents<T>(Entity entity)
{
if (!entity.HasRelationship<T>())
{
return;
}
foreach (KeyValuePair<Entity, T> dependent in entity.GetRelationships<T>())
{
dependent.Key.RemoveRelationship<T>(entity);
}
}
[SkipLocalsInit]
private static void RemoveDependents<T>(World world, Entity entity)
{
if (!entity.HasRelationship<T>())
{
return;
}
Span<Entity> dependents = stackalloc Entity[16];
while (true)
{
if (!entity.HasRelationship<T>())
{
return;
}
var count = 0;
Relationship<T> relationship = entity.GetRelationships<T>();
foreach (KeyValuePair<Entity, T> dependent in relationship)
{
dependents[count++] = dependent.Key;
if (count == dependents.Length)
{
break;
}
}
if (count == 0)
{
break;
}
Cull(entity, dependents, count);
}
return;
static void Cull(Entity entity, Span<Entity> entities, int count)
{
for (var index = 0; index < count; index++)
{
Entity dependent = entities[index];
if (dependent.IsAlive())
{
entity.RemoveRelationship<T>(dependent);
SafeDestroyEntity(dependent);
}
else
{
entity.RemoveRelationship<T>(dependent);
}
}
}
}
private static void CleanRelationship<TOwn, TReciprocal>(Entity entity)
{
if (!entity.HasRelationship<TOwn>())
{
return;
}
foreach (KeyValuePair<Entity, TOwn> existingParent in entity.GetRelationships<TOwn>())
{
if (existingParent.Key.HasRelationship<TReciprocal>(entity))
{
existingParent.Key.RemoveRelationship<TReciprocal>(entity);
}
if (entity.HasRelationship<TOwn>(existingParent.Key))
{
entity.RemoveRelationship<TOwn>(existingParent.Key);
}
}
}
}
There are two things I think would be nice here to have this just work out of the box: 1- (Required) Use EntityReference for relationship tracking, not Entity. This avoids the two scenarios I listed above 2- (Nice to have) The concept of dependent relationships, and that if an entity is deleted any entity with a dependent relationship is also deleted. An example of this would be if a TransformParent is deleted, it's typical for the child entities to also be deleted.
With the move to using Entity as what tracks versioning, I feel like this can be marked as resolved now. Do you agree @genaray ?