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

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 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!

(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! :slight_smile:

Breaking down a large problem such as inventory:

I have had the same issues regarding this topic and from my experience the effort is usually barely worth :frowning:

https://poreklorecnik.rs/

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:

[System.Serializable]
public class InventoryInstance
{
    // could be scriptable object, enum, etc
    // let's pretend its a scriptable object
    [SerialzieField]
    private ItemType _inventoryItemType;
  
    [Serializable]
    private List<Item> _inventoryItems = new();
  
    public bool TryAddItemToInventory(Item item)
    {
        // null is any item type
        bool canAdd = !_inventoryItemType || item.ItemType == _inventoryItemType;
      
        if (canAdd)
        {
            this.AddItemToInventory(item);          
        }
      
        return canAdd;
    }
  
    public void AddItemToInventory(Item item)
    {
        _inventoryItems.Add(item);
    }
}

public class PlayerInventory : Monobehaviour
{
    [SerialzieField]
    private List<InventoryInstance> _inventories = new();
  
    public void AddItemToInventory(Item item)
    {
        bool added = false;
      
        foreach (var inventory in _inventories)
        {
            added = inventory.TryAddItemToInventory(item);
          
            if (added)
            {
                break;
            }
        }
      
        if (!added)
        {
            // warn developer
        }
    }
}

Very simplified example to illustrate the idea.

2 Likes

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:

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.