I wanted to write this up for the forums as it has been bothering me for months as my project grew in complexity. I apologize if this information is already widely known, I have spent many, many hours researching this online and never really found a succinct write up of what is going on.
In my project I have a state machine built using ScriptableObject, a subclass for C# scripts similar to MonoBehavior but more lightweight. For months I’ve wanted to create a base class for these. There are 12 subclasses, and all but 3 have some overlapping public fields and functions. I wanted to move those common items into a base class to clean things up.
When I moved them my “build time” in unity would go from 2-3 seconds to 4-5 minutes, that’s not a typo. Over time I tracked that down to specifically me moving the public field into the base class. Putting 1 line of code into the base class, a public reference to a MonoBehavior took my project from instant builds → infinite builds, occasionally I would have to kill the unity editor entirely as it would simply never finish the “Asset Refresh” step.
I now understand why. It’s because of serialization. ScriptableObjects serialize to disk, including all of their public fields. Adding a public field to the base class in a complex project triggers a combinatorial explosion of serialization which bought my project to its knees. Asmdef won’t help you. The editor log won’t help you. The profiler won’t help, it will itself crash trying to profile this serialization. Moving things to special folders won’t help either.
To fix this you must add:
[System.NonSerialized]
To any public fields that you don’t need persisted across runs or shown in the editor. I added this to my one field and build time went from 5 minutes back to 2 seconds. I then added it to a bunch of other fields and my build times are effectively instantaneous now.
I apologize again if this is widely known, I wanted to leave this here as an artifact for others facing high build times in their projects. Hopefully some find it helpful.
Even more confusingly a private field in a ScriptableObject instance in the editor will persist from run to run.
This is more of an in-Editor bug-inducer than a performance problem. To see what I mean, lets say your ScriptableObject has some kind of “init once getter” type construct that sets a bool to true.
In a given editor session, once that true has been set, even if it is private, Unity will persist it for you from run to run, just in memory, not on disk.
This means you ALSO want to put the non-serialized decorator even on ScriptableObject private instance fields, otherwise you will get all kinds of weird editor-time issues.
Also, OnEnable()/OnDisable() are implemented and called for ScriptableObjects, but their lifetimes are NOT analogous to MonoBehaviors, and they persist from run to run in a given editor session. This too may blow your mind. Build time it will behave as expected, but not in the Editor, when essentially they get enabled once and never again.
Of course! It really is an unexpected “feature” when you consider how private fields are handled when you normally encounter them.
But at least it is consistent when you realize that in the editor, a ScriptableObject instance is created the very instant you access it in any way (click on it, run a script that references it, etc.) and you get the OnEnable().
From there on it never gets destroyed until you a) delete it, or b) leave the editor. You’ll not see another OnEnable/OnDisable after that first one.
Could you please file a bug for this? We’ve been tying up all kinds of ways in which the Profiler could crash due to huge amounts of data and if this happens on latest 2019.4 or comparable, we’d be very keen to know this and fix this instance too.
Sure! I’ll try to put together a reduction tonight that triggers this case (my real project is way too big to share). I am using 2020.1.1f1, but I see 2f1 is out so I can also upgrade to that and see if it triggers this.
I mean, the instances exist - just like you explained - for the entire session. So that would actually be “normal” and expected behaviour from a pure technical C# perspective. In other words, I wouldn’t expect my values to change if the SO is always that one exact instance.
So Unity doesn’t consciously “persist” these changes, but it just ignores these members entirely so that instance can happily keep its state.
In contrast to that, when the attribute System.NonSerialized is added, it’s more like Unity starts to care about these fields, actively steps in and “resets” these values - which is a little bit counter-intuitive at first. Because it kind of suggests that the values would be candidates for serialization if the attribute were not set - which is not the case.
The same applies to MB instances that live on prefabs btw, or more generally, it applies to all the fields on assets that are neither serialized nor marked non serialized.