ScriptableObject behaviour discussion (how Scriptable Objects work)

Ever since the well-known talk by Richard Fine about ScriptableObjects (link below) they’ve become more and more prevalent in Unity system architecture. Ryan Hipple took it a step further in his own talk (link below) and gave us a ton of practical examples on how to use them.

Despite both of these AND the Unity documentation on ScriptableObjects (link below) I’ve found that this powerful tool still has a bunch of gotchas. So I’d like to clear them up by talking about EXACTLY how they behave and why.

Here’s everything I know about them and how they work. If you can fill in a ‘???’ or provide additional insight into any of these points, please comment below. Note that all tests to verify these behaviours were done in Unity 2018.1.0f2 on a Windows OS.

ScriptableObjects - Their Callbacks And When They Receive Them

Awake() - From docs: This function is called when the ScriptableObject script is started.
Specifically, there are 3 cases in which a ScriptableObject receives an Awake() message from Unity:
1 - When the ScriptableObject is created (in editor or at runtime)
2 - When the ScriptableObject is selected from the project window in the Editor (??? what’s the root cause of this? Inspector?)
3 - When a scene is loaded IF at least one MonoBehaviour in that scene is referencing the ScriptableObject asset

Important! ScriptableObjects will only receive Awake() if OnDisable() was previously called on the object. So if Awake() was called because of case 2 and case 3 occurs before other callbacks, it won’t receive Awake() again.

OnEnable() - From docs: This function is called when the object is loaded.
Specifically, there are 3 cases in which a ScriptableObject receives an OnEnable() message from Unity:
1 - Immediately after the ScriptableObject’s Awake() (before other callbacks on this or other objects)
2 - When the Unity Editor reloads IF in a scene that has a MonoBehaviour referencing that ScriptableObject asset (right after OnDisable())
3 - When entering play mode IF in a scene that has a MonoBehaviour referencing that ScriptableObject asset (right after OnDisable())

OnDisable() - From docs: This function is called when the scriptable object goes out of scope.
Specifically, there are 4 cases in which a ScriptableObject receives an OnDisable() message from Unity:
1 - When a scene is loaded and there are no MonoBehaviours in that scene that reference the ScriptableObject asset
2 - When the Unity Editor reloads IF in a scene that has a MonoBehaviour referencing that ScriptableObject
3 - When entering play mode IF in a scene that has a MonoBehaviour referencing that ScriptableObject
4 - Right before any OnDestroyed() callback

Important! OnDisable() will only be called if OnEnable() was previously called on the ScriptableObject.

OnDestroy() - From docs: This function is called when the scriptable object will be destroyed.
As far as I can tell, that’s completely accurate. What’s worth mentioning here are the 3 (???) causes for ScriptableObject to be destroyed in the first place:
1 - The ScriptableObject is deleted in code
2 - The ScriptableObject is deleted from the assets folder in the Editor
3 - The ScriptableObject was created at runtime and the application is quitting (or exiting play mode)

ScriptableObjects - Other Behaviour

ScriptableObject vs. MonoBehaviour callbacks
It’s important to note that, when a scene is loaded, a ScriptableObject will normally receive its Awake() and OnEnable() messages before any MonoBehaviours receive their Awake() or any other callbacks, no matter the Script Execution Order. (??? anyone know why that is?)

Execution Order
ScriptableObjects themselves do not follow the Script Execution Order. If called at the same time, the order in which ScriptableObjects receive Awake() is unclear. (??? what determines the order of execution of ScriptableObjects?)

Runtime Creation
When creating a ScriptableObject at runtime, it does not appear in the Project’s Assets folder, nor does it appear in the Hierarchy. (??? is there a way to view them in Editor?) ScriptableObjects created this way will get deleted either when the application quits or when exiting play mode.

Scene References
If you have a ScriptableObject that is meant to hold references to scene objects, be aware that the object fields for those references will display:
(type mismatch)
The reference will still point to the correct scene object if clicked on in the inspector. This is because, by default, object fields in assets can only contain references to other non-scene assets. This can be overridden by writing a custom property drawer for the referenced behaviour, or a custom inspector for the ScriptableObject. Even if that is done, scene objects still cannot be dragged and dropped into object field on ScriptableObjects.

