Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Sharing Inventory and World GameObjects, or persisting data between them?

Discussion in 'Getting Started' started by viveleroi, May 31, 2018.

  1. viveleroi

    viveleroi

    Joined:
    Jan 2, 2017
    Posts:
    91
    I need input/advice on how to approach this scenario...

    I'm early into a 3D game and am planning how to design GameObjects for "items" - things that can be contained in an inventory but also dropped in the world.

    When in-world, they need a mesh renderer, collider, sometimes particle effects (like for a torch), etc.

    When in-inventory they need an image renderer.

    I had originally planned to simply make two gameobjects for each "item". However, I've realized that I can't "persist" data.

    For example: when held or dropped in-world, a torch burns and eventually runs out. Yet when picked up, I can't destroy the world object and create the UI object because I lose that "durability" value.

    I only see two solutions:

    1. Come up with some way to persist data between the two objects. Maybe when creating the inventory item I can "serialize" the data I need, and restore that data when re-creating the world object. So far this is the cleanest approach I can think of, but it doesn't excite me, it feels like the "least evil" method.
    2. Find a way to share one gameobject, disabling all world-specific components when its in the inventory. This seems like it's worse than the problem I'm trying to solve.
    Are there other solutions? I'm going to have a lot of items and I want this to remain flexible/extensible.
     
  2. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I think there should be one object (in the C# sense, not a Unity GameObject) that represents each item. It contains the persistent data. It also has methods to create a GameObject that represents it, either in the world-mode or in inventory-mode. And those objects can include a little component that links back to the C# object.

    So, when you spawn a new object out of nothing, you create the plain-old C# object first, and then ask it to create the proper GameObject to put in the world or inventory. And when you move an item between world and inventory, you grab a reference to the C# object, destroy the GameObject, and then ask it to create a new GameObject for the new context.
     
  3. viveleroi

    viveleroi

    Joined:
    Jan 2, 2017
    Posts:
    91
    This seems like a good approach, but I'm not sure it work with how flexible I was going to design items/world objects.

    For example I'm planning on designing items/world-objects with an "entity component system" approach. A Campfire "item" might have a "FuelComponent", a "battery" item would have an "EnergyComponent", tools would have "DurabilityComponent"s, etc. Using this system I don't need a new class for every type of item, but if I go with your suggested approach, I would need a custom class for every item which defines which data/components to "persist", right?
     
  4. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    No, you can do an entity/component system with plain old C# classes too; you don't need Unity's support to do it.

    The fundamental problem you've run into is that Unity is a basically a view API. But it's such a nice design (with IDE support, etc.) that you (we) are inclined to use it for everything. Trouble is, it's still just view — so what happens to the model part of your model-view-controller relationship? It gets mixed in with the view. And that becomes quite problematic in cases like this, where there are multiple very different views for the same model (or in cases where you need to run the model without any view at all, as in a simulation game; or when you want to sync the model across a network, etc.).

    So in such cases, I would encourage you to separate them, even if that feels a little painful at first. Make your model using whatever design patterns (including entity/component) you like, but keep it separate from the Unity scene graph. Think of the Unity scene graph as strictly the view (and, if we're honest, probably some of the controllers too). You should be able to build this view from the model at any time.

    I think you'll find that the view classes are actually pretty simple in this case. All the logic of "fuel-ness" or "battery-ness" or "weapon-ness" etc. should be in the model, not in the view. All the view needs to do is draw an icon or 3D model, with appropriate particle effects or trail renderers or whatnot. And then pass any collision events, frame-update events, etc. off to the model to figure out what's supposed to happen.
     
  5. viveleroi

    viveleroi

    Joined:
    Jan 2, 2017
    Posts:
    91
    That makes a lot of sense, and I can see how easy it is to fall into the trap. What you describe sounds a lot more like what I kept trying to make happen when I first started in Unity a year ago.

    However, I'm trying to envision a way to do this without having to duplicate "components". In my original post I was hoping to avoid duplicating or passing data back and forth but it sounds like that's the approach you're recommending.

    This is a super dumbed-down pseudo-code example just to see if I'm understanding what you're suggesting.

    Effectively I build a pure-c# model of what my item can be, allow that to "build" a world gameobject, and a ui gameobject.

    However, in order to persist component data between them, the item would have to a) have something representing which components it uses, and b) pass that data to the gameobject on instantiation/spawn.

    On destruction, the world gameobject would have to "return" the data to the equivalent components of an "item" object.

    I'm not sure it's smart to have one component that can be used in both places - saving some code but having a subclass of MonoBehavior with update logic on an item might be confusing.

    Passing data back and forth seems tedious but seems necessary since I can't use one gameobject for both in-world and UI. It seems very much prone to human error, and if I allow new items via an API, tricky for users to get.

    Am I misunderstanding any of this?

    Code (CSharp):
    1. // A pure c# "model" object that "builds" UI and World gameobjects
    2. public class Torch() {
    3.     // Basic durability component, would normally be cached in a list of components or something
    4.     ECSDurabilityComponent durability = new ECSDurabilityComponent(0, 100);
    5.  
    6.     public GameObject BuildWorldObject() {
    7.         var gameObject = Pool.Spawn(...);
    8.  
    9.         // Copy over our own "durability" data to the gameobject
    10.         var goDurability = gameObject.GetComponent<DurabilityComponent>;
    11.         goDurability.Damage = durability.Damage;
    12.         goDurability.Durability = durability.Durability;
    13.  
    14.         return gameObject;
    15.     }
    16.  
    17.     public GameObject BuildUIObject() {
    18.         var gameObject = Pool.Spawn(UIImagePrefab, ...);
    19.         // set Image component to the sprite for this item
    20.  
    21.         return gameObject;
    22.     }
    23. }
    24.  
    25. // A basic class for logic on the in-world torch
    26. public class TorchGameObject {
    27.     DurabilityComponent _durability;
    28.  
    29.     void Awake() {
    30.         _durability = GetComponent<DurabilityComponent>();
    31.     }
    32.  
    33.     public Torch ToItem() {
    34.         var torch = new Torch();
    35.  
    36.         var itemDurability = torch.GetECSComponent<ECSDurabilityComponent>();
    37.  
    38.         // Copy over our "durability" data back to an item
    39.         itemDurability.Damage = _durability.Damage;
    40.         itemDurability.Durability = _durability.Durability;
    41.     }
    42.  
    43.     void Update() {
    44.         // Assume torch is always lit
    45.         _durability.Damage += Time.deltaTime;
    46.  
    47.         if (_durability.Damage >= _durability.Durability) {
    48.             Destroy(gameObject);
    49.         }
    50.     }
    51. }
    52.  
    53. // This would go on the in-world object
    54. public class DurabilityComponent : MonoBehaviour {
    55.     public float Damage;
    56.  
    57.     public float Durability;
    58. }
    Edit: Or maybe, after thinking a bit more, both UI and World GameObjects would store a reference to the actual "Item" class, and simply directly access the Component data there? That certainly reduces code and solves the data persistence issue, but it would prevent using any Update logic in the Item code itself, which may be for the best conceptually.
     
    Last edited: Jun 1, 2018
  6. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yes...

    No....



    But it doesn't necessarily preclude putting Update logic in the Item code itself. You just need some component on the GameObject to pass the Update call on through to the item. Items would only create that component when they actually need frame updates (I imagine many of them wouldn't). And note that the should do this only if something about the actual model needs to change frame to frame — I dunno, maybe something like a spell effect wearing off. Anything that is purely visual (weapon trails, particle effects, etc.) should happen only in the GameObject components.

    To me this makes a nice clear separation between (1) data and logic which is inherent to the item itself; (2) stuff related to the audiovisual display of that item in the world; and (3) stuff related to the visual display of that item in inventory. You always have (1), and then you may sometimes have (2) or (3) depending on where the thing is.
     
    CheekySparrow78 likes this.