Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question When is a ScriptableObject enabled?

Discussion in 'Scripting' started by SMT5015, Feb 7, 2023.

  1. SMT5015

    SMT5015

    Joined:
    Apr 5, 2021
    Posts:
    53
    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?
     
  2. Nad_B

    Nad_B

    Joined:
    Aug 1, 2021
    Posts:
    340
    From stackexchange:

    Full thread: https://gamedev.stackexchange.com/questions/188224/scriptableobjects-events-execution-order
     
  3. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    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.
     
  4. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,384
    What are you trying to do? Sounds like you may be trying to use them in a way that doesn't really make sense.
     
  5. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,756
    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:

    https://docs.unity3d.com/ScriptReference/ScriptableObject.html

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

    ... completely omitting what "loaded" means. :)

    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;

    Code (csharp):
    1. [NonSerialized]
    2. private bool isInitialized;
    If you don't decorate it, the above boolean will remain true through your run session. :)
     
    Unifikation likes this.
  7. SMT5015

    SMT5015

    Joined:
    Apr 5, 2021
    Posts:
    53
    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?
     
  8. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    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://forum.unity.com/threads/sho...custom-scriptableobject-be-serialized.167645/

    Mostly, yes.
     
  9. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,883
    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.
     
    Kurt-Dekker likes this.
  10. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    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://forum.unity.com/threads/scr...iscussion-how-scriptable-objects-work.541212/

    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 (
     
  11. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,756
    Hmmm... I can't decide if we're agreeing or disagreeing. Heh... :)

    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.
     
  12. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    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..."
     
    Last edited: Feb 8, 2023
    SisusCo, Kurt-Dekker and Unifikation like this.
  13. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    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.
     
    Last edited: Feb 8, 2023
    SisusCo, Kurt-Dekker and Unifikation like this.
  14. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,114
    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)
     
    Last edited: Feb 8, 2023
    Paul-Sinnett and spiney199 like this.
  15. Paul-Sinnett

    Paul-Sinnett

    Joined:
    Nov 12, 2010
    Posts:
    20
    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.
     
  16. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,883
    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.
     
  17. Paul-Sinnett

    Paul-Sinnett

    Joined:
    Nov 12, 2010
    Posts:
    20
    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.
     
  18. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    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.
     
  19. Paul-Sinnett

    Paul-Sinnett

    Joined:
    Nov 12, 2010
    Posts:
    20
    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.
     
  20. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,883
    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.
     
  21. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,114
    I think I've been confused by this same behaviour when working on editor windows (which derive from ScriptableObject).

    I make a change in Awake, things don't work as expected while testing, I think I made a mistake in the code I have inside Awake and go examine it, I don't see nothing wrong, I add logging, I don't see any logging either, I finally realize I need to restart the editor to see my changes take effect (or maybe reopening the window was enough, I can't remember).
     
    paulatwarp likes this.
  22. SMT5015

    SMT5015

    Joined:
    Apr 5, 2021
    Posts:
    53
    I tried. It said that no such attribute exists. Should I use something special to make it visible?
     
  23. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,114
    You need to add
    using System;
    to the top of your script to use NonSerialized.
     
    Kurt-Dekker likes this.
  24. SMT5015

    SMT5015

    Joined:
    Apr 5, 2021
    Posts:
    53
    Thanks! I indeed faced trouble with all variables of my event system being serialized. Had to reset them manually at every launch of the game in the editor.
     
    Kurt-Dekker likes this.
  25. anthonov

    anthonov

    Joined:
    Sep 24, 2015
    Posts:
    160
    Ok I did not read all here since its quite confusing but :
    I want to use scriptable objects as singleton with static reference initialized in parameter less constructor. like this :

    Code (CSharp):
    1.     [CreateAssetMenu(fileName = "test", menuName = "test")]
    2.     public class SOTest : ScriptableObject
    3.     {
    4.         static public SOTest Instance { get; private set; }
    5.  
    6.         public SOTest()
    7.         {
    8.             Instance = this;
    9.         }
    10.     }
    I got a weird behavior where sometime in other monoBehevior scripts, this static reference are null in awake.
    And If I add a debug log in the constructor, the reference stop to be null and message is logged.
    Is there execution order or something with this ?
     
  26. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,883
    If you want a scriptable object singleton, use Pre-loaded assets: https://docs.unity3d.com/ScriptReference/PlayerSettings.GetPreloadedAssets.html

    You can be sure it's loaded with that.
     
    StaggartCreations likes this.
  27. anthonov

    anthonov

    Joined:
    Sep 24, 2015
    Posts:
    160
    thanks, tough that's a bit odd.
    I don't get how I can use this with all my scriptable objects that are ready in folders in editor.
    Do I need to write a script that direct reference and add them all to the pre-load table ? When is the right time to run this script ?
     
  28. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,883
    It's just an example. Don't take it too literally.

    The important part is this:
    Code (CSharp):
    1. public class ConfigObject : ScriptableObject
    2. {
    3.     public static ConfigObject configInstance;
    4.    
    5.     void OnEnable()
    6.     {
    7.         configInstance = this;
    8.     }
    9. }
    Then you just need some way to create it and add it to pre-loaded assets.

    If you have a bunch of existing assets, then yeah just write a script that adds them all to pre-loaded assets.