Need help with entity relations.

I need design advice for a simple scenario. Suppose we have container components called “box”.

public struct Box : IComponentData
{

}

Every box containes “items”. Items have “itemType” and “amount” properties.

public enum ItemType
{
    ITEM_A,
    ITEM_B,
    ITEM_C
    //...
}

And finally there are agents that can pick up or put down items in&out the boxes.

public struct PickUpAction : IComponentData
{
    public Entity box;
    public ItemType itemType;
    public int amount;
}

What is the best way of making a relationship between boxes, items, and pick up actions?

First I tried DynamicBuffers.

public struct ListItem : IBufferElementData
{
    public ItemType itemType;
    public int amount;
}

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?)

protected override void OnUpdate()
{
        var boxes = GetBufferFromEntity<ListItem>(false);

        Entities.ForEach((int entityInQueryIndex, Entity entity, in PickUpAction pickUpAction) =>
        {

            var boxContent = boxes[pickUpAction.box];
            for (int i = 0; i < boxContent.Length; i++)
            {
                var item = boxContent[i];

                if (item.itemType == pickUpAction.itemType && item.amount >= pickUpAction.amount)
                {
                    item.amount -= pickUpAction.amount;
                    boxContent[i] = item;

                    break;
                }
            }

        }).Schedule();
}

Then I tried moving items to their own entities. And things started to get complicated…

public struct Item : IComponentData
{
    public Entity box;
    public ItemType itemType;
    public int amount;
}

protected override void OnUpdate()
{
        var ecb = ecbSystem.CreateCommandBuffer().AsParallelWriter();

        NativeArray<Entity> allItemEntities = GetEntityQuery(ComponentType.ReadOnly<Item>()).ToEntityArray(Allocator.TempJob);
        var allItems = GetComponentDataFromEntity<Item>(true);

        Entities
            .WithDisposeOnCompletion(allItemEntities)
            .WithReadOnly(allItemEntities)
            .WithDisposeOnCompletion(allItems)
            .WithReadOnly(allItems)
            .ForEach((int entityInQueryIndex, Entity entity, in PickUpAction pickUpAction) =>
            {
                for (int i = 0; i < allItemEntities.Length; i++)
                {
                    var item = allItems[allItemEntities[i]];
                    if (item.box == pickUpAction.box && item.itemType == pickUpAction.itemType && item.amount >= pickUpAction.amount)
                    {
                        item.amount -= pickUpAction.amount;

                        ecb.SetComponent(entityInQueryIndex, allItemEntities[i], item);
                    }
                }

            }).ScheduleParallel();

        ecbSystem.AddJobHandleForProducer(Dependency);
}

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.
Source

using System;
using Unity.Collections;
using Unity.Entities;

public enum ItemType
{
    ITEM_A,
    ITEM_B,
    ITEM_C
    //...
}

public struct Box : IComponentData
{

}

public struct Item : IComponentData
{
    public Entity box;
    public ItemType itemType;
    public int amount;
}

public struct ListItem : IBufferElementData
{
    public ItemType itemType;
    public int amount;
}

public struct PickUpAction : IComponentData
{
    public Entity box;
    public ItemType itemType;
    public int amount;
}


public class PickUpItemSystem : SystemBase
{
    private EntityCommandBufferSystem ecbSystem;
    private System.Random random = new System.Random();

