Unity DOTS: Attaching an Entity to Another

Summary

To attach the transform of an entity to another, it requires:

  • Parent Entity
    • LocalToWorld
  • Child Entity
    • LocalToWorld
    • LocalToParent
    • Parent

Then ParentSystem will do the rest of the work for you:

  • Parent Entity
    • LocalToWorld
    • + Child
  • Child Entity
    • LocalToWorld
    • LocalToParent
    • Parent
    • + PreviousParent

The transform components on the child entity, including Translation, Rotation and Scale, will be treated as local transform from now on.

On the other hand, assuming we want to treat these entities as a whole, for example, the children should also be destroyed once we destroy the parent, we need to add a DynamicBuffer<LinkedEntityGroup> on the parent entity containing elements like [ParentEntity, ChildEntity].

Environment

  • Unity 2020.1.5f1
  • Entities 0.14.0-preview.18

Explanation

The attachment relationship can be split into two aspects:

  • Transform (ParentSystem)
  • Instantiate, enable and destroy (LinkedEntityGroup)

ParentSystem

The whole attaching process of the transform is handled by ParentSystem.

1. Updating New Entities with Parent

To trigger the process, the child entity should have LocalToWorld, LocalToParent and Parent, as the query is:

m_NewParentsGroup = GetEntityQuery(new EntityQueryDesc
{
    All = new ComponentType[]
    {
        ComponentType.ReadOnly<Parent>(),
        ComponentType.ReadOnly<LocalToWorld>(),
        ComponentType.ReadOnly<LocalToParent>()
    },
    None = new ComponentType[]
    {
        typeof(PreviousParent)
    },
    Options = EntityQueryOptions.FilterWriteGroup
});

For these entities, ParentSystem adds a PreviousParent component to them.

void UpdateNewParents()
{
    if (m_NewParentsGroup.IsEmptyIgnoreFilter)
        return;

    EntityManager.AddComponent(m_NewParentsGroup, typeof(PreviousParent));
}

2. Gathering Changed Entities with Parent

Later, in the same frame, it queries for entities with an extra PreviousParent.

m_ExistingParentsGroup = GetEntityQuery(new EntityQueryDesc
{
    All = new ComponentType[]
    {
        ComponentType.ReadOnly<Parent>(),
        ComponentType.ReadOnly<LocalToWorld>(),
        ComponentType.ReadOnly<LocalToParent>(),
        typeof(PreviousParent)
    },
    Options = EntityQueryOptions.FilterWriteGroup
});
m_ExistingParentsGroup.SetChangedVersionFilter(typeof(Parent));

And gather the parent entities with their child entities into a NativeMultiHashMap<Entity, Entity> in GatherChangedParents job.

3. Updating Child Entities to Their Parent Entities

In FixupChangedChildren job, it takes the previously gathered information in the hash map and updating the DynamicBuffer<Child> on the parent entities.

public void Execute()
{
    var parents = UniqueParents.GetKeyArray(Allocator.Temp);
    for (int i = 0; i < parents.Length; i++)
    {
        var parent = parents[i];
        var children = ChildFromEntity[parent];

        RemoveChildrenFromParent(parent, children);
        AddChildrenToParent(parent, children);
    }
}
void AddChildrenToParent(Entity parent, DynamicBuffer<Child> children)
{
    if (ParentChildrenToAdd.TryGetFirstValue(parent, out var child, out var it))
    {
        do
        {
            children.Add(new Child() { Value = child });
        }
        while (ParentChildrenToAdd.TryGetNextValue(out child, ref it));
    }
}

LinkedEntityGroup

Even after setting up the transform attachment, when we destroy the parent entity, the child entity will still remain. Therefore, we need LinkedEntityGroup. The entities in the same LinkedEntityGroup will be instantiated, enabled and destroyed together when we operate on the root entity, which has a DynamicBuffer<LinkedEntityGroup> containing all entities in the hierarchy.

For example, aside from the transform attachment we’ve set up:

DynamicBuffer<LinkedEntityGroup> linkedEntities = EntityManager.AddBuffer<LinkedEntityGroup>(Parent);
linkedEntities.Add(new LinkedEntityGroup {Value = Parent});
linkedEntities.Add(new LinkedEntityGroup {Value = Child});

Note that the root entity must be placed at the first element.

According to the EntityComponentStore in the EntityComponentStoreCreateDestroyEntities.cs, it seems that it will ignore the buffer with only one element, also skip the first element.

void AddToDestroyList(Chunk* chunk, int indexInChunk, int batchCount, int inputDestroyCount,
    ref UnsafeList entitiesList, ref int minBufferLength, ref int maxBufferLength)
{
    int indexInArchetype = ChunkDataUtility.GetIndexInTypeArray(chunk->Archetype, m_LinkedGroupType);
    if (indexInArchetype != -1)
    {
        var baseHeader = ChunkDataUtility.GetComponentDataWithTypeRO(chunk, indexInChunk, m_LinkedGroupType);
        var stride = chunk->Archetype->SizeOfs[indexInArchetype];
        for (int i = 0; i != batchCount; i++)
        {
            var header = (BufferHeader*)(baseHeader + stride * i);

            var entityGroupCount = header->Length - 1;
            if (entityGroupCount <= 0)
                continue;

            var entityGroupArray = (Entity*)BufferHeader.GetElementPointer(header) + 1;

            if (entitiesList.Capacity == 0)
                entitiesList.SetCapacity<Entity>(inputDestroyCount * entityGroupCount /*, Allocator.TempJob*/);
            entitiesList.AddRange<Entity>(entityGroupArray, entityGroupCount /*, Allocator.TempJob*/);

            minBufferLength = math.min(minBufferLength, entityGroupCount);
            maxBufferLength = math.max(maxBufferLength, entityGroupCount);
        }
    }
}

Reference

Leave a Comment