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);
}
}
}
Leave a Comment