Skip to content

Relationship tracking uses Entity instead of EntityReference #75

@ClxS

Description

@ClxS

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

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions