Getting unique information for multiples copies of Scriptable Objects

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;
}
}

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.

1 Like

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.
8151530--1058942--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.

No, I didn’t say not to use scriptable objects at all, I said to wrap your scriptable objects in plain classes.

Quick example:

public class Item : ScriptableObject
{
    [field: SerializeField]
    public float ItemMaxDurability { get; } = 500f;

    //more immutable fields, etc here
}

[System.Serializable]
public class ItemWrapper
{
    public ItemWrapper(Item item)
    {
        BaseItem = item;
        CurrentItemDurability = item.ItemMaxDurability;
    }
 
    [field; SerializeField]
    public Item BaseItem { get; set; }

    [field: SerializeField]
    public float CurrentItemDurability { get; set; }

    //more mutable fields here
}

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.

2 Likes

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.

1 Like

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.

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.

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:

[CreateAssetMenu(menuName = "Inventory System: Inventory Item")]
public class InventoryItemData : ScriptableObject
{
    public int ID;
    public string DisplayName;
    [TextArea(4, 4)] public string Description;
    public Sprite Icon;
    public int MaxStackSize; //limits inventory system number of items in slot
    public GameObject ItemPrefab; //assigned in inspector
    public float collectibleMaxFreshValue; //the highest freshvalue each collectible can have
    public int collectibleValue; //base museum value of collectible
    public string collectibleQuality; //quality of collectible, poor > good > best


    public class ItemWrapper
    {
        public ItemWrapper(InventoryItemData item)
        {
            BaseItem = item;
            freshValue = item.collectibleMaxFreshValue;
        }

        [field: SerializeField]
        public InventoryItemData BaseItem { get; set; }

        [field: SerializeField]
        public float freshValue { get; set; }
    }
}

And here’s my inventory slot script:

[System.Serializable]

public class InventorySlot
{
    [SerializeField] public InventoryItemData itemData; //reference to what's in the slot
    [SerializeField] public int stackSize; //how many of the item does player have currently
    [SerializeField] public float freshValue;
    InventoryItemData inventoryItemData;

    //public getters for the above
    public InventoryItemData ItemData => itemData;
    public int StackSize => stackSize;


    public InventorySlot (InventoryItemData source, int amount) //Constructor for occupied slot
    {
        itemData = source;
        stackSize = amount;
    }


    public InventorySlot() //Constructor for default empty slot with no items in it
    {
        ClearSlot();[ICODE][/ICODE]
    }


    public void ClearSlot() //Clears the slot
    {
        itemData = null;
        stackSize = -1;
    }


    public void AssignItem(InventorySlot invSlot) //Assigns an item to an inventory slot
    {
        if (itemData == invSlot.ItemData) //Does the slot already contain the same item? If so, add to stack.
        {
            AddToStack(invSlot.stackSize);
            Debug.Log("invSlot.ItemData: " + invSlot.ItemData);  
        }


        else //Slot does not already contain the same item, so make a new one
        {
            itemData = invSlot.itemData;
            stackSize = 0;
            AddToStack(invSlot.stackSize);
        }
    }


    public void UpdateInventorySlot(InventoryItemData data, int amount) //Assigns data and amount directly, updates inventory slot
    {
        itemData = data;
        stackSize = amount;

        if (stackSize <= 0)
        {
            ClearSlot();
        }
    }


    public bool EnoughRoomLeftInStack(int amountToAdd, out int amountRemaining) //Is there enough room in stack for the amount attempted to add?
    {
        amountRemaining = itemData.MaxStackSize - stackSize;

        return EnoughRoomLeftInStack(amountToAdd);
    }


    public bool EnoughRoomLeftInStack(int amountToAdd) //Not sure how this is different from above?
    {
 
        if (itemData == null || itemData != null && stackSize + amountToAdd <= itemData.MaxStackSize) return true;
        else return false;
    }


    public void AddToStack(int amount)
    {
        stackSize += amount;
    }


    public void RemoveFromStack(int amount)
    {
        stackSize -= amount;
    }


    public bool SplitStack(out InventorySlot splitStack)
    {
        if (stackSize <= 1)
        {
            splitStack = null;
            return false;
        }

        int halfStack = Mathf.RoundToInt(stackSize / 2); //Divides stack size by 2 on splitting
        RemoveFromStack(halfStack);

        splitStack = new InventorySlot(itemData, halfStack);
        return true;
    }
}

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.

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…

[System.Serializable]

public class InventorySlot
{
    [SerializeField] public ItemWrapper itemWrapper; //reference to what's in the slot
    [SerializeField] public int stackSize; //how many of the item does player have currently
    [SerializeField] public float freshValue;
    InventoryItemData inventoryItemData;