Deletion
When deleting a ScriptableObject, note that other components still have the C# reference to them

Please comment below if you have anything constructive to add.

Resources
Richard Fine panel/talk:

Ryan Hipple panel/talk:

Unity Documentation for Scriptable Objects:

Unity Documentation for custom inspectors and property drawers:

28 Likes

I would not use scriptable objects for anything other than user-defined assets, in all honesty.

It is an object that is detached from scene hieararchy and is best suitable for persistent data and configs.

Trying to make it run scripts is something I wouldn’t bother doing.

5 Likes

I use them for all types of things, though almost all a structured container of some sort. (Ui meshes, animation flows, vfx, event stacks, etc). I agree with you that using them to run gameplay scripts isn’t really I would do. They all have their own methods for managing/using the contents, but that is different. I typically wouldn’t use any of standard event in an so, with exception of onenable for de-serelization of custom (and some native) structs.

I think Unity has tried to push ScriptableObjects further then they are really designed to go.

Ultimately ScriptableObjects are flexible data containers. You can use them to hold all sorts of interesting data. You can modify the data at edit time, and have it be persistent. You can make them smart so that they do all sorts of useful operations with the data they hold when accessed.

But there are a few rules to remember with ScriptableObjects. They are assets. That means they should be treated as immutable. It also means they aren’t related to specific scenes, and should never have references to scene objects. It also means you shouldn’t be creating them at runtime. And it means you should only be using Unity’s magic messages at edit time, not at runtime.

7 Likes

That’s very true of ScriptableObject assets. But you don’t have to save a ScriptableObject as an asset. They’re just serializable objects. You can create ScriptableObject instances at runtime just like you can create an instance of any class.

And just like any other object in object-oriented programming, they typically contain data and methods. It’s been a while since I watched Richard Fine’s presentation, but I believe he put AI methods in ScriptableObject classes and saved instances as assets. This allowed him to plug different AI assets (blitz, snipe, etc.) into agents to give them different behavior. So I don’t think there’s anything wrong with putting code in a ScriptableObject class just like any other class.

Ultimately I think we’re all in agreement, though, that ScriptableObjects are nothing magical. It’s just a class that Unity can serialize.

7 Likes

So I can kind of see the value of instantiating a ScriptableObject and using it as a configuration plugin. Kind of like a light prefab. It lets you set up the data in the inspector on a single instance, then treat that a template. But I really struggle to see what advantage this has over using regular components or vanilla C# classes. It seems like a very narrow use case where ScriptableObjects would be the go to. (I’ll have to watch the video to see exactly what they are talking about. I may be missing something.)

As far as I can see, the only advantage ScriptableObjects have is as persistent assets in the editor. If you don’t need that persistence, a ScriptableObject is the wrong choice.

Cross scene references/communication (through a pub/sub interface), is the other advantage. Objects from any scene can reference a ScriptableObject, so if you have a, say, MonsterScriptableObject, with a Color property, you can set the color in your UI scene, and if the Game scene is open, the Monster component on some gameobject can go “Hey, i’m a monster. What’s my current color? Can you tell me when someone changes my color?”

Since cross scene editing is so much better than monoscene, especially with source control and collaborate, having these weak links/pubsub systems makes it really easy to decouple behaviour. If you separate out those events, too, you can hook up new behaviour to them pretty easily (eg. using the monster, have an OnColorChanged event that makes a cute particle burst when the color is changed), without having to actually change any existing code – or, indeed, without having to write any code at all.

5 Likes

That’s exactly the kind of behaviour I’m making very heavy use of in the system I’m currently building. Using the concepts and examples from Ryan Hipple’s talk as a basis, I took it a step further and built the variables in such a way that anything can listen for when their value changes (without needing to check the variable all the time).

But adding that conceptually simple behaviour has required all those simple container variables to have some logic of their own. And getting that logic to correctly hook up has proven to be a challenge, mostly due to not completely understanding when SOs received their Unity callbacks. So if any of you have any tips to add to the above, please do, for all our sakes. :slight_smile:

3 Likes

I think some of the confusion comes from ScriptableObject instances vs. ScriptableObject assets.

ScriptableObject.Awake() is only called on instances, when you create the instance. It’s not called on existing assets. (But it is called on the asset when you first create it as an instance, before saving it as an asset.)

