Search Unity

Patterns for groups of parent components each with groups of children?

Discussion in 'Entity Component System' started by Piefayth, Nov 17, 2018.

  1. Piefayth

    Piefayth

    Joined:
    Feb 7, 2017
    Posts:
    61
    For example, I have a Parent component type, and it is responsible for any number of Child component types. I often want to do things like, "for every parent -> do work on that parent's children." In my systems, this often results in me requesting two ComponentGroups: all typeof(Parent), and all typeof(Child).

    Within such a system, the resultant ComponentDataArrays and EntityArrays do not guarantee any particular order. By default, I do not know which EntityArray index represents the parent for a given child, even when the child stores the parent's Entity. For me to say, run a job on all Child, I would need to be able to provide that job with which Parent is associated with the Child currently being executed upon. I need to provide the job an index of Parent Entity-> ParentComponentDataIndex to avoid looping through the Parent EntityArray in every run of Execute. I have a system that creates and maintains that index with a NativeHashMap.

    Often, Child components need to know about other children associated with the same Parent, but the Parent cannot keep a collection of Child on its ComponentData and remain unshared. In my searching, I saw the recommendation to have a system responsible for maintaining a Parent -> Child index with a NativeArray. Unfortunately, this only works for a single Parent; once you have multiple parents, you need a 2D collection or a clever way to index multiple sets in a 1D collection. I would like to keep most of this work jobified, so non-native collections are less than ideal. NativeMultiHashMap exists, but at the worst case of iterating every child for a given parent to find a specific child. My children have a specific order based on their position, so there is no reason to ever have to iterate all of them to find a specific one.

    I realized I could add a DynamicBuffer of ordered children to each Parent entity and have a system responsible for maintaining the buffer. It, however, is not clear to me what data exactly I would store in the buffer. I cannot store a reference to the Child's ComponentData directly, as I must store an IBufferElementData. I could store a reference to the Child Entity, but when processing a job of Children I would not have a way to know which ComponentDataArray<Child> index contains the information for a given child except the one currently being processed. Before scheduling a job on all children, I would need to create an index of ComponentData(Child)Index -> BufferElementIndex.

    To sum it all up to a question, what is the best way to manage groups of components that each "parent" an arbitrary number of ordered child components that require lookups by index / from siblings?

    I am pretty new to ECS, so please excuse me if I am misunderstanding or overlooking some functionality.
     
    Last edited: Nov 17, 2018
  2. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    Can you add a SharedComponent of parent to the children?
    This would allow you to get all children for a parent, and also the children would then automatically be grouped as siblings.
    This might also work even with two parents if you come up with a unique key of both of them or use two SharedComponents. Depends what your data looks like.

    Alternatively if you store a reference to the parent entity in the child, then you can use ComponentDataFromEntity to lookup components on the parent. This also works in jobs but I'm not sure what the performance cost of this is.
     
  3. Piefayth

    Piefayth

    Joined:
    Feb 7, 2017
    Posts:
    61
    Yeah, I think so, and I'm gonna try this. It crossed my mind this was possible, but something just didn't seem right about having entities of Parent and entities of Child, Parent. It does make perfect sense to use a SharedComponent for such a purpose, though. Also, am I only storing the Entity reference in the collection on the SharedComponent? If that's the case, I run right back into the problem of needing to make an index of ChildEntity -> ComponentDataArrayIndex<Child> Index before scheduling a job that operates on every child. Makes me again wonder what the performance of ComponentDataFromEntity is like.

    I would go this direction if I had some confirmation that the cost was insignificant; I just assumed it was not.


    Edit: Additionally, it seems like it'd be useful to be able to modify a parent's list of children in paralell. Just as a quick example, when a parent is created it has NO children, but afterwards I would like to add tons of children simultaneously, knowing the desired index of each child in advance. In the case of SharedComponent, I can't parallelize that as far as I know.
     
    Last edited: Nov 17, 2018
  4. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    It really depends on the makeup of your parent/child relationship. Is it one/two/more parents to hundreds/thousands of children or hundreds of parents with a few children etc.
    Do the children already exist and you're attaching them to parents or are you spawning new children to add to parents? What kind of data do you need to accumulate/pass between parent/child/sibling?

    Without knowing the details, I'd go around in circles with all the options but ComponentDataFromEntity and SharedComponent are your two main tools.
    Processing parents/children seperately and writing to intermmediate Native Arrays/Hashmaps might also be faster than ComponentDataFromEntity.

    I don't quite get this bit sorry. With SharedComponent, parents and children only know about each other via that single SharedComponent. You can still operate on all children in a job irrespective of their SharedComponents.

    Ah I missed this from your first post. Probably why I'm confused. :) In what way are they ordered? Do you mean Entity order within chunks or are you referring to something else?
     
  5. Floofloof

    Floofloof

    Joined:
    Nov 21, 2016
    Posts:
    41
    Have you tried going the ChunkArchetype route?
    I think this might solve your problem as you would be able to get all of the entities with the Parent and then check that entity for its children.
    Maybe this page might help: https://github.com/ErikMoczi/packag...ty.Entities.Tests/ArchetypeChunkArrayTests.cs



    Code (CSharp):
    1.        [Test]
    2.         public void ACS_WriteMixed()
    3.         {
    4.             CreateMixedEntities(64);
    5.  
    6.             var query = new EntityArchetypeQuery
    7.             {
    8.                 Any = new ComponentType[] {typeof(EcsTestData2), typeof(EcsTestData)}, // any
    9.                 None = Array.Empty<ComponentType>(), // none
    10.                 All = Array.Empty<ComponentType>(), // all
    11.             };
    12.             var group = m_Manager.CreateComponentGroup(query);
    13.             var chunks = group.CreateArchetypeChunkArray(Allocator.TempJob);
    14.             group.Dispose();
    15.  
    16.             Assert.AreEqual(14,chunks.Length);
    17.  
    18.             var ecsTestData = m_Manager.GetArchetypeChunkComponentType<EcsTestData>(false);
    19.             var ecsTestData2 = m_Manager.GetArchetypeChunkComponentType<EcsTestData2>(false);
    20.             var changeValuesJobs = new ChangeMixedValues
    21.             {
    22.                 chunks = chunks,
    23.                 ecsTestData = ecsTestData,
    24.                 ecsTestData2 = ecsTestData2,
    25.             };
    26.  
    27.             var collectValuesJobHandle = changeValuesJobs.Schedule(chunks.Length, 64);
    28.             collectValuesJobHandle.Complete();
    29.  
    30.             ulong foundValues = 0;
    31.             for (int chunkIndex = 0; chunkIndex < chunks.Length; chunkIndex++)
    32.             {
    33.                 var chunk = chunks[chunkIndex];
    34.                 var chunkCount = chunk.Count;
    35.  
    36.                 Assert.AreEqual(4,math.ceilpow2(chunkCount-1));
    37.  
    38.                 var chunkEcsTestData = chunk.GetNativeArray(ecsTestData);
    39.                 var chunkEcsTestData2 = chunk.GetNativeArray(ecsTestData2);
    40.                 if (chunkEcsTestData.Length > 0)
    41.                 {
    42.                     for (int i = 0; i < chunkCount; i++)
    43.                     {
    44.                         foundValues |= (ulong)1 << (chunkEcsTestData[i].value-100);
    45.                     }
    46.                 }
    47.                 else if (chunkEcsTestData2.Length > 0)
    48.                 {
    49.                     for (int i = 0; i < chunkCount; i++)
    50.                     {
    51.                         foundValues |= (ulong)1 << (-chunkEcsTestData2[i].value0-1000);
    52.                     }
    53.                 }
    54.             }
    55.  
    56.             foundValues++;
    57.             Assert.AreEqual(0,foundValues);
    58.  
    59.             chunks.Dispose();
    60.         }
     
  6. Piefayth

    Piefayth

    Joined:
    Feb 7, 2017
    Posts:
    61
    After messing around some, it seems using the SharedComponent Parent as an actual SharedComponent was indeed the right way to go, and I managed to eliminate a bunch of needless code. Pretty cool.

    But I also managed to narrow down my actual source of confusion. Let's say I'm making something like, "People Waiting in Line Simulator" and a lot of my gameplay logic relies on relative position in line. If a person sees a friend of theirs 3 positions or less away, maybe they're happier. If the person next to them doesn't smell good, maybe they're sadder. To facilitate this, I would like to keep an ordered collection of people stored somewhere on the Line. This way, when running parallel job on all people, I can have references to other relatively located people. We need some index of <int(linePosition), Entity> for each Line that we can update by iterating the People who may have had their line position changed.

    I can't figure out how to make and maintain this collection representing the ordered line. Let's say it's a NativeArray<Entity> sized at some LineCapacity on the Line SharedComponent, and that array is allocated when the Line is created. Each Person already knows their position in line, I just need to iterate them and get a reference to each Person Entity for each Line component. I would like to modify the individual elements of the NativeArray<Entity> on the shared component in parallel, but I cannot do so directly; I need to copy it out, modify it in the parallel job, then re-set the component data. This ultimately means allocating a new NativeArray<Entity> to represent this index every time it updates! In a fast-paced Line simulation, those updates could happen pretty frequently, so this isn't desirable. I thought I might be able to use a DynamicBuffer as a scratch pad by copying the NativeArray<Entity> from the Line to the buffer, then allowing the job to overwrite the indices in the buffer in parallel, but it's not valid to do so directly because Entity is not an IBufferElementData


    Edit: So, it seems like what I'm describing above is possible? Here is a complete, runnable example of what I'm trying to do. Drop it into any new Unity project with Entities installed and hit play. It contains 3 systems: 1 to bootstrap each parent and allocate initial storage for the children + set the children's indexes in the parent, 1 to verify the result, and 1 to create the index and copy the result back into shared data. 2 jobs: 1 to extract the childrens' indices in parallel to the buffer, 1 to update the native array in the shared component to the buffer contents. My biggest gripe is having to re-establish the "parentEntityToParentBuffer" index every update, but I think I can just jobify that. Curious what others think of the approach, given my requirements.

    Double Edit: I see my mistake now. I need to track references to the NativeArray so when I change it it changes not only the data on the parent entity directly, but all children as well. Won't update below code to reflect that, but this will let me get the kind of behavior I actually expect. Good stuff. Thanks all!

    Code:
    Code (CSharp):
    1.  
    2. using Unity.Jobs;
    3. using Unity.Entities;
    4. using Unity.Collections;
    5.  
    6. public struct ChildBufferElement : IBufferElementData {
    7.     public Entity entity;
    8. }
    9.  
    10. public struct Parent : ISharedComponentData {
    11.     public NativeArray<ChildBufferElement> childrenIndex;
    12.     public int capacity;
    13. }
    14.  
    15. public struct Child : IComponentData {
    16.     public int index;
    17.     public Entity parent;
    18. }
    19.  
    20. public class ParentIndexUpdateBarrier : BarrierSystem { }
    21.  
    22. [UpdateAfter(typeof(Boot))]
    23. public class StandaloneExampleSystem : JobComponentSystem {
    24.     [Inject] ParentIndexUpdateBarrier barrier;
    25.  
    26.     ComponentGroup childGroup;
    27.     ComponentGroup parentGroup;
    28.     EntityManager entityManager;
    29.     NativeHashMap<Entity, int> parentEntityToParentBuffer;
    30.  
    31.     bool initialized = false;
    32.  
    33.     protected override JobHandle OnUpdate(JobHandle inputDeps) {
    34.         if (initialized) {
    35.             return inputDeps;
    36.         }
    37.  
    38.         Initialize();
    39.  
    40.         BufferArray<ChildBufferElement> parentBuffers = parentGroup.GetBufferArray<ChildBufferElement>();
    41.         SharedComponentDataArray<Parent> parents = parentGroup.GetSharedComponentDataArray<Parent>();
    42.         EntityArray parentEntities = parentGroup.GetEntityArray();
    43.  
    44.         EntityArray childEntities = childGroup.GetEntityArray();
    45.         ComponentDataArray<Child> children = childGroup.GetComponentDataArray<Child>();
    46.  
    47.         parentEntityToParentBuffer.Clear();
    48.  
    49.         for (int i = 0; i < parents.Length; i++) {
    50.             parentEntityToParentBuffer.TryAdd(parentEntities[i], i);
    51.             while (parentBuffers[i].Length < parents[i].capacity) {
    52.                 parentBuffers[i].Add(new ChildBufferElement { });
    53.             }
    54.         }
    55.  
    56.         IndexChildrenJob job = new IndexChildrenJob {
    57.             parentBuffers = parentBuffers,
    58.             parentEntityToParentBuffer = parentEntityToParentBuffer,
    59.             children = children,
    60.             childEntities = childEntities,
    61.         };
    62.  
    63.         JobHandle icjHandle = job.Schedule(children.Length, 64, inputDeps);
    64.  
    65.         JobHandle upcHandle = new UpdateParentComponentJob {
    66.             commandBuffer = barrier.CreateCommandBuffer().ToConcurrent(),
    67.             parentBuffers = parentBuffers,
    68.             parentEntities = parentEntities,
    69.         }.Schedule(parentEntities.Length, 64, icjHandle);
    70.  
    71.         initialized = true;
    72.         return upcHandle;
    73.     }
    74.  
    75.     void Initialize() {
    76.         entityManager = World.GetOrCreateManager<EntityManager>();
    77.         childGroup = entityManager.CreateComponentGroup(typeof(Child), typeof(Parent));
    78.         parentGroup = entityManager.CreateComponentGroup(typeof(Parent), typeof(ChildBufferElement));
    79.  
    80.         int maxParents = 100;
    81.         parentEntityToParentBuffer = new NativeHashMap<Entity, int>(maxParents, Allocator.Persistent);
    82.     }
    83.  
    84.     protected override void OnDestroyManager() {
    85.         parentEntityToParentBuffer.Dispose();
    86.     }
    87. }
    88.  
    89. public struct UpdateParentComponentJob : IJobParallelFor {
    90.     public EntityCommandBuffer.Concurrent commandBuffer;
    91.  
    92.     [ReadOnly]
    93.     public BufferArray<ChildBufferElement> parentBuffers;
    94.  
    95.     [ReadOnly]
    96.     public EntityArray parentEntities;
    97.  
    98.     public void Execute(int jobIndex) {
    99.         commandBuffer.SetSharedComponent(jobIndex, parentEntities[jobIndex], new Parent {
    100.             childrenIndex = parentBuffers[jobIndex].ToNativeArray(),
    101.         });
    102.     }
    103. }
    104.  
    105. public struct IndexChildrenJob : IJobParallelFor {
    106.     [NativeDisableParallelForRestriction]
    107.     public BufferArray<ChildBufferElement> parentBuffers;
    108.  
    109.     [ReadOnly]
    110.     public NativeHashMap<Entity, int> parentEntityToParentBuffer;
    111.  
    112.     [ReadOnly]
    113.     public ComponentDataArray<Child> children;
    114.  
    115.     [ReadOnly]
    116.     public EntityArray childEntities;
    117.  
    118.     public void Execute(int jobIndex) {
    119.         parentEntityToParentBuffer.TryGetValue(children[jobIndex].parent, out int bufIndex);
    120.         DynamicBuffer <ChildBufferElement> buf = parentBuffers[bufIndex];
    121.  
    122.         buf[children[jobIndex].index] = new ChildBufferElement {
    123.             entity = childEntities[jobIndex],
    124.         };
    125.     }
    126. }
    127.  
    128. [UpdateAfter(typeof(ParentIndexUpdateBarrier))]
    129. public class CheckIfItWorkedSystem : ComponentSystem {
    130.     bool initialized = false;
    131.  
    132.     protected override void OnUpdate() {
    133.         if (initialized) {
    134.             return;
    135.         }
    136.  
    137.         EntityManager entityManager = World.GetOrCreateManager<EntityManager>();
    138.         ComponentGroup parentGroup = entityManager.CreateComponentGroup(typeof(Parent), typeof(ChildBufferElement));
    139.  
    140.         SharedComponentDataArray<Parent> parents = parentGroup.GetSharedComponentDataArray<Parent>();
    141.         BufferArray<ChildBufferElement> parentBuffers = parentGroup.GetBufferArray<ChildBufferElement>();
    142.  
    143.         UnityEngine.Debug.Log(parentBuffers[0][5].entity); // entities were stored in order in parent buffer
    144.         UnityEngine.Debug.Log(parentBuffers[1][5].entity);
    145.  
    146.         UnityEngine.Debug.Log(parents[0].childrenIndex[5].entity); // and copied back to the native array
    147.         UnityEngine.Debug.Log(parents[1].childrenIndex[5].entity);
    148.  
    149.         initialized = true;
    150.     }
    151. }
    152.  
    153. public class Boot : ComponentSystem {
    154.     EntityManager entityManager;
    155.     NativeArray<Entity> parentEntities;
    156.     int numParents = 2;
    157.     bool initialized = false;
    158.  
    159.     protected override void OnUpdate() {
    160.         if (initialized) {
    161.             return;
    162.         }
    163.  
    164.         parentEntities = new NativeArray<Entity>(numParents, Allocator.Persistent);
    165.         entityManager = World.GetOrCreateManager<EntityManager>();
    166.  
    167.         for (int i = 0; i < parentEntities.Length; i++) {
    168.             parentEntities[i] = entityManager.CreateEntity(typeof(Parent), typeof(ChildBufferElement));
    169.  
    170.             Parent parent = new Parent {
    171.                 capacity = 100,
    172.             };
    173.  
    174.             entityManager.SetSharedComponentData(parentEntities[i], parent);
    175.  
    176.             Entity e;
    177.             for (int j = 0; j < 10; j++) {
    178.                 e = entityManager.CreateEntity(typeof(Child), typeof(Parent));
    179.                 entityManager.SetComponentData(e, new Child {
    180.                     index = j,
    181.                     parent = parentEntities[i]
    182.                 });
    183.                 entityManager.SetSharedComponentData(e, parent);
    184.             }
    185.         }
    186.  
    187.         initialized = true;
    188.     }
    189.  
    190.     protected override void OnDestroyManager() {
    191.         parentEntities.Dispose();
    192.     }
    193. }
    194.  
     
    Last edited: Nov 19, 2018