Scriptable Objects are a special type of data object in Unity. They have several important benefits but may not work ideally for every scenario. In this lesson we will cover what they are and how to use them.
Intro to Scriptable Objects
You can think of a scriptable object as an object that is meant only for holding data. If you have been using tradtional C# classes or structs for simple data-only objects, you could optionally use these instead. Of course you may be wondering “why” you would want to use a Scriptable Object. Here are a few pros and cons:
Pros
- They can survive an assembly reload (such as any time you build your scripts or enter and exit play mode).
- They save by reference, whereas normal classes and structs are serialized as full copies. This can help you avoid data duplication.
- They can handle polymorphism, whereas normal classes end up being treated as the base class.
- They can be saved as a project asset.
- They don’t need to be attached to Game Objects.
Cons
- You must inherit from Scriptable Object, which may break many of your design or architectural options.
- You can’t create them using normal constructors, but must use “CreateInstance” instead.
- The Serialization benefits are not equally applicable to runtime.
I have created several mini demos for clarification on these points. The first two demos show how you might run into problems if you weren’t using Scriptable Objects. The following two demos show how Scriptable Objects overcome those same issues.
Demo 1
Loss of object references on serialization
Let’s begin with some serialization examples. Begin by creating a new script called “Demo1” and another called “Demo1Data”, also add an editor script called “Demo1Inspector”:
[csharp]
using UnityEngine;
public class Demo1 : MonoBehaviour {
public Demo1Data dataA;
public Demo1Data dataB;
}
[/csharp]
This script will hold two copies of the same “Demo1Data” instance. We will use an editor script to create and assign its values.
[csharp]
using UnityEngine;
[System.Serializable]
public class Demo1Data {
public int value;
}
[/csharp]
This script shows a very simple standard C# class. It can be serialized, thanks to the “[System.Serializable]” tag, but Unity won’t handle it perfectly which will be demonstrated soon.
[csharp]
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Demo1))]
public class Demo1Inspector : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector ();
Demo1 myTarget = (Demo1)target;
if (GUILayout.Button (“Create Shared Data”))
{
myTarget.dataA = new Demo1Data ();
myTarget.dataB = myTarget.dataA;
}
}
}
[/csharp]
This script will provide a button in the inspector of our component that will create a new instance of “Demo1Data” and assign it to both fields on the “Demo1” script. IMPORTANT – the editor script must be added to an “Editor” folder or it wont work properly.
Go ahead and create a new scene. Add the “Demo1” as a component to any Game Object, such as by creating a new empty game object, or even by attaching it to the camera. Then look in the inspector. Unity will automatically create new instances of “Demo1Data” for both fields simply by looking at the object in the inspector. You can assign any value you want to each of the “Value” fields. If you enter and exit play mode, the values will even persist – so far so good.
Exit play mode (if you havent already), then use the “Create Shared Data” button in the inspector. The value for both fields should return to ‘0’ becuase both fields now refer to the same new instance. If you modify the value field of “dataB”, you should see the value field of “dataA” update to match accordingly. Still looking good… at least until you enter and exit play mode. Give it a try, then modify the value of “dataB” once again. Uh oh, the two are no longer referencing the same object! Unity has created a full copy of the original object for both fields.
Demo 2
Loss of object type on serialization
This demo will show how Unity fails to properly serialize the type of an object. You might encounter this problem with a polymorphic list of objects. Create the following:
[csharp]
using UnityEngine;
public class Demo2 : MonoBehaviour {
public Demo2Data[] dataArray;
}
[/csharp]
This script will hold an array of objects. Each object will share a base class – “Demo2Data”, but will actually be instantiated as a subclass.
[csharp]
using UnityEngine;
[System.Serializable]
public class Demo2Data
{
public string name;
public override string ToString ()
{
return string.Format (“[{0}]”, name);
}
}
[System.Serializable]
public class Demo2NumberData : Demo2Data
{
public int number;
public override string ToString ()
{
return string.Format (“[{0}, {1}]”, name, number);
}
}
[System.Serializable]
public class Demo2BoolData : Demo2Data
{
public bool toggle;
public override string ToString ()
{
return string.Format (“[{0}, {1}]”, name, toggle);
}
}
[/csharp]
There are three classes here, a base class called “Demo2Data” and two subclasses of it. Note that we will never instantiate a copy of the base class directly.
[csharp]
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Demo2))]
public class Demo2Inspector : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector ();
Demo2 myTarget = (Demo2)target;
if (GUILayout.Button (“Create Data”))
{
var dataA = new Demo2NumberData ();
dataA.name = “Demo2NumberData”;
dataA.number = UnityEngine.Random.Range (1, 100);
var dataB = new Demo2BoolData ();
dataB.name = “Demo2BoolData”;
dataB.toggle = UnityEngine.Random.value > 0.5;
myTarget.dataArray = new Demo2Data[] { dataA, dataB };
}
if (GUILayout.Button (“Log Values”))
{
foreach (var data in myTarget.dataArray)
{
Debug.Log (data.ToString());
}
}
}
}
[/csharp]
This script will provide some buttons in the inspector of our component. The first is labeled “Create Data” and will instantiate each of our data subclasses and assign them to the data array of our script. The second button is labeled “Log Values” and will cause each object in the array to print its values to the console window. IMPORTANT – the editor script must be added to an “Editor” folder or it wont work properly.
Go ahead and create a new scene. Add the “Demo2” as a component to any Game Object, such as by creating a new empty game object, or even by attaching it to the camera. Then look in the inspector. Unity will automatically create an empty array of data simply by looking at the object in the inspector. Let’s populate our object with data by clicking the “Create Data” button. You should see that the array now holds two objects.
Even though the base data class and its subclasses all have the “[System.Serializable]” tag, you wont see fields added for the “number” or “toggle” fields of the actual instances. This is because Unity is treating them as the base class, which only knows about the object’s “name”. However the data is still there (at least for the moment). Click the “Log Values” button and you should see the full description. In one of my own runs I saw output like the following:
[Demo2NumberData, 84]
[Demo2BoolData, False]
Looks good so far right? Well, let’s see if it can survive an assembly reload. Go ahead and enter and exit play mode. Now press the “Log Values” button once more. You should see output like this:
[Demo2NumberData]
[Demo2BoolData]
Just like Unity didn’t know how to display the objects properly, it also didn’t know how to serialize them properly! Both objects are now instances of the base class, and their subclass data is lost!
Demo 3
Scriptable Object references survive serialization
This time we will recreate Demo 1, except we will use a Scriptable Object for our serialized data instead of a standard C# class. Create the following:
[csharp]
using UnityEngine;
public class Demo3 : MonoBehaviour {
public Demo3Data dataA;
public Demo3Data dataB;
}
[/csharp]
[csharp]
using UnityEngine;
[System.Serializable]
public class Demo3Data : ScriptableObject {
public int value;
}
[/csharp]
[csharp]
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Demo3))]
public class Demo3Inspector : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector ();
Demo3 myTarget = (Demo3)target;
if (GUILayout.Button (“Create Shared Data”))
{
myTarget.dataA = ScriptableObject.CreateInstance
myTarget.dataB = myTarget.dataA;
}
}
}
[/csharp]
Create a new scene and attach the Demo3 script to an object. Unlike with Demo1, the Demo3 script will not automatically create new Scriptable Object instances just by looking at the script in the inspector. In order to begin playing with data, click the “Create Shared Data” button. Now, both fields show the data object iftself. We could further customize the editor script to make it look similar to Demo1 if desired, but for now it isn’t necessary. To edit the value of the shared object, double click the data object in either field. The inspector window will update showing only the object you are editing.
Now for the big test, can this version survive an assembly reload? Go ahead and enter then exit play mode. Try editing the value of either data object. Then go back and open the object through the other field. You should see that the reference was serialized properly, because it will hold the same value! Unity was able to retain the shared reference instead of needing to serialize a full copy of the object for each field.
Demo 4
Scriptable Object type survives serialization
Now let’s recreate Demo 2 (the polymorphism demo) while using scriptable objects instead of standard C# objects. Note that in Demo 2, the base data object and its subclasses shared a single script file. Unity has some additional requirements such that each scriptable object must appear in its own file, and the file name must match the class name.
[csharp]
using UnityEngine;
public class Demo4 : MonoBehaviour {
public Demo4Data[] dataArray;
}
[/csharp]
[csharp]
using UnityEngine;
public class Demo4Data : ScriptableObject
{
public override string ToString ()
{
return string.Format (“[{0}]”, name);
}
}
[/csharp]
[csharp]
using UnityEngine;
public class Demo4NumberData : Demo4Data
{
public int number;
public override string ToString ()
{
return string.Format (“[{0}, {1}]”, name, number);
}
}
[/csharp]
[csharp]
using UnityEngine;
public class Demo4BoolData : Demo4Data
{
public bool toggle;
public override string ToString ()
{
return string.Format (“[{0}, {1}]”, name, toggle);
}
}
[/csharp]
[csharp]
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Demo4))]
public class Demo4Inspector : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector ();
Demo4 myTarget = (Demo4)target;
if (GUILayout.Button (“Create Data”))
{
var dataA = ScriptableObject.CreateInstance
dataA.name = “Demo4NumberData”;
dataA.number = UnityEngine.Random.Range (1, 100);
var dataB = ScriptableObject.CreateInstance
dataB.name = “Demo4BoolData”;
dataB.toggle = UnityEngine.Random.value > 0.5;
myTarget.dataArray = new Demo4Data[] { dataA, dataB };
}
if (GUILayout.Button (“Log Values”))
{
foreach (var data in myTarget.dataArray)
{
Debug.Log (data.ToString());
}
}
}
}
[/csharp]
Go ahead and create a new scene, then attach the Demo4 script to any Game Object. Use the inspector script to “Create Data” on our Demo4 component. Just like in Demo3, you must double click the object field to see and edit the values of each object instance. Use the “Log Values” button to quickly see each printed to the console window.
Now for the big test, can this version survive an assembly reload? Go ahead and enter then exit play mode. Press the “Log Values” button once again. Success!
Demo 5
Scriptable Object Assets
I stated a few pro’s of Scriptable Objects that I have not yet demonstrated. For example, I said that you could save these objects as assets, and I also mentioned repeatedly that Scriptable Objects don’t get attached to GameObjects, yet in every demo so far I have only shown them as references in MonoBehaviour scripts which are, not surprisingly, attached to GameObjects. In this lesson I will finally show how to work with these data objects on their own.
It used to be a more cumbersome process to create Scriptable Objects, but now we have a tag called “CreateAssetMenu” that handles it auto-magically for us. You can get started with something as simple as:
[csharp]
using UnityEngine;
[CreateAssetMenu()]
public class Foo : ScriptableObject {
public int value;
}
[/csharp]
Build your code then head back to Unity. You can use the app’s menu bar (Assets -> Create -> Foo), or the Project panel’s “Create” pull down menu (Create -> Foo). Select either one and a new asset will be created in your project called “New Foo”. You can rename the asset, move it to another folder, populate it with custom data, etc. Save your project and you now have a handy data object asset… and did ya notice that no Game Object was required?
The “CreateAssetMenu” can also take parameters. In the version below, I specify the name of newly created instances, cause it to appear in a sub-menu, and specify an order so that I can make more frequently used objects appear at the top of the list.
[csharp]
[CreateAssetMenu(fileName = “Foo”, menuName = “Scriptable Objects/Foo”, order = 1)]
[/csharp]
If you’ve looked at the Scriptable Object documentation, you may have noticed that it has a few methods similar in name to those on a MonoBehaviour. For example, it has: Awake, OnDestroy, OnDisable, and OnEnable. Since there is no GameObject, when are they called? Unfortunately, the answer is probably not when you would expect. I added debug log messages to each of these methods in my “Foo” class, and also created a similar “Bar” class to test with.
[csharp]
using UnityEngine;
[CreateAssetMenu(fileName = “Foo”, menuName = “Scriptable Objects/Foo”, order = 1)]
public class Foo : ScriptableObject {
public int value;
void Awake() {
Debug.Log (“Awake Foo ” + name);
}
void OnDestroy() {
Debug.Log (“Destroy Foo ” + name);
}
void OnEnable() {
Debug.Log (“OnEnable Foo ” + name);
}
void OnDisable() {
Debug.Log (“OnDisable Foo ” + name);
}
}
[/csharp]
- Compile your scripts. Now create a new “Foo” asset. You should see that “Awake” gets called, and then “OnEnable”, in that order. This was probably expected, if you are familiar with the order from MonoBehaviour.
- Click off of the Foo asset so that its name is applied and so that it is no longer selected.
- Next, enter play mode. You should see “OnDisable” is called, then “OnEnable” is called once again. This has to do with the way that objects get passed around between the C++ core engine of Unity, and the C# scripting side of Unity.
- Exit play mode and you will again see “OnDisable”, but wont actually see a call to “OnEnable” like you might have expected.
- If you now select the “Foo” asset so that it appears in the inspector, you will see both an “Awake” and “OnEnable” call again.
Without having tested it, I would have thought that these methods were intended for runtime use and would not be invoked by editor actions. Furthermore, I would have thought that “Awake” would be reserved for creation of the asset only – especially since we are not allowed to use the constructor of a Scriptable Object. In my opinion, there really should be some sort of “init” method that is called once only for the creation of the asset. This is where I would typically add setup work for an object that I would not want to happen more than once. In order to get the behaviour I want, I can still use an editor script to manually create and configure my asset. See the “MakeScriptableObject” sample from this Unity Tutorial for an example.
Let’s continue to examine the flow of method calls at runtime. Before starting, I renamed my Foo asset to “Banana” and put it inside a “Resources” folder. I also created the following demo script, which I added to an object in a new scene.
[csharp]
using UnityEngine;
public class Demo5 : MonoBehaviour {
void Start () {
var foo = ScriptableObject.CreateInstance
Destroy (foo);
var asset = Resources.Load
var instance = Instantiate (asset);
Destroy (instance);
Resources.UnloadAsset (asset);
}
}
[/csharp]
- When I enter play mode, I first see that the project asset (banana) has the “OnDisable” and “OnEnable” called on it just like before.
- Then I see an “Awake” and “OnEnable” for the first “foo” instance I create at the beginning of the Demo’s start method.
- Even though I call “Destroy” next, I won’t see the object go out of scope until after the current update loop.
- The next logs I see are for the “Awake” and “OnEnable” of my “Banana(Clone)”. The “Banana” project asset itself didn’t need an additional “Awake” or “OnEnable” because it had already been loaded by the Unity editor.
- I have another call to Destroy the clone of Banana, but like before the actual removal wont occur until after the update loop.
- My call to unload the “Banana” asset then triggers the “OnDisable” method for that asset.
- Then I see calls for “OnDisable” and “Destroy” of my first created foo object. They always occur in this order.
- Finally I see calls for “OnDisable” and “Destroy” of my “Banana(Clone)” instance.
Much of this order was expected, with the exception that I would prefer different method calls for object instances and object assets. Oh well.
For another test, try commenting out the “Destroy (foo);” on line 6 of the Demo5 script. Even though the foo will have gone out of local scope, you should note that it is not disabled or destroyed at the completion of the Start method. This is different than a standard C# object which would be automatically garbage collected. In this case the Scriptable Object remains in the scene. Unfortunately it wont appear in the scene’s hierarchy pane in a way similar to the project asset in the project pane. You wont really know it exists unless you explicitly look for it. Add the following:
[csharp]
void Update() {
if (Input.GetKeyDown (“space”)) {
var foos = Object.FindObjectsOfType
for (int i = 0; i < foos.Length; ++i) {
Debug.Log (string.Format("Foo {0} -> {1}”, foos[i].name, foos[i].value));
}
}
}
[/csharp]
Now build and run the scene. After a moment press the space bar and you should see that our foo instance still exists. When you finally exit play mode, the foo object will have the “OnDisable” and “Destroy” methods invoked. As a side note, this is slightly different than when you change or reload scenes. In these cases, the scriptable objects will have their “OnDisable” method called, but will NOT have their “Destroy” method called as you might have expected.
Runtime Scriptable Objects
I have shown scriptbale objects used both in edit mode and during play mode. However, it is worth pointing out that some of the greatest benefits of scriptable objects – in particular its easy serialization, is not something you will be able to make use of at run time.
You can still save data, such as by using JsonUtility to convert your scriptable objects to or from JSON. The result could then be saved a variety of ways, such as by writing the value to PlayerPrefs, or by writing a file to disk. Unfortunately you will likely end up with the same serialization challenges demonstrated in my first two demos. You will not have an easy way to preserve object references, nor will you have an easy way to recreate arrays of polymorphic objects.
Summary
Scriptable Objects are great little data containers. They can be used at run time or edit time and can even be saved as project assets. They offer several benefits that standard objects and classes miss out on, such as proper serialization, but they aren’t perfect. I feel their interface could be more intuitive, and I don’t personally like the restriction of having to inherit from Scriptable Object, or of being unable to use a standard constructor. Overall, they are worth spending some time with because they can provide some convenient work flows and can help you quickly protoype your content.
You can download a project containing all of the scripts from this lesson here.
If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!
Great tutorial as always.
thanks 🙂
Thank you for this extensive write up.
I only wish you had explained how Scriptable Object classes serialize, then this probably would have been THE best article I’ve found on this topic 🙂 Either way, great article.
If you use Unity’s built in serializer / JsonUtility, instead of storing the actual data / values it stores only references to Scriptable Objects by IDs like this:
{ “list”:[ {“instanceID”: 10828},{“instanceID”: 10856} ]}
This wouldn’t be that bad (maybe?) if these IDs were static, but these are not guaranteed to be the same after you reboot Unity or re-run the game AFAIK. And this happens if you serialize SOs in scene or in c# class to JSON using Unity’s JsonUtility… I have no idea how third party JSON serializers like Json.net work with SOs.
I haven’t seen JSONUtility serialize Scriptable Objects as instance IDs before. I’m not sure if its a new feature since I wrote the blog post or not, but I don’t normally work with ScriptableObjects for run time data. Anyway, there is an answer on this stack overflow link that looks something like I would expect to be necessary:
https://stackoverflow.com/questions/56859245/deserialize-multiple-unrelated-scriptableobjects-from-one-save-file