ScriptableObject + SerializeReference = Universal Data Container?

ScriptableObject is actively pushed by UT as Architectual solution:

And you can find hundreds tutorials from community with this approach

Today at 2022 is it still optimal solution? Is it really universal, at all approaches? Lets Consider different approaches to data in development, and how we can (if can) resolve different requirements by ScriptableObject, and is this solution optimal or we need better solution from UT.

Shared State
Data example: Max HP

This approach is in every video-tutorial, as you can found, ScriptableObject is greatly share data between many MonoBehaviour-Instances.

How we can resolve this with ScriptableObject?
We create one ScriptableObject and assign it to all our target-user-scripts

Are available solutions optimal and by my opinion fully resolve approach?

  • yes

Non shared State
Data example: Current HP

The opposite for Shared, this approach expect that data is unique for every instance of target user-script.

How we can resolve this with ScriptableObject?
We can use ScriptableObject.CreateInstance() in MonoBehaviour.Start()

Are available solutions optimal and by my opinion fully resolve approach?

  • almost. only one problem is that if you create ScriptableObject dynamicly it has no file, and you can’t observe them all in one list. We can write some DynamicScriptableObjectExplorer, but may be UT should do this for us?
    Temporal
    Data example: Current HP
    Expects that value shouldn’t be saved between playmode-sessions and between buildrun-sessions.

How we can resolve this with ScriptableObject?
Buildrun: default ScriptableObject behaviour resets values to defaults at buildrun
Playmode: We can use MonoBehaviour.Start() to reset values in ScriptableObjects or InitializeOnEnterPlayMode attribute, but it should be used with static function so you need to implement some logic reinit all your instances of scriptableObjects.

Are available solutions optimal and by my opinion fully resolve approach?

  • partialiy resolved. In terms of Shared non-shared state Temporal values are always a problem, cause sometime we wanna share Temporal values between systems (share current HP for DamageSystem and HUD), but not share them between instances (Player, Enemy1, Enemy2 etc).

What is your expirience and approach with Temporal Data in ScriptableObjects?

Default for Temporal
Data example: Start HP
Shared or non-shared this value need to be available for our ResetTemporal() logic.

How we can resolve this with ScriptableObject?
Shared approach work fine here, we create one ScriptableObject and assign reference in our MonoBehaviours to it

Are available solutions optimal and by my opinion fully resolve approach?

  • yes

Hardcoded
Data example: Item ID
At this moment mutabilty of values of ScriptableObject is bad for identifiers. It can lead to human-factor errors, duplicative IDs, force developer to write boilerplate validation-systems.

How we can resolve this with ScriptableObject?
At this moment UT itself use code generation for hardcoded data, you can found example of this in UT Input System. It’s widely used to generate Enum, with hardcoded items list, this can be used for Item ID, with human-readable names, instead of simple Int32.

Are available solutions optimal and by my opinion fully resolve approach?

  • yes

Non Hardcoded
Data example: Item Database
We need to allow gamedesigners to expand item database, if possible - without programmer participation.

How we can resolve this with ScriptableObject?
We create Item : ScriptableObject and input-point for ScriptableObject-list (separated ItemsDB : ScriptableObject or allow to load Item : ScriptableObject by ItemID : int / string , from fixed address ).

Are available solutions optimal and by my opinion fully resolve approach?

  • yes

Overrides (prefab variant analogue)
Data example: Item Database
Items in database can differs from eachother only few values, for example Heal Potion 50%, Heal Potion 10% - this items has same Name, Icon, Description, behaviour, pickup prefab, pickup sound, only difference - heal value. Overrides allow to simplify long support, when we have dozens of mostly similar items.

How we can resolve this with ScriptableObject?

Are available solutions optimal and by my opinion fully resolve approach?

  • no. We are waiting builtin ScriptableObject Variants

Later:
Hierarchical
Flat (Sibling)
Serialization
polymorphysm
Version Control (Git)

Or you can use one or more scriptable objects encapsulating a plain class or struct, and just copy that object per run-time instance. No need to instance scriptable objects at runtime, it’s pointless imo.

Not really? C# has these things called Properties of which you can make read only through code. You can also, with minimal editor coding, make read only fields. There, problem solved.

I’ve been working on this, actually, using the old school prototype pattern as described by the that one book on game design patterns.

Here’s a little sample:

8513291--1134602--upload_2022-10-14_23-5-56.png

(Lots of Odin Inspector making this possible).

So not so much ‘overrides’, but you can make objects with various properties and them mash them together to build bigger items out of them. Right now I’ve only been playing with names, but the concept can be applied to… basically any property you could define in a component, which, of course, are being serialised with SerializeReference.

But honestly, SerializeReference is good at what it says on the box, serialising values by reference, so anything that’s not a UnityEngine.Object or a value type.

Same with scriptable objects. They’re good as sacks of data, sometimes for transmitting data between scenes, and a few other things if you get creative.

And if you want designer friendly, learn editor coding.

2 Likes

Shure it’s fine to clone plain class, instead of ScriptableObject. There is pros and cons with using ScriptableObject vs C# plain class, and main + of ScriptableObject is seprated Inspector.

Properties won’t help to defend user defined ID, you need to write own validation. And I’ve wroten that it’s not problem, there’re other approaches, point wasn’t to count and discuss them all. Certainly, you can write custom create function for creation ScriptableObject, define there unique ID and hide it from inspector. But you can’t disallow user to duplicate existing ScriptableObject, then you again need to validate ID’s externaly.

As you tell yourself - this is not overrides. That mean, that we can simplify maintenance of huge ScriptableObject-based databases. Your solution is just about solving another problem, I’m planning to write about it at this thread later, in Flat and Hierarchical approaches.

I’ve no problems with Editor coding. It doesn’t mean, that basic system and concepts shouldn’t progress.
For example:

  • earlier we haven’t SerializeReference, and no polymorphism with at plan classes. And it changed, and it’s great
  • earlier we haven’t prefab overrides
  • earlier we haven’t Material Variants
    etc
1 Like

Here’s my ScriptableObject-based data package: Datasacks!

Ultra simple, very useful in many different Unity contexts.

Datasacks is presently hosted at these locations:

https://bitbucket.org/kurtdekker/datasacks

https://github.com/kurtdekker/datasacks

1 Like

You know you can make plain classes editable in the inspector?

public class MyScriptableObject : ScriptableObject
{
    [SerializeField]
    private MyClass myClass = new myClass();
   
    public MyClass GetMyClassInstance() => return new MyClass(myClass);
}

[System.Serializable]
public class MyClass
{
    [SerializeField]
    private int _someInt;
   
    [SerializeField]
    private string _someString;
   
    public int SomeInt => _someInt;
   
    public string SomeStrong => _someString;
   
    public MyClass() { }
   
    public MyClass(MyClass myClass)
    {
        _someInt = myClass._someInt;
        _someString = myClass._someString;
    }
}

See, simple. Best of both worlds.

I mean, and that’s a problem Unity is never going to solve for us. Once again it comes down to producing the editor tools you and your team need to suit your work-flow, or getting addons that provide that functionality out of the box.

My rule of thumb is…ScriptableObjects should be reusable. If you’re only ever to have 1…but it we used everywhere, cool. If you have 1, but intend to create multiple instances from it, cool. If you have 1 for a specific task and that’s it…you probably shouldn’t be using it. A more intuitve way to know if you’re doing this right is, you gotta have much more scriptable object instances than scriptable objects. If you find yourself in a situation where you have 1 to 1 create scriptabeobject and then create an instance for i, and then create another scriptable object and an instance for it, etc, you are probably doing it wrong. Use SerializedReference instead.