Search Unity

ScriptableObject vs plain C# class

Discussion in 'Scripting' started by tiggus, May 26, 2015.

  1. tiggus

    tiggus

    Joined:
    Sep 2, 2010
    Posts:
    1,240
    See this answers question: http://answers.unity3d.com/questions/190350/what-is-the-purpose-of-scriptableobject-versus-nor.html

    I've seen a few differing opinions on this when I do a search - all of them by non unity staff. Some state ScriptableObject is only needed for serialization, some state that plain C# classes won't garbage collect properly, etc.

    Is there an official response(or can we get one?) on whether ScriptableObject is purely for serialization and plain C# classes are GC'ed in the normal and expected way if we don't need serialization?
     
  2. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    From http://docs.unity3d.com/Manual/class-ScriptableObject.html:

    Let's say you have Shopkeeper with a regular gameObject, a prefab called "ShopModel" using MonoBehaviour to store data instead ScriptableObject. If you would have 400 shopkeepers in same map, they would initialise their own ShopModel gameObjects, thus gobble up memory for nothing.

    Using scriptable object, they would instantiate, but point their ShopModel to the same ScriptableObject.

    That's how I understood ScriptableObject, someone can further explain if I missed/misunderstood something.

    As for regular classes, I think they are fine with GC, they will get collected if they are not referenced. But downside is, you can't see their content in inspector without some extra work.
     
    aimansarie likes this.
  3. tiggus

    tiggus

    Joined:
    Sep 2, 2010
    Posts:
    1,240
    So to take your example let's say I just define and instantiate my own C# class called ShopModel(maybe it points to a SQL database for instance) and reference it from ShopKeepers. In this case what is the real difference between a ScriptableObject derived ShopModel class and a "plain" C# ShopModel class - with regards to memory usage and collection.
     
  4. ThermalFusion

    ThermalFusion

    Joined:
    May 1, 2011
    Posts:
    906
    I suspected ScriptableObjects behaved more like assets rather than C# objects and decided to test this.
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. public class MyScriptableObject : ScriptableObject {
    6.  
    7.     public Vector3[] randomVectors;
    8.  
    9.     public void SpawnRandomVectors() {
    10.         int length = 1024;
    11.         randomVectors = new Vector3[length];
    12.         for (int i = 0; i < length; i++) {
    13.             randomVectors[i] = Random.onUnitSphere;
    14.         }
    15.     }
    16. }
    17.  
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. public class SpawnScriptableObjects : MonoBehaviour {
    6.  
    7.     public int numToSpawn = 64;
    8.     public void OnGUI() {
    9.         if (GUILayout.Button("Spawn")) {
    10.             Spawn();
    11.         }
    12.         if (GUILayout.Button("Clear")) {
    13.             Clear ();
    14.         }
    15.         if (GUILayout.Button("Delete")) {
    16.             Delete();
    17.         }
    18.         if (GUILayout.Button("Debug")) {
    19.             PrintDebug(false);
    20.         }
    21.         if (GUILayout.Button("Collect")) {
    22.             Collect();
    23.         }
    24.         if (GUILayout.Button("Unload")) {
    25.             Unload();
    26.         }
    27.     }
    28.  
    29.     private MyScriptableObject[] objectArray;
    30.     private void Spawn() {
    31.         Debug.Log ("<color=blue>Spawning!</color>");
    32.         objectArray = new MyScriptableObject[numToSpawn];
    33.         for (int i = 0; i < numToSpawn; i++) {
    34.             objectArray[i] = ScriptableObject.CreateInstance<MyScriptableObject>();
    35.             objectArray[i].SpawnRandomVectors();
    36.         }
    37.         Debug.Log (numToSpawn + " scriptable objects created and randomized and put into array!");
    38.         PrintDebug(false);
    39.         StartCoroutine(LateDebug());
    40.     }
    41.  
    42.     private void Clear() {
    43.         Debug.Log ("<color=blue>Clearing!</color>");
    44.         if (objectArray == null) {
    45.             Debug.LogWarning ("No array to clear!");
    46.             return;
    47.         }
    48.         for (int i = 0; i < objectArray.Length; i++) {
    49.             objectArray[i] = null;
    50.         }
    51.         objectArray = null;
    52.         Debug.Log ("objectArray cleared.");
    53.         PrintDebug(false);
    54.         StartCoroutine(LateDebug());
    55.     }
    56.  
    57.     private void Delete() {
    58.         Debug.Log ("<color=blue>Deleting!</color>");
    59.         if (objectArray == null) {
    60.             Debug.LogWarning ("No array to delete!");
    61.             return;
    62.         }
    63.         for (int i = 0; i < objectArray.Length; i++) {
    64.             Object.Destroy(objectArray[i]);
    65.         }
    66.         objectArray = null;
    67.         Debug.Log ("objectArray contents destroyed.");
    68.         PrintDebug(false);
    69.         StartCoroutine(LateDebug());
    70.     }
    71.  
    72.     private void Collect() {
    73.         Debug.Log ("<color=blue>Garbage collecting!</color>");
    74.         System.GC.Collect();
    75.         PrintDebug(false);
    76.         StartCoroutine(LateDebug());
    77.     }
    78.  
    79.     private void Unload() {
    80.         Debug.Log ("<color=blue>Unloading!</color>");
    81.         Resources.UnloadUnusedAssets();
    82.         PrintDebug(false);
    83.         StartCoroutine(LateDebug());
    84.     }
    85.  
    86.     private IEnumerator LateDebug() {
    87.         yield return null;
    88.         PrintDebug(true);
    89.     }
    90.  
    91.     private void PrintDebug(bool isLate) {
    92.         if (isLate) {
    93.             Debug.Log ("<color=red>LateDebug:</color>");
    94.         } else {
    95.             Debug.Log ("<color=green>InstantDebug:</color>");
    96.         }
    97.         if (objectArray == null) {
    98.             Debug.Log ("objectArray is null.");
    99.         } else {
    100.             Debug.Log ("objectArray is " + objectArray.Length + " long.");
    101.             int count = 0;
    102.             for (int i = 0; i < objectArray.Length; i++) {
    103.                 if (objectArray[i] != null) {
    104.                     count++;
    105.                 }
    106.             }
    107.             Debug.Log ("objectArray holds " + count + " reference.");
    108.         }
    109.        
    110.         MyScriptableObject[] objects = Object.FindObjectsOfType<MyScriptableObject>();
    111.         if (objects == null) {
    112.             Debug.Log ("Finding ScriptableObjects failed.");
    113.         } else {
    114.             Debug.Log (objects.Length + " ScriptableObjects found.");
    115.         }
    116.     }
    117. }
    118.  
    My findings are that ScriptableObjects must be Destroyed or Unloaded, just like Assets, and are not garbagecollected.
    It also appears the ScriptableObjects still exist within the same frame they are destroyed, even after Destroy has been called on them, as expected.
     
    Tonymotion and tiggus like this.
  5. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    @ThermalFusion That's an interesting find! And makes a lot of sense. Now I have a question when using ScriptableObjects programatically, if I call:

    Code (CSharp):
    1. MyModel model = null;
    2.  
    3. void Start()
    4. {
    5.      model = ScriptableObject.Create<MyModel>();
    6. }
    do I have to manually call Destroy(model)?

    Code (CSharp):
    1. void OnDestroy()
    2. {
    3.      Destroy( model );
    4. }
    or will the referenced ScriptableObject be destroyed along with gameObject? Thanks!
     
    tiggus likes this.
  6. ThermalFusion

    ThermalFusion

    Joined:
    May 1, 2011
    Posts:
    906
    Since the ScriptableObject does not exist on the GameObject it won't be destroyed with it automatically.
     
    Bunny83 and aimansarie like this.
  7. tiggus

    tiggus

    Joined:
    Sep 2, 2010
    Posts:
    1,240
    Very interesting, thanks Thermal!

    For some reason I was not making the connection that a SO is really just a stripped down GO without a visual representation in the hierarchy(hopefully that statement is correct). Whereas if I create an instance of a plain C# class it is GC'ed as soon as the last reference to the instance is nulled.
     
    Tonymotion likes this.
  8. ThermalFusion

    ThermalFusion

    Joined:
    May 1, 2011
    Posts:
    906
    This is not the case, it will be eligible for garbage collection but it will not be collected immediately.
     
    Bunny83 likes this.
  9. tiggus

    tiggus

    Joined:
    Sep 2, 2010
    Posts:
    1,240
    Sure, understood.

    This definitely helps me understand better when to use SO vs. plain classes. In my use case I was debating whether to have a monobehavior create a SO or a non unity class instance and it appears there is no reason not to go with the plain C# class since the monobehavior that creates the instance can be expected to hang around throughout the course of the game(such as a levelmanager object).
     
  10. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    Thanks a lot for clarification, time to refactor some code >_>"
     
  11. ThermalFusion

    ThermalFusion

    Joined:
    May 1, 2011
    Posts:
    906
    Their main strength, in my opinion, is that you can make assets from them, which can be referenced.
    I have a tower defense in the works where each tower type has its own ScriptableObject which contains values for damage, models, effects and the like. I store each tower type as its own asset. I then have a single tower prefab which i use to create all my towers, and feed them the ScriptableObject to change their behaviour and appearance.
     
    torque-d, zch1234qq and tiggus like this.
  12. tiggus

    tiggus

    Joined:
    Sep 2, 2010
    Posts:
    1,240
    Sort of like a nested prefab if Unity had nested prefabs?
     
  13. Alex16212

    Alex16212

    Joined:
    Dec 20, 2018
    Posts:
    41
    Ok, what about ScriptableObject VS plane GameObject as a storage of all global data? I can create a GameObject in the scene with only its Transform and attached script with singleton which stores all global data.

    To give a quick access to this global data (for example game manager) from another game object IN REAL TIME I have to:
    • in the case of ScriptableObject - load it through Resources.Load at every script;
    • in the case of GameObject - find it through GameObject.FindGameObjectsWithTag at every script.
    So I think the second variant is much easier and quicker... Or not?

    So why "IN REAL TIME"? Because I manage many objects through code - create, manipulate and destroy them. I have to track them, check their data, assign them a behavior with shaders, enable and disable these behaviors. Not just create a clones through instantiating prefabs.
     
  14. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,743
    As long as we're necro-posting 2015 threads for basic global game state questions, I'll offer my simple go-to solution for this stuff.

    ULTRA-simple static solution to a GameManager:

    https://forum.unity.com/threads/i-need-to-save-the-score-when-the-scene-resets.1168766/#post-7488068

    https://gist.github.com/kurtdekker/50faa0d78cd978375b2fe465d55b282b

    OR for a more-complex "lives as a MonoBehaviour/ScriptableObject" solution...

    Simple Singleton (UnitySingleton):

    Some super-simple Singleton examples to take and modify:

    Simple Unity3D Singleton (no predefined data):

    https://gist.github.com/kurtdekker/775bb97614047072f7004d6fb9ccce30

    Unity3D Singleton with a Prefab (or a ScriptableObject) used for predefined data:

    https://gist.github.com/kurtdekker/2f07be6f6a844cf82110fc42a774a625

    These are pure-code solutions, do not put anything into any scene, just access it via .Instance!

    If it is a GameManager, when the game is over, make a function in that singleton that Destroys itself so the next time you access it you get a fresh one, something like:

    Code (csharp):
    1. public void DestroyThyself()
    2. {
    3.    Destroy(gameObject);
    4.    Instance = null;    // because destroy doesn't happen until end of frame
    5. }
     
  15. Alex16212

    Alex16212

    Joined:
    Dec 20, 2018
    Posts:
    41
    Thank you Kurt for these links. But the question is a little bit different. Which variant is better to use for such global things like a game manager or another global game data?
    1. Unity ScriptableObject,
    2. Unity GameObject,
    3. C# static class,
    4. C# singleton class,
    5. Unity singleton class derived from MonoBehaviour,
    6. Something another?
    As far as I understand, such things like ScriptableObject or GameObject requires assingment through Unity editor or finding across the objects in the scene - so this is much more work in comparison with accessing just a static class or a singleton from another script?

    And by the way, you can just add a global singleton to, for example, scriptable or game object and use it through them or directly through the name of the singleton - has it some significance or is it just a meaningless operation?
     
    Last edited: Dec 27, 2021
  16. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,743
    The only points of making a singleton from a MonoBehaviour is:

    - if you want Update() and other calls.
    - if you want to hand-edit fields in the inspector

    Better for what? A 60-minute gamejam? Or a 6-year-long $10 million budget AAA console title?
     
    Alex16212 likes this.
  17. Alex16212

    Alex16212

    Joined:
    Dec 20, 2018
    Posts:
    41
    Understood )))