Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Relationship tracking uses Entity instead of EntityReference #75

Open
ClxS opened this issue Sep 11, 2024 · 0 comments
Open

Relationship tracking uses Entity instead of EntityReference #75

ClxS opened this issue Sep 11, 2024 · 0 comments
Labels
enhancement New feature or request

Comments

@ClxS
Copy link

ClxS commented Sep 11, 2024

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.

@genaray genaray added the enhancement New feature or request label Sep 17, 2024
@genaray genaray moved this to Todo in Arch Roadmap Sep 17, 2024
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
enhancement New feature or request
Projects
Status: Todo
Development

No branches or pull requests

2 participants