ScriptableObject instance vs Class from SO values

Hello,

I would like to know if it’s better to create a ScriptableObject instance at runtime (editor-only, to prevent modifications to default values) or create a class using the ScriptableObject values. Example: suppose I have the UpgradeSO as shown below.

public class UpgradeSO : ScriptableObject
{
    public string title = "New Upgrade";
    public int level = 0;
    public PreRequisiteUpgrade[] upgradeRequisites;
}

[System.Serializable] public struct PreRequisiteUpgrade
{
    public int minLevelNeeded;
    public UpgradeSO upgradeNeeded;
}

So, I want to be able to display the upgrade on the upgrade store as long as all pre-requisites are met. Example: upgradeA has, in it’s upgradeRequisites[0], minLevelNeed = 5 and upgradeNeeded = upgradeB (set in the inspector). So upgradeA should only be displayed on the store as long as upgradeB is at level 5 or more. So far so good.

In my GameManager script I have an array of upgrades (UpgradeSO[ ] upgrades) that loads from all UpgradeSO in the Resources folder. The problem is: if I make a change to any value of upgradeA (ex: level = 2), it modifies the actual asset on the Resources folder, so if I re-enter play mode it will begin at level 2.

The solution I found is to instantiate every UpgradeSO on the Awake() function of GameManager, for editor only. But the problem is: I have to instantiate every UpgradeSO in the upgrades array and then, for each upgrade, loop through its upgradeRequisites, find the UpgradeSO (instance) in the upgrades array for which the title corresponds to upgradeNeeded.title and assign it to upgradeRequisites.upgradeNeeded, because of I don’t do this, upgradeNeeded will continue referencing the actual asset on Resources folder. This is presented below:

    private void Awake()
    {
        for (int i = 0; i < upgrades.Length; i++)
                upgrades[i] = Instantiate(upgrades[i]);
        for (int i = 0; i < upgrades.Length; i++)
            for (int j = 0; j < upgrades[i].upgradeRequisites.Length; j++)
                upgrades[i].upgradeRequisites[j].upgrade = Array.Find(upgrades, element => element.title == upgrades[i].upgradeRequisites[j].upgrade.title);
    }

Also, I will have later other fields that contain references to ScriptableObjects, so I would need to instantiate them all first and loop again etc.

I also thought of creating an Upgrade Class that has a constructor that takes an UpgradeSO as argument and gets all the data from that UpgradeSO. This way, I could create objects of that class from the UpgradeSO in the Resource folder, and use it as I would use the ScriptableObject without modifying the assets.

What should I do to solve this problem? I’m kinda lost of what is better here (both to organize better and to have better performance) and would like any help.

Thanks!

Don’t fall into the trap of trying to do too much with ScriptableObjects. They are good for one thing only in my opinion, and that is as a static data repository.

I use ScriptableObjects for template data only. Creating a new ScriptableObject instance at runtime serves no tangible benefit over a plain old C# object. For example in one of my projects I have a ShipSO which is the template for a Ship object. Ship has a constructor that accepts a ShipSO and copies over the template data from the scriptableObject. Ship is the object I actually use logically in the game. It’s the thing that takes damage and gets buffs etc… Ship is a JsonSerializable object and is what I actually use for my savegames. When I load a savegame I load that data directly into a Ship object. So the scriptableObject only comes into play when I’m creating a brand new thing and need template data to populate it.

1 Like

So you would recommend the approach I though of? Create an Upgrade Class that has a constructor that accepts an UpgradeSO and fills in the data from that SO?

The thing is that I would like to be able to create multiple upgrades on the go without having to assign them to any GameObject; I would create the SO on the Resource folders and it would automatically be included in the game. So I would, at Awake(), fill in the array of upgrades (Upgrade[ ] upgrades) with objects of class Upgrade, using each loaded UpgradeSO from Resources. Something like:

    // Have a Class Upgrade that has a constructor that accepts an UpgradeSO as argument.
    private void Awake()
    {
        UpgradeSO[] loadedUpgrades = Resources.LoadAll<UpgradeSO>("Upgrades");

        for (int i = 0; i < loadedUpgrades.Length; i++)
            upgrades.Add(new Upgrade(loadedUpgrades[i]));
    }

Yep that’s exactly what I would recommend.