When is a ScriptableObject enabled?

I use scriptable objects for storing input and loading events, since they don’t depend on scenes, and create these events in their OnEnable(). However, I keep getting null references when subscribing to them, and so I am confused about when exactly OnEnable() is triggered.

In Editor, it is not triggered until I click the ScriptableObject asset, and I guessed that they should be enabled properly in build. But no, I still got null references.

So when exactly is ScriptableObject’s OnEnable() called? Awake()? What they should be used for? Should I just call all the preparations manually in some MonoBehaviour’s Awake() instead?

From stackexchange:

Full thread: unity - ScriptableObjects events execution order? - Game Development Stack Exchange

OnEnable is called whenever the SO is ‘loaded’.

Think of an SO like any asset (e.g. a Mesh). It’s ‘loaded’ when it is first referenced.
When you click it in the editor, it creates an inspector for it, and thus loads it.

What are you trying to do? Sounds like you may be trying to use them in a way that doesn’t really make sense.

Sounds like he’s trying to use SO’s as an event-system. (Which is totally a ‘valid’ way to use them: Architect your code for efficient changes and debugging with ScriptableObjects | Unity )

Just for historic completeness, ScriptableObject callbacks significantly changed sometime during Unity2017, probably related to changes in how domain reloads work in Unity.

Prior to this a ScriptableObject instance ONLY ever called OnEnable once per booted application session (build or editor), so it was effectively useless for initialization. Now it can be used almost as a MonoBehaviour.

Unfortunately the documentation is not clear:

OnEnable() - This function is called when the object is loaded.

… completely omitting what “loaded” means. :slight_smile:

But it APPEARS to act almost like a global start / stop indicator for your app.

As always, guard your initialization code in a way that lets you identify immediately if the calling contract were to change and your initialization doesn’t get the calls you expect.

ANOTHER gotcha for ScriptableObject instances is that even private field (variable) values are serialized by default. This behaviour is completely opposite that in MonoBehaviours.

If you do NOT want a private variable serialized, you must go out of your way to decorate it as such;

[NonSerialized]
private bool isInitialized;

If you don’t decorate it, the above boolean will remain true through your run session. :slight_smile:

1 Like

Yep, exactly. I looked here and found nothing.

Now it feels weird, I could make SOs do the thing manually, but it would require a reference, and a reference allows to initialize automatically.

By the way, what if I reference one SO and it has references to another? Would it be enabled too?

Will look into it. Are manuals correct about script serialization then?

Sounds like something that would be quite easy & quick to test.
My guess would be yes.

I do not believe this is correct. Serialization would mean the values for private variables are also written to the asset (on disk). I don’t believe that they are.
However: SO’s lifecycle is somewhat different, as they’re usually referenced in some weird places, meaning they don’t ‘unload’ like you’d expect them to (e.g. like normal MonoBehaviours). They’re persistent, but not serialized afaik.

https://discussions.unity.com/t/495825

Mostly, yes.

No Kurt is correct. Private fields not decorated with [NonSerialized] in scriptable objects will persist between domain reloads because Unity will specifically serialise them (and store this data ‘somewhere’) to maintain their state. I assume this is true of all project file assets.

Monobehaviours work the opposite way… sort of. Private fields without [NonSerialized] will be serialised during domain reloads with the intent to reset their state.

1 Like

Yes

No. Because they’re simply NOT unloaded or reloaded.

Most, yes. Think meshes, materials, etc.
But Components (& monobehaviours) aren’t.

When you enter play mode, do a domain reload, etc. MonoBehaviour-Instances are destroyed, recreated & serialized. ScriptableObjects are NOT.
SO’s persist, because they’re assets. Data-Containers.
SO’s are disabled & re-enabled.
When you look at the asset after the domain reload, it’s the same asset as the one before the domain reload. All of your MonoBehaviour-instances are ‘new’, but the SO is not. It’s still the exact same SO as before. You just got a new pointer (managed wrapper) to it.
(Exceptions to this are SO’s instantiated at Runtime (SO Instances)).

This thread explains it as well:
https://discussions.unity.com/t/708570

SO’s often aren’t unloaded unless you close the entire application (due to dangling references, caching, etc. on both managed & unmanaged side).

Serialization of SO’s happens e.g. when you .ApplyModifiedProperties() in an editor-script. That will write changes to the asset on disk (

Hmmm… I can’t decide if we’re agreeing or disagreeing. Heh… :slight_smile:

All I’m sayin is that:

if I decorate a variable with [NonSerialized], then after each play/stop cycle in the editor, it will have been restored to default(T)

You may say that it’s the same object and somehow the constructor was re-fired on that chunk of RAM.

But I’m telling you the feeling (with those private fields reset to default(T)) is that a new C# object was created.

So… I can sort of understand the pedantry in regards to kurt saying “even private field (variable) values are serialized by default” if only because it can be easily confused to mean serialized to disk. Which it is not serialized to disk. But I would also be willing to not nitpick because I get what he’s saying.

But if we’re going to be pedantic.

Well:

They are unloaded and reloaded. Or SOMETHING is happening that could be described with the terms ‘loaded’ and ‘serialized’.

We can check the object every time you start the game by simply reading its address, instance id, and header hash.

The address and instance id remains the same. BUT the header hash (using something like RuntimeHelpers.GetHashCode will create a hash based on the header of the object in memory at the address of the object) will be different between starts.

This means that an entire new object has been placed into memory at the same address of the previous object from previous play session.

Of course… now we’re getting into the gritty details of what makes a new object a new object. Is it actually new if it’s at the same address and any and all references therefore inherently synced to this new object making it appear like it hasn’t changed down to its instanceid and address being identical. Is it new if the C++/internal object may or may not have been altered.

But it’s not identical, because 1) the header has changed and 2) its entire state isn’t necessarily maintained (and state is sort of the entire point of object identity) since fields that can’t be serialized aren’t maintained.

