Skip to content
New issue

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

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

Already on GitHub? Sign in 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Status: Todo
Development

No branches or pull requests

2 participants