Search Unity

Question Need help with entity relations.

Discussion in 'Entity Component System' started by soundeos, Oct 25, 2021.

  1. soundeos

    soundeos

    Joined:
    Mar 4, 2014
    Posts:
    21
    I need design advice for a simple scenario. Suppose we have container components called "box".
    Code (CSharp):
    1. public struct Box : IComponentData
    2. {
    3.  
    4. }
    Every box containes "items". Items have "itemType" and "amount" properties.
    Code (CSharp):
    1. public enum ItemType
    2. {
    3.     ITEM_A,
    4.     ITEM_B,
    5.     ITEM_C
    6.     //...
    7. }
    8.  
    And finally there are agents that can pick up or put down items in&out the boxes.
    Code (CSharp):
    1. public struct PickUpAction : IComponentData
    2. {
    3.     public Entity box;
    4.     public ItemType itemType;
    5.     public int amount;
    6. }
    7.  
    What is the best way of making a relationship between boxes, items, and pick up actions?

    First I tried DynamicBuffers.
    Code (CSharp):
    1. public struct ListItem : IBufferElementData
    2. {
    3.     public ItemType itemType;
    4.     public int amount;
    5. }
    6.  
    Since the content of the boxes is not fixed (there can be lots of item types), I don't think it is good solution. But on the other hand writing a system for pick up jobs is very easy. (By the way, is there a way to run this parallel?)
    Code (CSharp):
    1. protected override void OnUpdate()
    2. {
    3.         var boxes = GetBufferFromEntity<ListItem>(false);
    4.  
    5.         Entities.ForEach((int entityInQueryIndex, Entity entity, in PickUpAction pickUpAction) =>
    6.         {
    7.  
    8.             var boxContent = boxes[pickUpAction.box];
    9.             for (int i = 0; i < boxContent.Length; i++)
    10.             {
    11.                 var item = boxContent[i];
    12.  
    13.                 if (item.itemType == pickUpAction.itemType && item.amount >= pickUpAction.amount)
    14.                 {
    15.                     item.amount -= pickUpAction.amount;
    16.                     boxContent[i] = item;
    17.  
    18.                     break;
    19.                 }
    20.             }
    21.  
    22.         }).Schedule();
    23. }
    24.  
    Then I tried moving items to their own entities. And things started to get complicated...
    Code (CSharp):
    1. public struct Item : IComponentData
    2. {
    3.     public Entity box;
    4.     public ItemType itemType;
    5.     public int amount;
    6. }
    7.  
    8. protected override void OnUpdate()
    9. {
    10.         var ecb = ecbSystem.CreateCommandBuffer().AsParallelWriter();
    11.  
    12.         NativeArray<Entity> allItemEntities = GetEntityQuery(ComponentType.ReadOnly<Item>()).ToEntityArray(Allocator.TempJob);
    13.         var allItems = GetComponentDataFromEntity<Item>(true);
    14.  
    15.         Entities
    16.             .WithDisposeOnCompletion(allItemEntities)
    17.             .WithReadOnly(allItemEntities)
    18.             .WithDisposeOnCompletion(allItems)
    19.             .WithReadOnly(allItems)
    20.             .ForEach((int entityInQueryIndex, Entity entity, in PickUpAction pickUpAction) =>
    21.             {
    22.                 for (int i = 0; i < allItemEntities.Length; i++)
    23.                 {
    24.                     var item = allItems[allItemEntities[i]];
    25.                     if (item.box == pickUpAction.box && item.itemType == pickUpAction.itemType && item.amount >= pickUpAction.amount)
    26.                     {
    27.                         item.amount -= pickUpAction.amount;
    28.  
    29.                         ecb.SetComponent(entityInQueryIndex, allItemEntities[i], item);
    30.                     }
    31.                 }
    32.  
    33.             }).ScheduleParallel();
    34.  
    35.         ecbSystem.AddJobHandleForProducer(Dependency);
    36. }
    Is this the right way to get the related items?

    Since this system uses entity command buffer, the items will not be updated until it finishes running. This brings a problem. Suppose we have one box and an item entity is linked to it. This item entity has 1 amount of ITEM_A. When 10 pickup actions try to get this one item, they will all succeed. Because the amount is not updated and all the agents see there is 1 amount of ITEM_A. How can I solve this issue? Is there a way to check if the data is updated from this or another system?

    Here is the source code if you want to play around? Thanks.
    Code (CSharp):
    1. using System;
    2. using Unity.Collections;
    3. using Unity.Entities;
    4.  
    5. public enum ItemType
    6. {
    7.     ITEM_A,
    8.     ITEM_B,
    9.     ITEM_C
    10.     //...
    11. }
    12.  
    13. public struct Box : IComponentData
    14. {
    15.  
    16. }
    17.  
    18. public struct Item : IComponentData
    19. {
    20.     public Entity box;
    21.     public ItemType itemType;
    22.     public int amount;
    23. }
    24.  
    25. public struct ListItem : IBufferElementData
    26. {
    27.     public ItemType itemType;
    28.     public int amount;
    29. }
    30.  
    31. public struct PickUpAction : IComponentData
    32. {
    33.     public Entity box;
    34.     public ItemType itemType;
    35.     public int amount;
    36. }
    37.  
    38.  
    39. public class PickUpItemSystem : SystemBase
    40. {
    41.     private EntityCommandBufferSystem ecbSystem;
    42.     private System.Random random = new System.Random();
    43.  
    44.     protected override void OnCreate()
    45.     {
    46.         ecbSystem = World.GetExistingSystem<EndSimulationEntityCommandBufferSystem>();
    47.         EntityManager manager = World.DefaultGameObjectInjectionWorld.EntityManager;
    48.  
    49.         //Test boxes
    50.         EntityArchetype boxEntityArchetype = manager.CreateArchetype(typeof(Box));
    51.  
    52.         int boxCount = 1;
    53.         NativeArray<Entity> boxEntities = new NativeArray<Entity>(boxCount, Allocator.Temp);
    54.         manager.CreateEntity(boxEntityArchetype, boxEntities);
    55.  
    56.         //Test buffer items
    57.         //for (int i = 0; i < boxEntities.Length; i++)
    58.         //{
    59.         //    manager.SetName(boxEntities[i], $"Box {i}");
    60.  
    61.         //    var buffer = manager.AddBuffer<ListItem>(boxEntities[i]);
    62.         //    buffer.Add(new ListItem
    63.         //    {
    64.         //        itemType = ItemType.ITEM_A,
    65.         //        amount = 1
    66.         //    });
    67.  
    68.         //    //buffer.Add(new ListItem
    69.         //    //{
    70.         //    //    itemType = ItemType.ITEM_B,
    71.         //    //    amount = random.Next(100)
    72.         //    //});
    73.         //}
    74.  
    75.         //Test items
    76.         EntityArchetype itemEntityArchetype = manager.CreateArchetype(typeof(Item));
    77.  
    78.         int itemCount = 1;
    79.         NativeArray<Entity> itemEntities = new NativeArray<Entity>(itemCount, Allocator.Temp);
    80.         manager.CreateEntity(itemEntityArchetype, itemEntities);
    81.  
    82.         for (int i = 0; i < itemEntities.Length; i++)
    83.         {
    84.             manager.SetName(itemEntities[i], $"Item {i}");
    85.  
    86.             manager.SetComponentData(itemEntities[i], new Item
    87.             {
    88.                 box = boxEntities[random.Next(boxCount)],
    89.                 itemType = ItemType.ITEM_A,
    90.                 amount = 1
    91.             });
    92.         }
    93.  
    94.         //Agents
    95.         EntityArchetype agentEntityArchetype = manager.CreateArchetype(typeof(PickUpAction));
    96.  
    97.         int agentCount = 10;
    98.         NativeArray<Entity> agentEntities = new NativeArray<Entity>(agentCount, Allocator.Temp);
    99.         manager.CreateEntity(agentEntityArchetype, agentEntities);
    100.  
    101.         //Test items
    102.         for (int i = 0; i < agentEntities.Length; i++)
    103.         {
    104.             manager.SetName(agentEntities[i], $"Agent {i}");
    105.  
    106.             manager.SetComponentData(agentEntities[i], new PickUpAction
    107.             {
    108.                 box = boxEntities[random.Next(boxCount)],
    109.                 itemType = ItemType.ITEM_A,
    110.                 //amount = random.Next(10) + 1
    111.                 amount = 1
    112.             });
    113.         }
    114.     }
    115.  
    116.     protected override void OnUpdate()
    117.     {
    118.         //var ecb = ecbSystem.CreateCommandBuffer().AsParallelWriter();
    119.  
    120.         //var boxes = GetBufferFromEntity<ListItem>(false);
    121.  
    122.         //Entities.ForEach((int entityInQueryIndex, Entity entity, in PickUpAction pickUpAction) =>
    123.         //{
    124.  
    125.         //    var boxContent = boxes[pickUpAction.box];
    126.         //    for (int i = 0; i < boxContent.Length; i++)
    127.         //    {
    128.         //        var item = boxContent[i];
    129.  
    130.         //        if (item.itemType == pickUpAction.itemType && item.amount >= pickUpAction.amount)
    131.         //        {
    132.         //            item.amount -= pickUpAction.amount;
    133.         //            boxContent[i] = item;
    134.  
    135.         //            ecb.AddComponent<Disabled>(entityInQueryIndex, entity);
    136.  
    137.         //            break;
    138.         //        }
    139.         //    }
    140.  
    141.         //}).Schedule();
    142.  
    143.         //ecbSystem.AddJobHandleForProducer(Dependency);
    144.  
    145.         var ecb = ecbSystem.CreateCommandBuffer().AsParallelWriter();
    146.  
    147.         NativeArray<Entity> allItemEntities = GetEntityQuery(ComponentType.ReadOnly<Item>()).ToEntityArray(Allocator.TempJob);
    148.         var allItems = GetComponentDataFromEntity<Item>(true);
    149.  
    150.         Entities
    151.             .WithDisposeOnCompletion(allItemEntities)
    152.             .WithReadOnly(allItemEntities)
    153.             .WithDisposeOnCompletion(allItems)
    154.             .WithReadOnly(allItems)
    155.             .ForEach((int entityInQueryIndex, Entity entity, in PickUpAction pickUpAction) =>
    156.             {
    157.                 for (int i = 0; i < allItemEntities.Length; i++)
    158.                 {
    159.                     var item = allItems[allItemEntities[i]];
    160.                     if (item.box == pickUpAction.box && item.itemType == pickUpAction.itemType && item.amount >= pickUpAction.amount)
    161.                     {
    162.                         item.amount -= pickUpAction.amount;
    163.  
    164.                         ecb.SetComponent(entityInQueryIndex, allItemEntities[i], item);
    165.                         ecb.AddComponent<Disabled>(entityInQueryIndex, entity);
    166.                     }
    167.                 }
    168.  
    169.             }).ScheduleParallel();
    170.  
    171.         ecbSystem.AddJobHandleForProducer(Dependency);
    172.     }
    173. }
    174.  
     
    Last edited: Oct 26, 2021
    apkdev likes this.
  2. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Here's how I think I'd implement this:
    • an Box is an entity with a DynamicBuffer<Item> and a DynamicBuffer<ItemTransactionRequest>
    • a Item is a bufferElement containing item type, current quantity & max quantity
    • a ItemTransactionRequest is a buffer element containing the Entity of the actor who wants to take/give items to the box, and the amount/type of items they want to take/give
    • first you launch a parallel job that will use EntityCommandBuffer.AppendToBuffer() to make all actors append all of their ItemTransactionRequest on the boxes they are interacting with. No item quantity checks are done at this point. (using an ECB is what allows this job to be parallel)
    • now playback the ECB to apply the changes to the ItemTransactionRequest buffers
    • then you launch a parallel job that iterates on all DynamicBuffer<ItemTransactionRequest > + DynamicBuffer<Item> of all boxes, and processes the transactions one by one by doing the actual item quantity checks. This job may be parallel, but since all requests affecting the same box are in the same DynamicBuffer, you can safely make checks to determine who got the item first in concurrency scenarios.
      • NOTE: you may want to add some kind of tag component to the box entity whenever someone adds an ItemTransactionRequest. This would save you from uselessly iterating on all boxes every frame in order to check for transaction requests
    This assumes that your "items" are simple enough to not require any sort of extra data to define them. However, if that was the case, I think the implementation would look roughly the same except the DynamicBuffer<Item> on the Box entity would be pointing to item Entities instead of using enums

    But also, chances are that making all this be parallel jobs would be a waste. Unless you think you might have thousands of actors interacting with boxes on some frames, it would probably be more efficient as single-thread jobs. It would have smaller scheduling cost, and remove the need to work with an ECB to append item transaction requests
     
    Last edited: Oct 25, 2021
    apkdev likes this.
  3. Tony_Max

    Tony_Max

    Joined:
    Feb 7, 2017
    Posts:
    352
    I have chosen DynamicBuffer with type and amount. You can use Reinterpret<ItemType> and then use IndexOf. In my project i have implemented few methods to quick find particular / min / max element in NativeArray, so any code that need to surch item type in dynamic buffer do it in 1-2 line of code and it is no such dramaticaly boilerplaty.
    Also i would recomend you not to use enum to define item type, instead use Entity as unique identifier, because after that you have no need to adjust your enum code, you just create new entity + you can assign any data on your "enum" entity such as icon sprite / cost / etc.
     
  4. soundeos

    soundeos

    Joined:
    Mar 4, 2014
    Posts:
    21
    My "items" are simple but there are lots of item types, nearly 100. One box can contain lots of item types and the other might be empty. So, in this case, is it a good idea to store item list with dynamic buffers, even the buffer element is pointing to an item entity?
     
  5. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I assume that the problem you are seeing here is that the "search" for the desired item might be expensive if you need to iterate over 100 items in order to find the one with the right type (?)

    In this case, there's a way to make this extremely fast: each item type in your DynamicBuffer<Item> would have a pre-determined index in the buffer. So every DynamicBuffer<Item> of every box would be pre-initialized with 100 elements, all with a quantity of 0. You could use the item type enum as a way of defining the index of that item type. With this strategy, finding an item in your buffer only becomes a question of getting the item at myItemsBuffer[(int)itemType];
     
    Last edited: Oct 25, 2021
    Krajca likes this.
  6. soundeos

    soundeos

    Joined:
    Mar 4, 2014
    Posts:
    21
    No. The issue I have is, I don't know the size of my lists. So I don't know how much I need to allocate for the dynamic buffers. I feel like it is bad memory management.
     
  7. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    The [InternalBufferCapacity] attribute lets you control how much space the buffer actually takes inside the chunk, and how much of it is stored outside (accessed via pointer). In your case, you'd probably want to give your Item buffers an internal capacity of 0, so all those items inside don't interfere with fast chunk iteration. You can read about it here
     
    Last edited: Oct 25, 2021
  8. soundeos

    soundeos

    Joined:
    Mar 4, 2014
    Posts:
    21
    I thought when the dynamic buffers moved out of the chunk, it will slow down the iteration. Isn't is the case, since we are reading/writing to the dynamic buffer?
     
  9. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Accessing the part of the buffer that is outside the chunk will be a bit slower than accessing the part inside the chunk, but the cost should be very negligible in reality. Especially for this use case, where you probably won't have thousands of item transactions happening every frame
     
  10. soundeos

    soundeos

    Joined:
    Mar 4, 2014
    Posts:
    21
    Cool. I'll stick with the dynamic buffer approach then. Thanks.