And to note, we know some sort of serialization is definitely occurring. Serialization doesn’t necessarily mean it has to be to disk. Serialization is just the creation of a byte stream out of an object. That byte stream can be stored to disk, transmitted over network, moved between memory spaces, or any number of things.

But we know it’s serialized for a few reasons:

  1. the ISerializationCallbackReceiver interface will fire both of its events on play. The ‘before’ just before the transition to play, and ‘after’ as play enters.

  2. marking things System.NonSerialized gets ignored and will reset as if they were initialized by the constructor

  3. types that the Unity serialization engine doesn’t support (like Dictionary or HashSet) are ignored as well.

So clearly unity is utilizing the same serialization logic that they use else where during this process. But they’re including private fields that aren’t otherwise marked nonserialized explicitly or can’t be by said logic.

I guess this may or may not be considered ‘unloaded/loaded’… but that’s more to do with the nebulous definitions of those words since they’re not explicitly defined in the context. Which is the same cloudy nature of the phrase ‘serialize’ since depending context they can imply different things.

TLDR; it’s kind of pedantic at this level of discussion.

The point still stands… state is maintained between play except for things that can’t be serialized (nonserialized, or just outright can’t because its unsupported).

As kurt said: “Hmmm… I can’t decide if we’re agreeing or disagreeing. Heh…”

3 Likes

And my personal 2 cents.

I find it really annoying that private fields are treated this way in ScriptableObjects, especially since not easily serialized obejcts like HashSet aren’t conserved as well. I tripped over this ages ago and it effectively instilled the practice of my marking everything ‘System.NonSerialized’ when I don’t want it serialized if it’s in a SO or MB regardless of access modifier.

I guess I sort of get it since it allows states to persist play sessions in editor if you need it (which wouldn’t be doable otherwise less you marked those fields serializefield which then would then bring fields that wouldn’t otherwise need to be into the asset on disk).

But… the only times I personally can think of to need that would be say things like ScriptableSingleton, in which case, then just reserve the behaviour for that. But who knows… that’s me, I’ve personally never needed it. Maybe someone else has needed it for reasons I never tripped over, and all it requires is this explicit flagging of nonserialized rather than creating a like ‘EditorPersistentScriptableObject’ or something for them to use???

:shrug: I don’t know… still the lack of documentation is the main issue here… which honestly… is the usual thing dealing with these quarky nature of Unity. But hey… to be fair… Unity’s documentation is actually not terrible. While not amazing in some respects, it is actually pretty darn good compared to some other stuff I’ve used. MSDN is obviously significantly better, but MS has WAY MORE money than Unity. So… discussions like these on the forums suffice.

3 Likes

This is the behaviour I’ve observed scriptable objects to have in my testing:

In Builds:

In builds scriptable objects’ Awake and OnEnable functions get executed when a scene or prefab that contains a reference to them is deserialized. This includes chained references (a scene object referencing a scriptable object referencing a scriptable object).

If you add the RuntimeInitializeOnLoadMethodAttribute with RuntimeInitializeLoadType.BeforeSceneLoad to a method, the Awake and OnEnable functions of scriptable objects referenced in the first scene of the build actually get executed before the method with the attribute does. The Awake and OnEnable functions of scene objects get executed after the method and the scriptable objects, when the first scene becomes active.

If a scriptable object instance is loaded or created manually using methods like Resources.Load, ScriptableObject.CreateInstance, then the Awake and OnEnable methods of the instance are executed immediately following the construction/deserialization process.

In The Editor:

In the editor scriptable objects’ Awake and OnEnable functions also get executed when a scene or prefab that contains a reference to them is deserialized. This can happen already in the edit mode, if for example any object in one of the open scenes contains a reference to them or you select them in the Project window.

Entering and existing play mode does not necessarily cause scriptable objects Awake nor OnEnable functions to execute again in the editor.

Confusingly, when scripts are reloaded, the OnEnable function seems to get retriggered for scriptable objects referenced in the open scenes, but the Awake function does not.

(when it comes to the editor side behaviour, your mileage may vary depending on your Enter Play Mode Settings)

2 Likes

I just got caught out by this. If you edit your Awake function in your script it won’t actually do anything when you switch back to Unity. You have to close the editor and re-open it for your changes to take effect. But if you use OnEnable it works as expected (in Editor at least.) Is this a bug - or is there some reasoning behind this different behaviour? It feels like a bug.

Not a bug. Always worked like this.

From experience, Awake has never been useful with respect to scriptable objects. OnEnable/OnDisable is generally all you’ll need to use, as this more closely emulates the behaviour you see in builds anyway.

Maybe it was always a bug.

The odd behaviour here is that it acts differently in the editor and in a build. And it acts differently if you edit an open project, or close the project and then open it again.

That’s because it isn’t unloaded when you exit playmode, unlike MonoBehaviours.
It behaves like e.g. a Mesh. It remains the same even if you leave & re-enter playmode.

I don’t mean exit play mode, I mean close the editor and re-open it. If you edit a mesh outside of Unity, you don’t usually have to quit and restart Unity to see the changes.

That’s not what they’re saying. Script objects are assets, same as anything else that lives in your project folders. They all follow the same principles, including having a lifetime that lives outside of scenes when in the editor.

Tons of things in Unity differ between editor and build due to their vastly different nature. Doesn’t necessarily make every one of them a bug.