Simple ECS Inventory Question

Below is a very simple inventory system that I’m trying to create in an attempt to better understand ECS.

Each entity has an InventoryComponent which stores the maximum number of a single item.
Each entity has a DynamicBuffer of InventoryItemData which stores an itemID and quantity.

I’m familiar about how to create systems that iterate over components to perform operations, but need a better understanding of how to interact with ECS from other systems / game logic, and how to iterate over items stored in a buffer.

Currently, using the ‘AddItem’ method, the data is added to the system ad-hoc which seems a bit ‘OOP’.

My next idea was to replace ‘AddItem’ with ‘AddItemAction’ and instead queue ‘InventoryActions’ against an entity, and then process them at once as part of the InventorySystem.OnUpdate.

The questions are:

  • Do you usually add methods to a system to add/remove data from the system as below?
  • How would you go about writing the OnUpdate method?
  • Is there a better collection for the itemData I could use similar to a Dictionary<int, int> items
  • Currently, the AddItem method returns a bool to indicate whether the add was successful or not - how would i go about doing something similar in an ECS way?

Thanks!

public struct InventoryAction : IBufferElementData
{
    public int itemID;
    public int quantity;
}

public struct InventoryItemData: IBufferElementData
{
    public int itemID;
    public int quantity;
}

public struct InventoryComponent: IComponentData
{
    public int capacity;
}

public class InventorySystem : ComponentSystem
{

    protected override void OnUpdate()
    {
        // Loop through entities with attached InventoryActions

            // If entity has an InventoryItemData with itemID = action.itemID
                // increment quantity
                // remove Inventory Action

            // else add new InventoryItemData with quantity


    }

    public bool AddItem(Entity inventory, int itemID, int quantity)
    {

        var inv = EntityManager.GetComponentData<InventoryComponent>(inventory);
        var items = EntityManager.GetBuffer<InventoryItemData>(inventory);

        // if we can't fit quantity inside this inventory - return false
        if (quantity > inv.capacity) return false;

        int index = -1;
        InventoryItemData item = new InventoryItemData()
        {
            itemID = itemID,
            quantity = quantity
        };

        // look for an existing record and add quantity if exists
        for (int i = 0; i < items.Length; i++)
        {
            if (items[i].itemID == itemID)
            {
                item.quantity += items[i].quantity;
                index = i;
                break;
            }
        }

        // limit item to capacity
        item.quantity = Mathf.Max(item.quantity, inv.capacity);

        // add or update item count
        if (index == -1)
            items.Add(item);
        else
            items[index] = item;

        return true;
    }

    public void AddItemAction(Entity inventory, int itemID, int quantity)
    {
        var actions = EntityManager.GetBuffer<InventoryAction>(inventory);

        actions.Add(new InventoryAction()
        {
            itemID = itemID,
            quantity = quantity
        });
    }

    public Entity NewInventory(int capacity)
    {
        var result = EntityManager.CreateEntity();
        EntityManager.AddComponentData<InventoryComponent>(result, new InventoryComponent() { capacity = capacity });
        EntityManager.AddBuffer<InventoryItemData>(result);
        EntityManager.AddBuffer<InventoryAction>(result);
        return result;
    }

My take on this :

  • I restrict my game to 256 possible different items
  • I restrict the quantity to 256
[InternalBufferCapacity(256)]
public struct Inventory : IBufferElementData
{
public byte Count;
}

and that’s it :slight_smile:

Unity’s ECS is a tuple-based ECS where data is stored independent of systems. Because of this, it is typically not a good idea to have public methods on systems, especially since such methods will never work with Burst. For an inventory system where things can be added, queried, and removed from multiple different systems, I might consider making a static class of utility functions which update state. If your inventory management requires working with multiple different types of components and buffers, and you can create a chunk iteration context object and a random access context object which store the appropriate type handles together as a struct that the static methods can use.

As for an equivalent to Dictionary, there’s NativeHashMap. There’s also UnsafeHashMap which you can store inside a component but does not have any safety features so you have to be conscious of how you use it.

Not sure which is faster, but I did mine like a Database.
A item have a ISharedComponentData called Parent that have a Entity which indicates who is the parent of the item.
To get all items in a container, just get all entities that have the same Parent ISharedComponentData whose Entity is the container.
To limit the total number of items in a container, check the total amount of entities as above if or not above limit before adding to it.
Item stacks (how many health potions you have in this slot) are just a property of the item health potion

I’ve basically have been treating the whole ECS as a giant in memory database, ISharedComponentData are indexes, IComponentData are tables and Entity are ids. No idea if it’s a fast way to use it, but it’s certainly easy and trivial to serialize for saving.

Thanks for your reply, would you mind posting your Update() method on your system when you get a chance? The discoverability of the various methods of iteration and documentation seem pretty poor at the moment.

Thanks

A bunch of inventory management systems: Systems.7z
This was one of the first things I did in ECS, so it’s not really optimized.
Get Inventory Size sample:

public virtual int GetInventorySize() {
        if (invSize == -1) {
            EntityManager em = DatabaseLoader.EntityManager;
            if (em.Exists(container)) {
                EntityQuery query = DumbForEachAccess.I.DumbGetComponentGroup(typeof(ItemParent));
                query.SetSharedComponentFilter(new ItemParent() { parent = container });
                return query.CalculateEntityCount();
            }
        }
        return 0;
    }

Get All Items (entities) in a container

public virtual IEnumerable<Entity> GetInventory() {
        EntityManager em = DatabaseLoader.EntityManager;
        if (em.Exists(container)) {
            EntityQuery query = DumbForEachAccess.I.DumbGetComponentGroup(typeof(ItemParent));
            query.SetSharedComponentFilter(new ItemParent() { parent = container });
            NativeArray<Entity> ents = query.ToEntityArray(Allocator.TempJob);
            foreach (Entity ent in ents) {
                yield return ent;
            }
            ents.Dispose();
        }
    }

DumbForEachAccess is just to access ForEach from anywhere and QuickSystems are just shortcuts to trivial code
QuickSystems: using Unity.Entities;using UnityEngine;public abstract class QuickSystemBa - Pastebin.com (do compile in a DLL, otherwise this greatly increases compile time)

6165663–674457–Systems.7z (5.73 KB)

Thanks, that’s really helpful.