Search Unity

How do you handle per-instance logic in an inventory system?

Discussion in 'Game Design' started by BIGTIMEMASTER, Jul 17, 2022.

  1. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Suppose you have thousands of items in your game which might exist in the world as a distinct game object, but can be added to a players inventory.

    You don't keep the actual gameobjects loaded except when they need to be seen. Once passed into players inventory, you could just store an ID (probably an integer) that is mapped with a structure which would hold instance variables.

    So the map(dictionary) might look like this: 10200349 / item info struct (items display name, items durability, item spoilage, items weight, items category, etc)

    As long as all of the items associated variables are static, meaning they never will change, I think that is all you ever need. In fact you might not need the struct at all as you could just grab data from a spreadsheet if you need it.

    But in the case that variables might change at runtime - like an apple spoils - in that case you need for the spoilage logic to be running somewhere. Initially I thought you'd have that exist in a component that would attach to gameobject - but you don't want to pass around references to an entire game object, right? There is no need for the gameobject is the apple is living inside the player inventory?

    Does this necessitate a manager class? Like a "global food decay" class? And this class searches the world for both gameobjects with a flag/tag/component which identifies them as subscribing to this spoilage event? And it would also need to manage those items in the players inventory - in which case it would need to filter for looping through every item and checking for the relevant variables?

    I read a bit about object pooling but that didn't seem to be the solution to this problem. Maybe there is some other pattern that addresses a case like this? If anybody has some thoughts I'd love to hear them. The manager class is my best idea so far.
     
  2. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,619
    Do you need to update "spoilage" at all? I imagine that in that case there would be a spoilage duration, and for each instance a spoilage start time. You can then trivially calculate how spoiled a given item as (currentTime - spoilageStartTime) / spoilageDuration. So there's no need to have logic updating it, because it can be calculated functionally (as in, a math function) on an as-needed basis. It's good to have things operate like this wherever reasonably possible, because you really don't want many thousands of objects running code to update their properties.

    For properties that need to be saved as a result of some interaction, you can have a Dictionary<ItemID, ItemPropertiesOverrides>. So if Apple #53 has had its healing properties increased you can add that to the dictionary. When an item's properties are looked up that dictionary can first be checked, and if there's no overrides for it then the defaults are used.
     
    Ryiah and BIGTIMEMASTER like this.
  3. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    You're correct; there's no need for a GameObject if the item is only in the player's inventory. You only need to set up a GameObject when putting an instance of that item in the game world. For a few thousand items, you might just brute force it. If you keep that code modular, you can replace the underlying implementation with something fancier later if necessary.

    If high performance is a concern from the beginning and you really have a lot of items, search up the Entity Attribute Value (EAV) model. It's designed for exactly this kind of data problem.
     
  4. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Thanks @angrypenguin @TonyLi

    I might have responded sooner but for some reason i am not getting notifications for when my own threads are responded to. I get notifications for threads i havent responded to for like 2 years though...

    Anyway :), totally makes sense about using simple math to just figure out the current value of an item, and I am sure i can make it so we just do that calculation at times when player might view the items current conditional state.

    I had also asked on some generic game dev forums and the unreal forums as well and most people are saying similar things. So, I think I have a pretty good plan now, looking like this:
    • Abstract class holds items conditional variables (any variable that might be changed at runtime) - this is the persistent object to be saved.
    • A data table holds references to what gameobject class would be used to represent item if it needs to be spawned in the game, as well as other static data like item weight, sprite, value, etc
    I believe like @TonyLi is saying, for a case like this I can probably just hold reference to those abstract classes in an array and save/load that. The only items I can imagine would need to be dynamically updated like this would be perishables, and player would not have anything beyond hundreds (that being an extreme).

    In that case, I think I'll start with the simplest method first of just dealing with the abstract data classes directly, but should there be a need this idea of using a map where i can hold some simple ID's with overridden variables is on the table, so I'll add it into my project wiki.

    And thanks for reference to the EAV model. I figured there must be some design patterns because i am not first person to do something like this, but i lacked vocabulary to find what i was looking for.

    Well, that is definitely enough plan to get started with. Should be able to work out little details from here.
     
    Ryiah, angrypenguin and TonyLi like this.
  5. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,619
    The one thing I'm unsure of there is your use of the term "abstract class". That term can be used in different ways, and since you're getting into this stuff it's worth checking in to make sure you're not using it confusingly (for you, later).

    In language generally, the way you're using the word is correct. You're talking about a class which represents a broad idea, and is therefore "abstract" in nature. However, in programming specifically, an "abstract class" is a class which can not be instantiated, which is a way to force programmers to implement concrete (non-abstract) derived classes where they fill in whatever blanks are relevant.

    Either approach will work here. That's just a word which can mean different things in different places, which might trip you up if you're not aware.
     
    Ryiah and BIGTIMEMASTER like this.
  6. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    @angrypenguin you are right i used it in the wrong place there.

    The parent class is abstract - it just holds common data to all items (name, weight, value, etc).
    But the children classes derived from that which are actually used directly at runtime are not abstract.

    In my case, making the parent class abstract doesnt really do anything because I already know its not the one to use. Setting it to abstract is just a protection in a larger team environment where not everyone will know the purpose of every thing and could cause bugs if they had access to things they don't need, right?
     
  7. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,619
    It both communicates and enforces that, yes. From a functional perspective, making a class "abstract" allows you to put abstract methods in it. These are virtual methods without a body, which are a blank spot which child classes are forced to fill in, otherwise the code won't compile (not out of pedantism, but because there's missing info). Usually an abstract class would have at least one abstract method in it. E.g. your Food class might have a CalculateSpoilage() abstract method in it, forcing every derived class to explicitly define how they spoil.

    A better example could be giving a Vehicle class an Accelerate() method. A car accelerates very differently to a helicopter, so that same intent ("go faster") might be implemented very differently for each, and unlike food spoilage there is no simple default implementation which can be shared between them. So that's a good case for a programmer to say "here's all of the common stuff, but we don't know how any given vehicle will accelerate, decelerate, or turn, so derived classes must fill that in for themselves".
     
    Ryiah and BIGTIMEMASTER like this.
  8. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    funny you think to mention that. It actually solves a problem that had me scratching my head just yesterday. Kept getting funny errors I couldn't figure out. I made a dumb workaround, but now I know what the actual problem was.
     
    Ryiah and angrypenguin like this.