-
Notifications
You must be signed in to change notification settings - Fork 51
Description
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.
Metadata
Metadata
Assignees
Labels
Projects
Status