ScriptableObject.OnEnable(), like you mentioned, is only called on assets when the editor loads the asset for the inspector, or when the asset is first referenced in a build. I don’t think it’s guaranteed to be called when switching into playmode.

What’s the difference between an SO instance and an SO asset?
Aren’t all SO assets instances?

Just discovered that Awake() CAN be called more than once while the editor is open, all depending on scene loading.
Similarly, OnDisable()'s call seem to also be tied in part to scene loading.

Will update top description to match.

An instance is just an object in memory. Here’s an instance of a simple class, allocated using new:

public class Foo
{
}

Foo foo = new Foo();

For ScriptableObjects, Unity provides a special ScriptableObject.CreateInstance() method to use instead of new:

ScriptableObject so = ScriptableObject.CreateInstance<T>();

(And a special Destroy() method to cleanly deallocate it.) But apart from that, and the special methods and handling that it inherits, it’s just an object in memory like any other.

Only when you save it to an asset file in your Unity project does it become an asset.

I guess it gets a little muddy because Unity will technically load an instance of the asset when it gets referenced. In the editor, Unity will serialize changes back into the asset, too.

Ah, so you’re saying the asset is just saved data (which doesn’t do anything), but when it gets loaded for any reason, it creates an instance, and that instance is what receives the Mono callbacks.

I’ve been really happy using SO for data in my current project. It’s a serializable object that also supports inheritance, which is huge.

For example, I can create a SO that defines the fundamental behavior of a game stage, then extend it to create assets that represent each game stage. At runtime, a stage can be instantiated from its ‘template’ asset when appropriate, modified during its lifetime with state information, and then automatically garbage collected afterwards. It makes a great state machine and has been a boon for nice event-driven behavior.

They’re also convenient for inclusion in Asset Bundles.

Right. But the editor will keep an instance around for a while; it only gets reloaded under certain circumstances.

I’ve been curious, though: is there any pragmatic difference between using MonoBehaviour on a prefab and a SO asset?

Go watch the linked Richard Fine talk. He explains some of the differences between SO vs Monobehaviour on a prefab.

1 Like

I have tried and discovered that when entering play mode, OnDisable is called before OnEnable and I don’t understand why. Has anyone here encountered this before?

The current state is destroyed right before entering playmode, then re-created in playmode (a reload). So, your SO is indeed disabled before entering, then enabled after entering.

You can check that Application.IsPlaying will be false in that OnDisable call.

3 Likes

From what I’m finding, my gut tells me something along these lines…

If you call anything that causes a ScriptableObject asset to be instantiated (cloned) or loaded, Awake() will be called. Loading an asset for the first time creates the (let’s call it) root_instance. Consider the reports in the forums of Awake() not being called when loading an asset. This happens because the root_instance is already loaded. You didn’t ask for a new instance (clone), you asked for the root_instance which is already in memory and is returned to you, so no new instance, and therefore no Awake().

When the Editor is involved it adds another complication. While you may tear down your ScriptableObject instances in code, that doesn’t mean the Editor unloaded the asset. i.e. the root_instance is still alive.

The presence of the loaded root_instance also seems to explain why OnDisable() may be called when entering playmode. It might make sense that root_instances would be disabled on play, as you may have been doing naughty things with them in the Editor and you are given the opportunity to clean up (reset). As playmode begins, OnEnable() is called, giving you the chance to re-initialize, just as if the game were running standalone. You definitely have the opportunity to create mayhem with root_instances in the Editor, if not careful. Try loading a root_instance from Editor code, give it some bad data and in some scene code, load the asset to get the root_instance and take a look at the data. I’d wager the badness persisted into playmode, if not fixed-up by OnDisable()/OnEnable().

If you instantiate (clone) the root instance, Awake(), OnEnable(), etc. will be called for that instance, per usual.

If you destroy an instance, OnDisable()/OnDestroy() should be called. But when you unload the root_instance, I’m not entirely sure either will be called.

What’s more, some editor APIs, like AssetDatabase.GetMainAssetTypeAtPath() appear to load the root_instance. I’m sitting here watching the new Addressable Asset System code peg my Awake() for an asset marked as Addressable, but not used/referenced in the executing code, when I did not ask for a load…

Does all this seem accurate?