How to Handle Scriptable Object Items that have to change Data at runtime?

121 views Asked by At

I have an Item class that stores ItemSO scriptable object

public class Item : MonoBehaviour
{
    [SerializeField] private ItemSO inventoryItem;
}

My Item SO is an abstract class that stores general info that every item has

public abstract class ItemSO : ScriptableObject
    {
        public string ItemName;
        public GameObject prefab;
    }

For every specific Item type, I made another Scriptable object that inherits from ItemSO

public class FuelItemSO : ItemSO, IDestroyableItem
{
    [Tooltip("Burn Time of the fuel in minutes")]
    public float BurnTimeIncreasePerKg;
    public float TemperatureIncreasePerKg;    
}

Now everything was working fine, while I was using my Scriptable objects as data containers as they are meant to be, but now I need to have another Item type, where I need to change the values of the variables. Let's say this class

public class ContainerItemSO : ItemSO, IDestroyableItem
{
    public float maxAmount;
    public float CurrentAmount;    
}

I can't seem to find a solution how the base Item class would GET, and SET different data according to the ItemSO. Like let's say it's a water container, and I want to add water into it.

I'm thinking of having an interface IHaveChangableData that ContainerItemSO will inherit from

public interface IHaveChangableData 
{
    ContainerData GetData();
}

and maybe a ContainerData class that will store the variables, so that I don't store them directly in the Item class

public class ContainerData
{
    //No idea how to store ItemSO specific data
}

idk if I'm overcomplicating stuff, but maybe someone could share a solution to this.

Also, should I consider so switching to classes from Scriptable objects if I want to have a coulpe of items with changeble data?

2

There are 2 answers

0
derHugo On

Depends a bit on your exact requirements.

The property approach as mentioned here is actually pretty clever and intuitive.


You could also e.g. simply use a copy instance at runtime:

public class Item : MonoBehaviour
{
    [SerializeField] private ItemSO _inventoryItem;

    private void Awake()
    {
        _inventoryItem = Instantiate(_inventoryItem);
    }
}

This creates a clone of the referenced ScriptableObject while entering playmode / when being spawned. So the original SO would never be touched and modified at all.

Drawback: If you need those all to be connected to the original SO then this would not work as every instance would end up with their own respective clone instance ;)


You could however introduce a global Singleton Manager to handle that and only create one clone per instance of any SO

public static class SOManager
{
    private readonly static Dictionary<ScriptableObject, ScriptableObject> cloneByOriginal = new();

    public static T GetCloneSave(T original) where T : ScriptableObject
    {
        if(!cloneByOriginal.TryGetValue(original, out var clone)
        {
            clone = Instantiate(original);
            cloneByOrigonal[original] = clone;
        }

        return clone as T;
    }
}

and then use

public class Item : MonoBehaviour
{
    [SerializeField] private ItemSO _inventoryItem;

    private void Awake()
    {
        _inventoryItem = SOManager.GetCloneSave(_inventoryItem);
    }
}

This way there should only be created a clone once and every other user of the same SO - which has to also use the upper way for getting the clone - would get the same SO instance so they are all connected again but to a runtime clone instance of the original SO without ever touching the original one.


Typing only on the phone but I hope the idea gets clear

5
Cool guy On

it's not very straightforward to revert runtime changes to scriptable objects, as they're designed around the idea of being configurations rather than runtime data. However if you're looking for a way to do this anyway, my suggestion is not to serialize your runtime data, so for example your ContainerItemSO class would look like so:

public class ContainerItemSO : ItemSO, IDestroyableItem
{
    public float maxAmount;
    [NotSerialized]
    public float CurrentAmount;    
}

or make it a property, whatever works best for you.

If you want the runtime data be inspectable and serializable (maybe for testing scenarios?) you can create a separate type for runtime data and make a copy on OnEnable

public class ContainerItemSO : ItemSO, IDestroyableItem
{  
    public float maxAmount;
    public RuntimeData runtimeData { get; set; }
 
    [SerializeField] 
    private RuntimeData serializedRuntimeData;
     
    private void OnEnable()
    {
        runtimeData = serializedRuntimeData;
    }

    [Serializable]
    public struct RuntimeData
    {
        public float CurrentAmount;
    }
}