    //public getters for the above (which may not be needed since I made the above private variables public after tutorial anyway
    public ItemWrapper ItemWrapper => ItemWrapper;
    public int StackSize => stackSize;


    public InventorySlot (ItemWrapper source, int amount) //Constructor for occupied slot
    {
        itemWrapper = source;
        stackSize = amount;
    }


    public InventorySlot() //Constructor for default empty slot with no items in it
    {
        ClearSlot();
    }


    public void ClearSlot() //Clears the slot
    {
        itemWrapper = null;
        stackSize = -1;
    }


    public void AssignItem(InventorySlot invSlot) //Assigns an item to an inventory slot
    {
        if (itemWrapper == invSlot.itemWrapper) //Does the slot already contain the same item? If so, add to stack
        {
            AddToStack(invSlot.stackSize);
            Debug.Log("invSlot.ItemData: " + invSlot.itemWrapper);  
        }

        else //Slot does not already contain the same item, so make a new one
        {
            itemWrapper = invSlot.itemWrapper;
            stackSize = 0;
            AddToStack(invSlot.stackSize);
        }
    }


    public void UpdateInventorySlot(ItemWrapper data, int amount/*, float freshness*/) //Assigns data and amount directly, updates inventory slot
    {
        itemWrapper = data;
        stackSize = amount;

        if (stackSize <= 0)
        {
            ClearSlot();
        }
    }


    public bool EnoughRoomLeftInStack(int amountToAdd, out int amountRemaining) //Is there enough room in stack for the amount attempted to add?
    {
        amountRemaining = ItemWrapper.BaseItem.MaxStackSize - stackSize;

        return EnoughRoomLeftInStack(amountToAdd);
    }


    public bool EnoughRoomLeftInStack(int amountToAdd) 
    {
 
        if (itemWrapper == null || itemWrapper != null && stackSize + amountToAdd <= ItemWrapper.BaseItem.MaxStackSize) return true;
        else return false;
    }


    public void AddToStack(int amount)
    {
        stackSize += amount;
    }


    public void RemoveFromStack(int amount)
    {
        stackSize -= amount;
    }


    public bool SplitStack(out InventorySlot splitStack)
    {
        if (stackSize <= 1)
        {
            splitStack = null;
            return false;
        }

        int halfStack = Mathf.RoundToInt(stackSize / 2); //Divides stack size by 2 on splitting
        RemoveFromStack(halfStack);

        splitStack = new InventorySlot(itemWrapper, halfStack);
        return true;
    }
}

You might want to actually explain what doesn’t work.

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:

public class InventorySlotUI : MonoBehaviour
{

    [SerializeField] private Image itemSprite;
    [SerializeField] private GameObject _slotHighlight;
    [SerializeField] private TextMeshProUGUI itemCount;
    [SerializeField] private TextMeshProUGUI collectibleFreshValue;
    [SerializeField] public InventorySlot assignedInventorySlot;
    private Button button;
    public InventorySlot AssignedInventorySlot => assignedInventorySlot;
    public InventoryDisplay ParentDisplay {get; private set;}


    private void Awake()
    {
        ClearSlot();

        button = GetComponent<Button>();
        button?.onClick.AddListener(OnUISlotClick);

        ParentDisplay = transform.parent.GetComponent<InventoryDisplay>();
    }


    public void Init(InventorySlot slot)
    {
        assignedInventorySlot = slot;
        UpdateUISlot(slot);
    }


    public void UpdateUISlot(InventorySlot slot)
    {
        if (slot.ItemWrapper != null)
        {
            itemSprite.sprite = slot.ItemWrapper.BaseItem.Icon;
            itemSprite.color = Color.white;
            if (slot.StackSize >= 1) itemCount.text = slot.StackSize.ToString(); //show item count text if stack has multiple collectibles
            collectibleFreshValue.text = slot.ItemWrapper.freshValue.ToString();

        }

        else
        {
            ClearSlot();
        }
    }


    public void ToggleHighlight()
    {
        _slotHighlight.SetActive(!_slotHighlight.activeInHierarchy);
    }


    public void UpdateUISlot() //allows for updating slot without passing in an inventory slot as in method above
    {
        if (assignedInventorySlot != null) UpdateUISlot(assignedInventorySlot);
    }


    public void ClearSlot()
    {
        AssignedInventorySlot?.ClearSlot();
        itemSprite.sprite = null;
        itemSprite.color = Color.clear;
        itemCount.text = "";
        collectibleFreshValue.text = "";
    }


    public void OnUISlotClick()
    {
        ParentDisplay?.SlotClicked(this);
    }



}

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.

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?

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.