Search Unity

Question Best approach for multiple inventories dealing with a base Item class?

Discussion in 'Scripting' started by PascalTheDog, Jul 2, 2022.

  1. PascalTheDog

    PascalTheDog

    Joined:
    Mar 16, 2015
    Posts:
    86
    My game features two types of items: weapons and consumables. They share enough features (fields: prefab, name, description, icon, value, etc; methods: use, equip, discard, sort, etc) that it would make sense for them to derive from the same abstract Item base class (which itself derives from ScriptableObject). Their gameplay purposes are however distinct enough to warrant two different inventories, be it only because they have different management interfaces; I might also want consumables but not weapons to be stackable, or carrying limit to be infinite for weapons but not consumables — that kind of thing. And maybe later, I might even want a Talisman inventory or a Key inventory to boot.

    Now I have a TreasureChest class; all that is required is to drag 'n' drop an Item in the Inspector field, which thanks to polymorphism could be any class deriving from Item i.e. a weapon OR a consumable. The player interacts with the chest, and the item is returned to them. And this is where I'm a bit troubled: I'd like to devise some system which is able to determine what type of item was obtained and put it in the matching inventory, and I'm not quite sure how to go about it. After all, treasure chests can contain any type of items and TreasureChest therefore returns an Item (the base class) rather than one of the specific classes deriving from it.

    My Inventories class contains two instances (one for each item type) of the Inventory class, which is centered around a List<Item> and contains all sorts of methods that pertain to adding, removing and sorting items in that List. That means I can easily create new inventories if need be, but it also means any inventory could technically hold any kind of item regardless of function. I could make the Inventory class an abstract base for specific inventories much like items, but then I'd lose the versatility that allows me to to easily create new inventories; every item type would need its matching inventory type.

    What I'm using right now is a convoluted type check: the item returned from the chest is handled by a method in my Inventories class that determines which exact child class the item is an instance of (i.e. if returnedItem is ConsumableItem, etc) and then adds it to the appropriate inventory. It works, but rubs me the wrong way: Isn't the whole point of abstract base classes to... abstract from particulars and eliminating the need to check for specifics? I believe there is a fundamental architectural flaw in my current approach and I'd greatly appreciate some assistance from more experienced programmers. Cheers!
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,697
    (not sure there is one answer to your question, but this can help you think about it)

    These things (character customization, inventories, shop systems) are fairly tricky hairy beasts, definitely deep in advanced coding territory. They contain elements of:

    - a database of items that you may possibly possess / equip
    - a database of the items that you actually possess / equip currently
    - perhaps another database of your "storage" area at home base?
    - persistence of this information to storage between game runs
    - presentation of the inventory to the user (may have to scale and grow, overlay parts, clothing, etc)
    - interaction with items in the inventory or on the character or in the home base storage area
    - interaction with the world to get items in and out
    - dependence on asset definition (images, etc.) for presentation

    Just the design choices of an inventory system can have a lot of complicating confounding issues, such as:

    - can you have multiple items? Is there a limit?
    - are those items shown individually or do they stack?
    - are coins / gems stacked but other stuff isn't stacked?
    - do items have detailed data shown (durability, rarity, damage, etc.)?
    - can users combine items to make new items? How? Limits? Results? Messages of success/failure?
    - can users substantially modify items with other things like spells, gems, sockets, etc.?
    - does a worn-out item (shovel) become something else (like a stick) when the item wears out fully?
    - etc.

    Your best bet is probably to write down exactly what you want feature-wise. It may be useful to get very familiar with an existing game so you have an actual example of each feature in action.

    Once you have decided a baseline design, fully work through two or three different inventory tutorials on Youtube, perhaps even for the game example you have chosen above.

    Or... do like I like to do: just jump in and make it up as you go. It is SOFT-ware after all... evolve it as you go! :)

    Breaking down a large problem such as inventory:

    https://forum.unity.com/threads/weapon-inventory-and-how-to-script-weapons.1046236/#post-6769558
     
  3. julianarikmar

    julianarikmar

    Joined:
    Dec 9, 2021
    Posts:
    1
    I have had the same issues regarding this topic and from my experience the effort is usually barely worth :(
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,864
    I don't think it was worth necro-posting a thread just to provide nothing useful.

    To answer OP's post, even though this thread is a year old, I would wonder why you have multiple inventories on the player that only allow specific items. It would make sense to just have the one inventory everything gets thrown inside, and then be able to filter said inventory.

    Nonetheless there are ways you could make this data driven and not require hard-coded type matching.

    For example:
    Code (CSharp):
    1. [System.Serializable]
    2. public class InventoryInstance
    3. {
    4.     // could be scriptable object, enum, etc
    5.     // let's pretend its a scriptable object
    6.     [SerialzieField]
    7.     private ItemType _inventoryItemType;
    8.  
    9.     [Serializable]
    10.     private List<Item> _inventoryItems = new();
    11.  
    12.     public bool TryAddItemToInventory(Item item)
    13.     {
    14.         // null is any item type
    15.         bool canAdd = !_inventoryItemType || item.ItemType == _inventoryItemType;
    16.      
    17.         if (canAdd)
    18.         {
    19.             this.AddItemToInventory(item);          
    20.         }
    21.      
    22.         return canAdd;
    23.     }
    24.  
    25.     public void AddItemToInventory(Item item)
    26.     {
    27.         _inventoryItems.Add(item);
    28.     }
    29. }
    30.  
    31. public class PlayerInventory : Monobehaviour
    32. {
    33.     [SerialzieField]
    34.     private List<InventoryInstance> _inventories = new();
    35.  
    36.     public void AddItemToInventory(Item item)
    37.     {
    38.         bool added = false;
    39.      
    40.         foreach (var inventory in _inventories)
    41.         {
    42.             added = inventory.TryAddItemToInventory(item);
    43.          
    44.             if (added)
    45.             {
    46.                 break;
    47.             }
    48.         }
    49.      
    50.         if (!added)
    51.         {
    52.             // warn developer
    53.         }
    54.     }
    55. }
    Very simplified example to illustrate the idea.
     
    Bunny83 and ijmmai like this.
  5. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,993
    If inventories can have a filter to it, I would generally just add this filtering mecanism so we ensure no invalid item is ever pushed into that inventory. So some kind of filter function "CanItemBeAdded" would do the job. It can check all sorts of conditions the item type and if you want if there's enough room in the inventory or whatever condition might be necessary. Every Add method would always be capable to "reject" an item and return a boolean value to indicate if it was added or not.

    When using interfaces for the actual inventories, you can even create a separate PlayerInventory class which is just a wrapper around all the players individual inventories. So when you add an item to this inventory, all it does is iterate through the list of inventories and try to add the item to each, one by one. Once one of the accepts the item, you return true. Of course if the player might have additional general purpose inventories (like a backpack with limited space) those should be checked last.

    So individual inventories would all implement the same interface (might even derive from a common base class) and implement their special purpose. So you could have a general Inventory class and a derived one which might be a FilteredInventory that has a list of item types that are accepted and would override the corresponding filter function that is used when adding items. Of course in general purpose inventories the filter method may just return true. Though if the space check is also done that way, that should be implemented there as well.

    For inventory interactions it doesn't really matter if the checks are complex and complicated as they only run occationally and checking a few things really isn't an issue.

    You may have heard about the game minecraft and that using many "hoppers" (which are inventories with 5 slots and can transfer items from other inventories to other inventories) can be bad for performance. That's because they are implemented in a naive straight forward way. That is it checks every slot of the source inventory against every slot in the target inventory and that both, for pulling items in and for pushing items out. Though for it to be a modular block there's not really a better way. Mods that implement item transport systems usually construct networks between individual inventories directly, even over large distances. So the network is build / rebuild when you add or remove "pipes" to the network but the actual transfer of items happens directly. Though my point is, even when you have a lot of vanilla hoppers, the impact is not that bad, even though they check for a potential transfer every 8 ticks (every 0.4 seconds). So performance is one of the least concerns. If inventories get large, that inventory might implement a dictionary to look up slots with a certain item type to speed up the search for potential item stack merging. Though when you implement an interface that defines all potential interactions with an inventory, each individual inventory class can implement internal optimisations if necessary. Though from the outside they just work as an
    IInventory
    .

    So when you implement a "wrapper" inventory class for the player that wraps all the individual inventories, the "GetAllItems" method would just collect items from all the wrapped inventories. Such methods are best implemented with a signature like this:
    Code (CSharp):
    1. GetAllItems(List<IItem> aList)
    That way any system can implement a garbage free query of all items and wrappers can easily delegate the accumulation to the wrapped inventories.

    Inventory systems can get quite complex, so you should think about a solid foundation. So for example the concept of a "slot" could be realised simply by combining a slot index and an IInventory reference in a simple slot struct. This slot struct could delegate a lot of the inventory interactions to the actual inventory based on the slot index. That way wrapper or slot search functions can return you a list of "slots" even from various inventories and you can interact with those slots in a similar manner as you could interact with a inventory as a whole. Such wrapper structs are quite useful as they allow to wrap mutliple values together and combine them into a single "thing" you can pass around.