Search Unity

What's the correct way of doing Item Stacking?

Discussion in 'Entity Component System' started by Guedez, Feb 14, 2020.

  1. Guedez

    Guedez

    Joined:
    Jun 1, 2012
    Posts:
    827
    I've somehow managed to get Item Stacking working on my game. Item Stacking being: If you are about to add an item to an inventory, check if the inventory already contains an sufficiently similar item, and then add the "itemToBeAdded".Amount to the "SufficientlySimilarItem".Amount and then delete instead of adding to inventory.

    The biggest challenge was "sufficiently similar item", and it was solved with a lot of reflection.
    In pseudocode:
    First group the items to be added to inventories to their target new parent inventories
    Then check if they stack with one another
    Then iterate through the container, and for each item, check if it can accept any of the items being stacked
    If it can, perform stacking operations on all components, then delete the item that was being added instead of adding
    If any of the items that were being added couldn't be stacked, just add them to the inventory.


    First the code that solves the problem, then the issues:
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using Unity.Collections;
    5. using Unity.Entities;
    6. using UnityEngine;
    7.  
    8. [UpdateBefore(typeof(UpdateInventorySystem))]
    9. public class AddItemToInventory : ComponentSystem {
    10.     EntityQuery m_MainGroup;
    11.     EntityQuery entityQuery;
    12.     protected override void OnCreate() {
    13.         m_MainGroup = GetEntityQuery(typeof(ToAddToInventory));
    14.         entityQuery = GetEntityQuery(typeof(ItemParent), typeof(ItemBaseReference));
    15.     }
    16.     protected override void OnUpdate() {
    17.         Dictionary<Entity, List<Entity>> targets = new Dictionary<Entity, List<Entity>>();
    18.         Entities.With(m_MainGroup).ForEach((Entity e, ref ToAddToInventory t1) => {
    19.             if (!targets.ContainsKey(t1.target)) {
    20.                 targets[t1.target] = new List<Entity>();
    21.             }
    22.             targets[t1.target].Add(e);
    23.             this.PostUpdateCommands.RemoveComponent<ToAddToInventory>(e);
    24.         });
    25.         this.PostUpdateCommands.Playback(EntityManager);
    26.         EntityCommandBuffer PostUpdateCommands = new EntityCommandBuffer(Allocator.TempJob);
    27.         var keys = targets.Keys.ToArray();
    28.         foreach (Entity key in keys) {
    29.             List<Entity> list = targets[key];
    30.             List<Entity> notStacked = new List<Entity>();
    31.             if (list.Count > 1) {
    32.                 List<Tuple<Entity, ItemBaseReference>> ToStacks = new List<Tuple<Entity, ItemBaseReference>>();
    33.                 foreach (Entity e in list) {
    34.                     ToStacks.Add(new Tuple<Entity, ItemBaseReference>(e, EntityManager.GetComponentData<ItemBaseReference>(e)));
    35.                 }
    36.                 List<Tuple<Entity, ItemBaseReference>> stackTo = ToStacks;
    37.                 while (stackTo.Count > 1) {
    38.                     notStacked.AddRange(AttempStack(Entities, EntityManager, PostUpdateCommands, stackTo.Take(1).Cast(T => T.Item1).ToList(), stackTo = stackTo.Skip(1).ToList()));
    39.                 }
    40.                 notStacked.AddRange(stackTo.Cast(T => T.Item1));
    41.             }
    42.             targets[key] = notStacked;
    43.         }
    44.         PostUpdateCommands.Playback(EntityManager);
    45.         PostUpdateCommands.Dispose();
    46.         PostUpdateCommands = new EntityCommandBuffer(Allocator.TempJob);
    47.         foreach (Entity key in keys) {
    48.             List<Tuple<Entity, ItemBaseReference>> stackTo = new List<Tuple<Entity, ItemBaseReference>>();
    49.             entityQuery.SetSharedComponentFilter(new ItemParent(key));
    50.             Entities.With(entityQuery).ForEach((Entity e, ref ItemBaseReference bref) => {
    51.                 stackTo.Add(new Tuple<Entity, ItemBaseReference>(e, bref));
    52.             });
    53.             targets[key] = AttempStack(Entities, EntityManager, PostUpdateCommands, targets[key], stackTo);
    54.         }
    55.         PostUpdateCommands.Playback(EntityManager);
    56.         PostUpdateCommands.Dispose();
    57.         PostUpdateCommands = new EntityCommandBuffer(Allocator.TempJob);
    58.         foreach (Entity key in keys) {
    59.             foreach (Entity value in targets[key]) {
    60.                 EntityManager.AddSharedComponentData(value, new ItemParent(key));
    61.             }
    62.         }
    63.         foreach (Entity key in keys) {
    64.             if (!EntityManager.HasComponent<UpdateInventory>(key)) {
    65.                 PostUpdateCommands.AddComponent(key, new UpdateInventory());
    66.             }
    67.         }
    68.         PostUpdateCommands.Playback(EntityManager);
    69.         PostUpdateCommands.Dispose();
    70.     }
    71.  
    72.     public static List<Entity> AttempStack(EntityQueryBuilder ent, EntityManager em, EntityCommandBuffer postUpdateCommands, List<Entity> list, List<Tuple<Entity, ItemBaseReference>> stackTo) {
    73.         Type stacktype = typeof(IStackable);
    74.         List<Entity> newlist = new List<Entity>();
    75.         List<Entity> stacked = new List<Entity>();
    76.         Dictionary<long, List<Entity>> posssibles = new Dictionary<long, List<Entity>>();
    77.         Dictionary<long, List<int>> tocheckAgainst = new Dictionary<long, List<int>>();
    78.         ComponentType[][] archetype = new ComponentType[list.Count][];
    79.         int[] count = new int[list.Count];
    80.         ItemBaseReference[] refs = new ItemBaseReference[list.Count];
    81.         for (int i = 0; i < list.Count; i++) {
    82.             Entity t = list[i];
    83.             ItemBaseReference bref = refs[i] = em.GetComponentData<ItemBaseReference>(t);
    84.             posssibles[bref.ItemReference] = new List<Entity>();
    85.             tocheckAgainst[bref.ItemReference] = new List<int>();
    86.             NativeArray<ComponentType> comps = em.GetComponentTypes(t, Allocator.TempJob);
    87.             archetype[i] = comps.ToArray().Concat(new ComponentType[] { typeof(ItemParent) }).OrderBy(T => T.TypeIndex).ToArray();
    88.             count[i] = em.GetComponentCount(t);
    89.             comps.Dispose();
    90.         }
    91.         for (int i = 0; i < list.Count; i++) {
    92.             tocheckAgainst[refs[i].ItemReference].Add(i);
    93.         }
    94.         foreach (Tuple<Entity, ItemBaseReference> test in stackTo) {
    95.             if (posssibles.ContainsKey(test.Item2.ItemReference)) {
    96.                 posssibles[test.Item2.ItemReference].Add(test.Item1);
    97.             }
    98.         };
    99.         foreach (long key in posssibles.Keys) {
    100.             foreach (Entity itemInContainer in posssibles[key]) {
    101.                 List<int> list1 = tocheckAgainst[key];
    102.                 for (int list_i = 0; list_i < list1.Count; list_i++) {
    103.                     int itemBeingAdded = list1[list_i];
    104.                     bool stack = true;
    105.                     Entity eItemBeingAdded = list[itemBeingAdded];
    106.                     //not doing archetype check yet, both items must have the very same IComponentDatas, IDynamicBuffers and ISharedComponentDatas,
    107.                     //this is currently being assumed on test data
    108.                     foreach (ComponentType type in archetype[itemBeingAdded]) {
    109.                         Type managed = type.GetManagedType();//if this type.GetManagedType() was not available, I would probably need to check every single
    110.                         //IComponentData in the assembly and test if both entities contains it or something similar
    111.                         if (stacktype.IsAssignableFrom(managed)) {
    112.                             stack &= CheckDoStack(em, itemInContainer, eItemBeingAdded, managed);
    113.                         }
    114.                     }
    115.                     if (stack) {
    116.                         ItemInstance instA = em.GetComponentData<ItemInstance>(itemInContainer);
    117.                         ItemInstance instB = em.GetComponentData<ItemInstance>(eItemBeingAdded);
    118.                         foreach (ComponentType type in archetype[itemBeingAdded]) {
    119.                             Type managed = type.GetManagedType();
    120.                             if (stacktype.IsAssignableFrom(managed)) {
    121.                                 DoStack(em, itemInContainer, eItemBeingAdded, instA.Amount, instB.Amount, managed);
    122.                             }
    123.                         }
    124.                         stacked.Add(eItemBeingAdded);
    125.                         postUpdateCommands.DestroyEntity(eItemBeingAdded);
    126.                         list1.RemoveAt(list_i);
    127.                         list_i--;
    128.                     }
    129.                 }
    130.             }
    131.         }
    132.  
    133.         return list.Except(stacked).ToList();
    134.     }
    135.  
    136.     private static void DoStack(EntityManager em, Entity itemInContainer, Entity eItemBeingAdded, float amA, float amB, Type managed) {
    137.         Utils.RunGeneric(((Action<EntityManager, Entity, Entity, float, float>)DoStack<Dummy>).Method, managed, em, itemInContainer, eItemBeingAdded, amA, amB);
    138.     }//A better way of getting a MethodInfo straight in the code would be nice, unfortunately just DoStack<Dummy>.Method or DoStack<>.Method does not work
    139.  
    140.     private static void DoStack<T>(EntityManager em, Entity itemInContainer, Entity eItemBeingAdded, float amA, float amB) where T : struct, IComponentData, IStackable {
    141.         T a = em.GetComponentData<T>(itemInContainer);
    142.         T b = em.GetComponentData<T>(eItemBeingAdded);
    143.         a.PerformStack(b, amA, amB);
    144.         em.SetComponentData<T>(itemInContainer, a);
    145.     }
    146.  
    147.     private static bool CheckDoStack(EntityManager em, Entity itemInContainer, Entity eItemBeingAdded, Type managed) {
    148.         return (bool)Utils.RunGeneric(((Func<EntityManager, Entity, Entity, bool>)CheckDoStack<Dummy>).Method, managed, em, itemInContainer, eItemBeingAdded);
    149.  
    150.     }
    151.  
    152.     private static bool CheckDoStack<T>(EntityManager em, Entity itemInContainer, Entity eItemBeingAdded) where T : struct, IComponentData, IStackable {
    153.         T a = em.GetComponentData<T>(itemInContainer);
    154.         T b = em.GetComponentData<T>(eItemBeingAdded);
    155.         return a.CanStack(b);
    156.     }
    157.  
    158.     private struct Dummy : IComponentData, IStackable {
    159.         bool IStackable.CanStack(object other) { return false; }
    160.         void IStackable.PerformStack(object other, float amA, float amB) { }
    161.     }
    162.  
    163.     public interface IStackable {
    164.         bool CanStack(object other);
    165.         void PerformStack(object other, float amA, float amB);
    166.     }
    167. }
    Code (CSharp):
    1. private static Dictionary<Tuple<MethodInfo, Type>, Delegate> RunGenericCache = new Dictionary<Tuple<MethodInfo, Type>, Delegate>();
    2.     private static Dictionary<Tuple<MethodInfo, Type, object>, Delegate> RunGenericCallerCache = new Dictionary<Tuple<MethodInfo, Type, object>, Delegate>();
    3.     internal static object RunGeneric(MethodInfo method, Type type, params object[] param) {
    4.         Tuple<MethodInfo, Type> key = new Tuple<MethodInfo, Type>(method, type);
    5.         if (!RunGenericCache.TryGetValue(key, out Delegate del)) {
    6.             RunGenericCache[key] = del = method.GetGenericMethodDefinition().MakeGenericMethod(type).CreateDelegate();
    7.         }
    8.         return del.DynamicInvoke(param);
    9.     }
    10.     internal static object RunGeneric(object caller, MethodInfo method, Type type, params object[] param) {
    11.         if (caller == null) {
    12.             return RunGeneric(method, type, param);
    13.         }
    14.         Tuple<MethodInfo, Type, object> key = new Tuple<MethodInfo, Type, object>(method, type, caller);
    15.         if (!RunGenericCallerCache.TryGetValue(key, out Delegate del)) {
    16.             RunGenericCallerCache[key] = del = method.GetGenericMethodDefinition().MakeGenericMethod(type).CreateDelegate(caller);
    17.         }
    18.         return del.DynamicInvoke(param);
    19.     }
    20.     public static Delegate CreateDelegate(this MethodInfo methodInfo, object target) {
    21.         Func<Type[], Type> getType;
    22.         var isAction = methodInfo.ReturnType.Equals((typeof(void)));
    23.         var types = methodInfo.GetParameters().Select(p => p.ParameterType);
    24.  
    25.         if (isAction) {
    26.             getType = Expression.GetActionType;
    27.         } else {
    28.             getType = Expression.GetFuncType;
    29.             types = types.Concat(new[] { methodInfo.ReturnType });
    30.         }
    31.  
    32.         if (methodInfo.IsStatic) {
    33.             return Delegate.CreateDelegate(getType(types.ToArray()), methodInfo);
    34.         }
    35.  
    36.         return Delegate.CreateDelegate(getType(types.ToArray()), target, methodInfo);
    37.     }
    38.  
    39.     public static Delegate CreateDelegate(this MethodInfo methodInfo) {
    40.         Func<Type[], Type> getType;
    41.         var isAction = methodInfo.ReturnType.Equals((typeof(void)));
    42.         var types = methodInfo.GetParameters().Select(p => p.ParameterType);
    43.  
    44.         if (isAction) {
    45.             getType = Expression.GetActionType;
    46.         } else {
    47.             getType = Expression.GetFuncType;
    48.             types = types.Concat(new[] { methodInfo.ReturnType });
    49.         }
    50.  
    51.         if (methodInfo.IsStatic) {
    52.             return Delegate.CreateDelegate(getType(types.ToArray()), methodInfo);
    53.         }
    54.  
    55.         return Delegate.CreateDelegate(getType(types.ToArray()), methodInfo);
    56.     }
    First issue is using Reflection, there seems to be no way of getting a IComponentData from type variables because that would cause boxing.
    Second issue is the very checking of "does these two entities have compatible IComponentDatas, IDynamicB(...)"
    The only way I can think of is this:
    Code (CSharp):
    1. IEnumerable<Entity> others = list.Skip(1);
    2.         Entity first = list.First();
    3.         int firstT = em.GetComponentCount(first);
    4.         foreach (Entity other in others) {
    5.             int otherT = em.GetComponentCount(other);
    6.             NativeArray<ComponentType> comps = em.GetComponentTypes(first, Allocator.TempJob);
    7.             if (firstT == otherT) {
    8.                 NativeArray<ComponentType> othercomps = em.GetComponentTypes(first, Allocator.TempJob);//please ignore the obvious groupXgroup comparison optimizations
    9.                 if (comps.All(T => othercomps.Contains(T)) && othercomps.All(T => comps.Contains(T))) {//as that's not the point of the thread
    10.                     //do the rest of the checking
    11.                 }
    12.             }
    13.         }
    Notable comment for this one, but unfortunately, before adding to the parent object (ISharedData reference to the parent inventory's entity), the archetypes would never be the same, and the stacking check happens before adding to the parent object
    Code (CSharp):
    1. IEnumerable<Entity> others = list.Skip(1);
    2.         Entity first = list.First();
    3.         int firstT = em.GetComponentCount(first);
    4.         foreach (Entity other in others) {
    5.             int otherT = em.GetComponentCount(other);
    6.             if (em.GetChunk(first).Archetype == em.GetChunk(other).Archetype) {
    7.                 //do the rest of the checking
    8.             }
    9.         }
    Any takers on figuring out what's the ECS'ey way of achieving this?
     
  2. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Don't use ECS for this, not like you are at least.

    At a high level you have your static item definitions. Then you have item instances, data that is unique per instance so minimum an id. And yes you need your own id's if you don't have them.

    Plus, you rarely need all items for all logic, in fact it's generally quite the opposite. Maybe you need all items for your inventory management UI but you certainly don't need them in ECS there. For say combat logic you just have equippped items. Or you might have entities representing individual items in the world. Where ECS logic needs item data, it's almost always going to be a small subset. So items are something where you should start with a normalized form, and denormalize (put into ECS) for very specific context's.

    So your static definitions are say in ScriptableObjects, which you can convert to blob assets, put in a NativeHashMap, what approach works best here depends on usage. But definitions are loaded once on game start and readonly. So you can easily attach small numbers of item instance data via dynamic buffers, fixed list, or just a simple component with fields for instances if the number is small enough. And then you take your instance in context, lookup the definition in the blob/map, and you have your complete item information. It's all compact and performant.


    The above will scale very well and it takes into account a bunch of other concerns you haven't hit yet but might, like multiplayer.
     
  3. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    For stacking specifically basically with instance data you only stack the same SKU where instance data can't vary for that SKU. At least with the common definition of stacking. You could group items of the same sku with different instance attributes, but I would use a separate abstraction for that and reserve stacking for SKU's that can't vary per instance. Just because that's very often a thing and when it is, you really don't want to be sending say 1k instances over the network when you could just be sending one with a quantity of 1000.
     
  4. Guedez

    Guedez

    Joined:
    Jun 1, 2012
    Posts:
    827
    Currently I check first if the Item ID is the same (happens when grouping items, it's not a an if)
    Then if the number of components is the same
    (Get the components array)
    Then check if the components are the same
    Then use Reflection to verify one by one if they are stackable, as some item parameters have more or less leway than others, and the lastly run the stacking code for each component (weighted average of some parameters, etc)

    I am 'satisfied' with my solution, The performance is good enough that I am having issues finding the system in the profiler timeline on normal usage scenarios.
    I am merely curious of what's the proper ECS way of doing all of this.
    It would probably involve splitting the whole operation in many phases and plenty of new systems, like one system for each component that might support stacking, then another system that performs the stacking operation for that component. How do one even manage to keep track of all moving parts, the order, and even some more complex operations like the whole 'checking if both items have the same archetype save for the 'please add to inventory' component that started the whole thing'.
    All of this without adding or removing any components to any of the items, as that would move them to another chunk, and apparently, that's as bad as adding/removing/changing the value of a ISharedComponentData that was so ill advised

    If I seriously want to keep using ECS, I better get pretty good at it, and understanding how to make 'Item Stacking' on pure ECS would help a lot
     
  5. Sarkahn

    Sarkahn

    Joined:
    Jan 9, 2013
    Posts:
    440
    I've found myself going a bit too far when it comes to splitting up my systems. Generally you can run most or all of your operations related to a certain archetype inside a single system, and just use functions to keep things organized. Theres no hard and fast rules of course but I'm finding that mindset is helping me keep things less cluttered.

    I wouldn't be so fast to dismiss using tag components for things like this. Especially now that we can use command buffers in burst and the structural changes themselves us burst, it's way less costly than it used to be. And I find it hard to imagine an inventory system could become a performance issue in most cases anyways.

    I'd be more worried about keeping your code readable and straightforward, which using components to define your operations helps with I've found.

    I'll be tackling inventory before too long in my roguelike, looking forward to coming up with my own solution.
     
    NotaNaN and Guedez like this.