    protected override void OnCreate()
    {
        ecbSystem = World.GetExistingSystem<EndSimulationEntityCommandBufferSystem>();
        EntityManager manager = World.DefaultGameObjectInjectionWorld.EntityManager;

        //Test boxes
        EntityArchetype boxEntityArchetype = manager.CreateArchetype(typeof(Box));

        int boxCount = 1;
        NativeArray<Entity> boxEntities = new NativeArray<Entity>(boxCount, Allocator.Temp);
        manager.CreateEntity(boxEntityArchetype, boxEntities);

        //Test buffer items
        //for (int i = 0; i < boxEntities.Length; i++)
        //{
        //    manager.SetName(boxEntities[i], $"Box {i}");

        //    var buffer = manager.AddBuffer<ListItem>(boxEntities[i]);
        //    buffer.Add(new ListItem
        //    {
        //        itemType = ItemType.ITEM_A,
        //        amount = 1
        //    });

        //    //buffer.Add(new ListItem
        //    //{
        //    //    itemType = ItemType.ITEM_B,
        //    //    amount = random.Next(100)
        //    //});
        //}

        //Test items
        EntityArchetype itemEntityArchetype = manager.CreateArchetype(typeof(Item));

        int itemCount = 1;
        NativeArray<Entity> itemEntities = new NativeArray<Entity>(itemCount, Allocator.Temp);
        manager.CreateEntity(itemEntityArchetype, itemEntities);

        for (int i = 0; i < itemEntities.Length; i++)
        {
            manager.SetName(itemEntities[i], $"Item {i}");

            manager.SetComponentData(itemEntities[i], new Item
            {
                box = boxEntities[random.Next(boxCount)],
                itemType = ItemType.ITEM_A,
                amount = 1
            });
        }

        //Agents
        EntityArchetype agentEntityArchetype = manager.CreateArchetype(typeof(PickUpAction));

        int agentCount = 10;
        NativeArray<Entity> agentEntities = new NativeArray<Entity>(agentCount, Allocator.Temp);
        manager.CreateEntity(agentEntityArchetype, agentEntities);

        //Test items
        for (int i = 0; i < agentEntities.Length; i++)
        {
            manager.SetName(agentEntities[i], $"Agent {i}");

            manager.SetComponentData(agentEntities[i], new PickUpAction
            {
                box = boxEntities[random.Next(boxCount)],
                itemType = ItemType.ITEM_A,
                //amount = random.Next(10) + 1
                amount = 1
            });
        }
    }

    protected override void OnUpdate()
    {
        //var ecb = ecbSystem.CreateCommandBuffer().AsParallelWriter();

        //var boxes = GetBufferFromEntity<ListItem>(false);

        //Entities.ForEach((int entityInQueryIndex, Entity entity, in PickUpAction pickUpAction) =>
        //{

        //    var boxContent = boxes[pickUpAction.box];
        //    for (int i = 0; i < boxContent.Length; i++)
        //    {
        //        var item = boxContent[i];

        //        if (item.itemType == pickUpAction.itemType && item.amount >= pickUpAction.amount)
        //        {
        //            item.amount -= pickUpAction.amount;
        //            boxContent[i] = item;

        //            ecb.AddComponent<Disabled>(entityInQueryIndex, entity);

        //            break;
        //        }
        //    }

        //}).Schedule();

        //ecbSystem.AddJobHandleForProducer(Dependency);

        var ecb = ecbSystem.CreateCommandBuffer().AsParallelWriter();

        NativeArray<Entity> allItemEntities = GetEntityQuery(ComponentType.ReadOnly<Item>()).ToEntityArray(Allocator.TempJob);
        var allItems = GetComponentDataFromEntity<Item>(true);

        Entities
            .WithDisposeOnCompletion(allItemEntities)
            .WithReadOnly(allItemEntities)
            .WithDisposeOnCompletion(allItems)
            .WithReadOnly(allItems)
            .ForEach((int entityInQueryIndex, Entity entity, in PickUpAction pickUpAction) =>
            {
                for (int i = 0; i < allItemEntities.Length; i++)
                {
                    var item = allItems[allItemEntities[i]];
                    if (item.box == pickUpAction.box && item.itemType == pickUpAction.itemType && item.amount >= pickUpAction.amount)
                    {
                        item.amount -= pickUpAction.amount;

                        ecb.SetComponent(entityInQueryIndex, allItemEntities[i], item);
                        ecb.AddComponent<Disabled>(entityInQueryIndex, entity);
                    }
                }

            }).ScheduleParallel();

        ecbSystem.AddJobHandleForProducer(Dependency);
    }
}
1 Like

Here’s how I think I’d implement this:

  • an Box is an entity with a DynamicBuffer and a DynamicBuffer

  • 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 + DynamicBuffer 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 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

1 Like

I have chosen DynamicBuffer with type and amount. You can use Reinterpret 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.

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?

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 would have a pre-determined index in the buffer. So every DynamicBuffer 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];

1 Like

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.

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

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?

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

Cool. I’ll stick with the dynamic buffer approach then. Thanks.