Search Unity

Resolved Getting unique information for multiples copies of Scriptable Objects

Discussion in 'Scripting' started by ruanweber007, May 24, 2022.

  1. ruanweber007

    ruanweber007

    Joined:
    Feb 18, 2021
    Posts:
    3
    Hello people,
    I am trying to have equipments on my game that can be created several times
    with the scriptable objects, two equipments will have the same information,
    if this informations changes for one copy, then the other copy of the specific equipment happen to change too.

    I know this is the behaviour of the scriptable objects, but what i need is like having, for example, two swords of the same scriptable object with diferent actual durability.

    actualy, this is possible to do with unity?
    below i am trying to create a second class called Durability on the same script,
    but i am struggling to get this information after, someone can help?

    SCRIPT item.cs:
    using UnityEngine;

    /* The base item class. All items should derive from this. */

    [CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item")]
    public class Item : ScriptableObject
    {

    new public string name = "New Item"; // Name of the item
    public Sprite icon = null; // Item icon
    public bool showInInventory = true;

    public int maxqtd;
    public int qtd;
    public int uses;

    public float durMax;
    public float currentDur;
    public bool isBroken;

    public bool isConsumable;

    public Durability newDurability()
    {
    Durability durability = new Durability(this);
    return durability;
    }

    // Called when the item is pressed in the inventory
    public virtual void Use()
    {
    // Use the item
    // Something may happen
    }

    // Call this method to remove the item from inventory
    public void RemoveFromInventory()
    {
    Inventory.instance.Remove(this, 0);
    }

    }

    [System.Serializable]
    public class Durability
    {
    public float durMax;
    public float currentDur;
    public bool isBroken;

    public Durability(Item item)
    {
    durMax = item.durMax;
    currentDur = item.currentDur;
    isBroken = item.isBroken;
    }

    public void setDurability(float durMaxUP, float currentDurUP)
    {
    durMax = durMaxUP;
    currentDur = currentDurUP;
    if (currentDur <= 0)
    {
    isBroken = true;
    }
    else
    {
    isBroken = false;
    }
    }

    public float getCurrentDurability()
    {
    return currentDur;
    }

    public float getmaxDurability()
    {
    return durMax;
    }

    public bool getIsBroken()
    {
    return isBroken;
    }
    }
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,819
    You can Instantiate() scriptable objects like any other Unity Object to get a duplicate, thus modifying the values of duplicate will not affect the source SO. However, this does bring about many other issues such as serialisation, saving/loading, etc.

    I'm of the opinion that if you're duplicating SO's at run time, you should just be using plain classes.

    You could look at wrapping your scriptable objects in plain C# classes, which can be instantiated the traditional C# way, and use this to store the mutable data, while keeping the immutable data inside the SO still. This plays better with serialisation and easier to work with when saving/loading.
     
    ruanweber007 likes this.
  3. ruanweber007

    ruanweber007

    Joined:
    Feb 18, 2021
    Posts:
    3
    Thanks for your response Spiney,
    instantiate the scriptable object is out of question then, i also want to persist this information.

    i have created this second class bellow that is not a scriptable object,
    but is in the same C# script. I thought since this class is not a scriptable object, it will have unique information for each instanciated object.
    upload_2022-5-24_2-24-22.png

    but i guess i will have to change to not use scriptable object at all and use only plain C# classes as you suggested.
    Thanks for the help.
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,819
    No, I didn't say not to use scriptable objects at all, I said to wrap your scriptable objects in plain classes.

    Quick example:
    Code (CSharp):
    1. public class Item : ScriptableObject
    2. {
    3.     [field: SerializeField]
    4.     public float ItemMaxDurability { get; } = 500f;
    5.  
    6.     //more immutable fields, etc here
    7. }
    8.  
    9. [System.Serializable]
    10. public class ItemWrapper
    11. {
    12.     public ItemWrapper(Item item)
    13.     {
    14.         BaseItem = item;
    15.         CurrentItemDurability = item.ItemMaxDurability;
    16.     }
    17.  
    18.     [field; SerializeField]
    19.     public Item BaseItem { get; set; }
    20.  
    21.     [field: SerializeField]
    22.     public float CurrentItemDurability { get; set; }
    23.  
    24.     //more mutable fields here
    25. }
    Then in your inventory you would use instances of ItemWrapper, which you can make new ones of with
    ItemWrapper itemWrapper = new ItemWrapper(item);


    This is a very rough example to explain the concept as well.

    Defining the class inside the SO doesn't change that an instance of Durability declared inside an SO belongs to that instance of the SO.
     
    Last edited: May 24, 2022
    CodeRonnie and ruanweber007 like this.
  5. ruanweber007

    ruanweber007

    Joined:
    Feb 18, 2021
    Posts:
    3
    With a lot of work, i changed the entire project to adapt to the change you suggested and worked very well.

    I was missing to understand that sometimes i was needing to pass the scriptable object as a parameter and with this, the solution was just instantiate the ItemWrapper as you mentioned.
    using the concept of "BaseItem" make me understand.
    Thank you very much for your help Spiney.
     
    Last edited: May 29, 2022
    spiney199 likes this.
  6. kcenci

    kcenci

    Joined:
    Feb 27, 2022
    Posts:
    5
    Hi @spiney199 , thanks so much for your advice here. I'm in a similar situation - does the solution in this thread work if the scriptable objects are linked to game objects? So for example, I have an "itemdata" scriptable object base template, and a "collectible" game object with mutable qualities that references that scriptable object in the inspector.
     
  7. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,819
    In the examples I gave before, it's better to think of the 'Item Wrapper' example as an more of an 'Item Instance' (which is what I should call them moving forward), as in, a live instance of an item in the game, which references an item scriptable object to draw the majority of its information from.

    Then you can have another class to represent an item instance, and the quantity of that item. I've called this an 'Item Listing' in projects.

    So I guess it comes down to your requirements. If you need mutable data for items, then you may want an Item Instance wrapper class. If you just need items and quantity, then have an Item Listing wrapper. If you need both, use both.
     
  8. kcenci

    kcenci

    Joined:
    Feb 27, 2022
    Posts:
    5
    I see. I'm not totally clear on how to connect the Item Wrapper to the inventory slot script however. Can you please take a look at the code below and make a recommendation? The "freshValue" is the mutable quality for each item.


    Here's my Item Data Scriptable Object:

    Code (CSharp):
    1. [CreateAssetMenu(menuName = "Inventory System: Inventory Item")]
    2. public class InventoryItemData : ScriptableObject
    3. {
    4.     public int ID;
    5.     public string DisplayName;
    6.     [TextArea(4, 4)] public string Description;
    7.     public Sprite Icon;
    8.     public int MaxStackSize; //limits inventory system number of items in slot
    9.     public GameObject ItemPrefab; //assigned in inspector
    10.     public float collectibleMaxFreshValue; //the highest freshvalue each collectible can have
    11.     public int collectibleValue; //base museum value of collectible
    12.     public string collectibleQuality; //quality of collectible, poor > good > best
    13.  
    14.  
    15.     public class ItemWrapper
    16.     {
    17.         public ItemWrapper(InventoryItemData item)
    18.         {
    19.             BaseItem = item;
    20.             freshValue = item.collectibleMaxFreshValue;
    21.         }
    22.  
    23.         [field: SerializeField]
    24.         public InventoryItemData BaseItem { get; set; }
    25.  
    26.         [field: SerializeField]
    27.         public float freshValue { get; set; }
    28.     }
    29. }



    And here's my inventory slot script:

    Code (CSharp):
    1. [System.Serializable]
    2.  
    3. public class InventorySlot
    4. {
    5.     [SerializeField] public InventoryItemData itemData; //reference to what's in the slot
    6.     [SerializeField] public int stackSize; //how many of the item does player have currently
    7.     [SerializeField] public float freshValue;
    8.     InventoryItemData inventoryItemData;
    9.  
    10.     //public getters for the above
    11.     public InventoryItemData ItemData => itemData;
    12.     public int StackSize => stackSize;
    13.  
    14.  
    15.     public InventorySlot (InventoryItemData source, int amount) //Constructor for occupied slot
    16.     {
    17.         itemData = source;
    18.         stackSize = amount;
    19.     }
    20.  
    21.  
    22.     public InventorySlot() //Constructor for default empty slot with no items in it
    23.     {
    24.         ClearSlot();[ICODE][/ICODE]
    25.     }
    26.  
    27.  
    28.     public void ClearSlot() //Clears the slot
    29.     {
    30.         itemData = null;
    31.         stackSize = -1;
    32.     }
    33.  
    34.  
    35.     public void AssignItem(InventorySlot invSlot) //Assigns an item to an inventory slot
    36.     {
    37.         if (itemData == invSlot.ItemData) //Does the slot already contain the same item? If so, add to stack.
    38.         {
    39.             AddToStack(invSlot.stackSize);
    40.             Debug.Log("invSlot.ItemData: " + invSlot.ItemData);  
    41.         }
    42.  
    43.  
    44.         else //Slot does not already contain the same item, so make a new one
    45.         {
    46.             itemData = invSlot.itemData;
    47.             stackSize = 0;
    48.             AddToStack(invSlot.stackSize);
    49.         }
    50.     }
    51.  
    52.  
    53.     public void UpdateInventorySlot(InventoryItemData data, int amount) //Assigns data and amount directly, updates inventory slot
    54.     {
    55.         itemData = data;
    56.         stackSize = amount;
    57.  
    58.         if (stackSize <= 0)
    59.         {
    60.             ClearSlot();
    61.         }
    62.     }
    63.  
    64.  
    65.     public bool EnoughRoomLeftInStack(int amountToAdd, out int amountRemaining) //Is there enough room in stack for the amount attempted to add?
    66.     {
    67.         amountRemaining = itemData.MaxStackSize - stackSize;
    68.  
    69.         return EnoughRoomLeftInStack(amountToAdd);
    70.     }
    71.  
    72.  
    73.     public bool EnoughRoomLeftInStack(int amountToAdd) //Not sure how this is different from above?
    74.     {
    75.  
    76.         if (itemData == null || itemData != null && stackSize + amountToAdd <= itemData.MaxStackSize) return true;
    77.         else return false;
    78.     }
    79.  
    80.  
    81.     public void AddToStack(int amount)
    82.     {
    83.         stackSize += amount;
    84.     }
    85.  
    86.  
    87.     public void RemoveFromStack(int amount)
    88.     {
    89.         stackSize -= amount;
    90.     }
    91.  
    92.  
    93.     public bool SplitStack(out InventorySlot splitStack)
    94.     {
    95.         if (stackSize <= 1)
    96.         {
    97.             splitStack = null;
    98.             return false;
    99.         }
    100.  
    101.         int halfStack = Mathf.RoundToInt(stackSize / 2); //Divides stack size by 2 on splitting
    102.         RemoveFromStack(halfStack);
    103.  
    104.         splitStack = new InventorySlot(itemData, halfStack);
    105.         return true;
    106.     }
    107. }
    108.  
     
  9. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,819
    Well you've just nested the ItemWrapper class definition in your inventory scriptable object (should be it's own script asset). You haven't actually used an instance of it anywhere.

    Basically, where-ever you use an item at runtime, you use an instance of the wrapper rather than the raw scriptable object reference.

    You're kinda already doing what you want with the InventorySlot class anyway. All you need to do replace the
    itemData
    and
    freshValue
    fields with just the one ItemWrapper instance instead.
     
  10. kcenci

    kcenci

    Joined:
    Feb 27, 2022
    Posts:
    5
    Got it, thanks. I separated the Wrapper into a separate script. I haven't used wrappers before so I'm not certain how to use an instance in the InventorySlot script. Below I tried replacing the InventoryItemData references with ItemWrapper instead but it doesn't seem to work - any ideas what I'm doing wrong? Not sure if I should be linking back to the ItemData SO with ItemWrapper.BaseItem.[variable] anywhere...

    Code (CSharp):
    1. [System.Serializable]
    2.  
    3. public class InventorySlot
    4. {
    5.     [SerializeField] public ItemWrapper itemWrapper; //reference to what's in the slot
    6.     [SerializeField] public int stackSize; //how many of the item does player have currently
    7.     [SerializeField] public float freshValue;
    8.     InventoryItemData inventoryItemData;
    9.  
    10.     //public getters for the above (which may not be needed since I made the above private variables public after tutorial anyway
    11.     public ItemWrapper ItemWrapper => ItemWrapper;
    12.     public int StackSize => stackSize;
    13.  
    14.  
    15.     public InventorySlot (ItemWrapper source, int amount) //Constructor for occupied slot
    16.     {
    17.         itemWrapper = source;
    18.         stackSize = amount;
    19.     }
    20.  
    21.  
    22.     public InventorySlot() //Constructor for default empty slot with no items in it
    23.     {
    24.         ClearSlot();
    25.     }
    26.  
    27.  
    28.     public void ClearSlot() //Clears the slot
    29.     {
    30.         itemWrapper = null;
    31.         stackSize = -1;
    32.     }
    33.  
    34.  
    35.     public void AssignItem(InventorySlot invSlot) //Assigns an item to an inventory slot
    36.     {
    37.         if (itemWrapper == invSlot.itemWrapper) //Does the slot already contain the same item? If so, add to stack
    38.         {
    39.             AddToStack(invSlot.stackSize);
    40.             Debug.Log("invSlot.ItemData: " + invSlot.itemWrapper);  
    41.         }
    42.  
    43.         else //Slot does not already contain the same item, so make a new one
    44.         {
    45.             itemWrapper = invSlot.itemWrapper;
    46.             stackSize = 0;
    47.             AddToStack(invSlot.stackSize);
    48.         }
    49.     }
    50.  
    51.  
    52.     public void UpdateInventorySlot(ItemWrapper data, int amount/*, float freshness*/) //Assigns data and amount directly, updates inventory slot
    53.     {
    54.         itemWrapper = data;
    55.         stackSize = amount;
    56.  
    57.         if (stackSize <= 0)
    58.         {
    59.             ClearSlot();
    60.         }
    61.     }
    62.  
    63.  
    64.     public bool EnoughRoomLeftInStack(int amountToAdd, out int amountRemaining) //Is there enough room in stack for the amount attempted to add?
    65.     {
    66.         amountRemaining = ItemWrapper.BaseItem.MaxStackSize - stackSize;
    67.  
    68.         return EnoughRoomLeftInStack(amountToAdd);
    69.     }
    70.  
    71.  
    72.     public bool EnoughRoomLeftInStack(int amountToAdd)
    73.     {
    74.  
    75.         if (itemWrapper == null || itemWrapper != null && stackSize + amountToAdd <= ItemWrapper.BaseItem.MaxStackSize) return true;
    76.         else return false;
    77.     }
    78.  
    79.  
    80.     public void AddToStack(int amount)
    81.     {
    82.         stackSize += amount;
    83.     }
    84.  
    85.  
    86.     public void RemoveFromStack(int amount)
    87.     {
    88.         stackSize -= amount;
    89.     }
    90.  
    91.  
    92.     public bool SplitStack(out InventorySlot splitStack)
    93.     {
    94.         if (stackSize <= 1)
    95.         {
    96.             splitStack = null;
    97.             return false;
    98.         }
    99.  
    100.         int halfStack = Mathf.RoundToInt(stackSize / 2); //Divides stack size by 2 on splitting
    101.         RemoveFromStack(halfStack);
    102.  
    103.         splitStack = new InventorySlot(itemWrapper, halfStack);
    104.         return true;
    105.     }
    106. }
     
  11. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,819
    You might want to actually explain what doesn't work.
     
  12. kcenci

    kcenci

    Joined:
    Feb 27, 2022
    Posts:
    5
    Yes - what doesn't work is that when I pick up an item and add it to my inventory, the inventory slot remains blank (no sprite image populates but the pick up action has successfully been done. There are a number of inventory scripts in addition to the ones I mentioned above that handle other functionality, like this InventorySlotUI.cs below. If I had to guess, there's something wrong about how I'm populating the sprite and other info in the UpdateUISlot() method, which previously referenced InventoryItemData script above instead of the new ItemWrapper:


    Code (CSharp):
    1. public class InventorySlotUI : MonoBehaviour
    2. {
    3.  
    4.     [SerializeField] private Image itemSprite;
    5.     [SerializeField] private GameObject _slotHighlight;
    6.     [SerializeField] private TextMeshProUGUI itemCount;
    7.     [SerializeField] private TextMeshProUGUI collectibleFreshValue;
    8.     [SerializeField] public InventorySlot assignedInventorySlot;
    9.     private Button button;
    10.     public InventorySlot AssignedInventorySlot => assignedInventorySlot;
    11.     public InventoryDisplay ParentDisplay {get; private set;}
    12.  
    13.  
    14.     private void Awake()
    15.     {
    16.         ClearSlot();
    17.  
    18.         button = GetComponent<Button>();
    19.         button?.onClick.AddListener(OnUISlotClick);
    20.  
    21.         ParentDisplay = transform.parent.GetComponent<InventoryDisplay>();
    22.     }
    23.  
    24.  
    25.     public void Init(InventorySlot slot)
    26.     {
    27.         assignedInventorySlot = slot;
    28.         UpdateUISlot(slot);
    29.     }
    30.  
    31.  
    32.     public void UpdateUISlot(InventorySlot slot)
    33.     {
    34.         if (slot.ItemWrapper != null)
    35.         {
    36.             itemSprite.sprite = slot.ItemWrapper.BaseItem.Icon;
    37.             itemSprite.color = Color.white;
    38.             if (slot.StackSize >= 1) itemCount.text = slot.StackSize.ToString(); //show item count text if stack has multiple collectibles
    39.             collectibleFreshValue.text = slot.ItemWrapper.freshValue.ToString();
    40.  
    41.         }
    42.  
    43.         else
    44.         {
    45.             ClearSlot();
    46.         }
    47.     }
    48.  
    49.  
    50.     public void ToggleHighlight()
    51.     {
    52.         _slotHighlight.SetActive(!_slotHighlight.activeInHierarchy);
    53.     }
    54.  
    55.  
    56.     public void UpdateUISlot() //allows for updating slot without passing in an inventory slot as in method above
    57.     {
    58.         if (assignedInventorySlot != null) UpdateUISlot(assignedInventorySlot);
    59.     }
    60.  
    61.  
    62.     public void ClearSlot()
    63.     {
    64.         AssignedInventorySlot?.ClearSlot();
    65.         itemSprite.sprite = null;
    66.         itemSprite.color = Color.clear;
    67.         itemCount.text = "";
    68.         collectibleFreshValue.text = "";
    69.     }
    70.  
    71.  
    72.     public void OnUISlotClick()
    73.     {
    74.         ParentDisplay?.SlotClicked(this);
    75.     }
    76.  
    77.  
    78.  
    79. }
     
  13. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,819
    I think this is something you need to debug. Ergo, does the wrapper actually have a referenced item, or is the code even running at all.
     
  14. kcenci

    kcenci

    Joined:
    Feb 27, 2022
    Posts:
    5
    After more investigation, it seems like the former. The ItemWrapper is null and not populating with any values from the InventoryItemData SO... I think I'm still missing something about using an instance of the wrapper. Can you say more about where the ItemWrapper itemWrapper = new ItemWrapper(item); fits in?
     
  15. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,819
    It can't be both null, and not populated with values. It's either null or it isn't.

    You just make a new wrapper when a new live item enters the game world. Otherwise that same wrapper should persist across all cases until the item is destroyed, consumed, etc. It's a record of the item's current state after all.

    Otherwise it's just a regular C# reference type object.