Search Unity

A guide to editor script serialization - making sure fields don't go null on assembly reloads

Discussion in 'Assets and Asset Store' started by Acegikmo, Oct 25, 2014.

  1. Acegikmo

    Acegikmo

    Joined:
    Jun 23, 2011
    Posts:
    1,294
    Working with editor scripting is like a minefield if you're not entirely sure what you're doing. After fighting with this issue in Shader Forge, and now again in Shader Forge 2, I thought it might be time to share my workflow/mind-map when creating editor scripts.

    This flowchart might not be 100% accurate, so please let me know if anything is wrong, and I'll update it :)



    The safest route to go about, seems to be something like this:
    1. Inherit from ScriptableObject in all your classes - create them using ScriptableObject.CreateInstance()
    2. Don’t use non-built-in structs
    3. Make sure all fields are public, or have [SerializeField]
    4. Set hideFlags to HideFlags.HideAndDontSave in OnEnable()
    5. Do null checks on members in OnEnable()
    6. Destroy any ScriptableObjects it’s referring to, in OnDestroy(), in your EditorWindow as well as all your ScriptableObjects
    7. Make sure everything survives assembly reloads, by pressing play, and see if any data was lost

    Almost all of my classes have this structure:

    Code (CSharp):
    1. public class MyClass : ScriptableObject {
    2.  
    3.     [SerializeField] MyOtherScriptableObjectClass m_Object;
    4.  
    5.     void OnEnable() {
    6.         hideFlags = HideFlags.HideAndDontSave;
    7.         if( m_Object == null )
    8.             m_Object = ScriptableObject.CreateInstance<MyOtherScriptableObjectClass>();
    9.     }
    10.  
    11.     void OnDestroy() {
    12.         DestroyImmediate( m_Object );
    13.     }
    14.  
    15. }
    Code (CSharp):
    1. public class MyClass : ScriptableObject {
    2.  
    3.     // Example of a ScriptableObject we want this object to use
    4.     [SerializeField] MyOtherScriptableObjectClass m_Object;
    5.  
    6.     void OnEnable() {
    7.  
    8.         // HideFlags need to be set to properly serialize this object
    9.         hideFlags = HideFlags.HideAndDontSave;
    10.  
    11.         // Locally created objects initialize here
    12.         if( m_Object == null )
    13.             m_Object = ScriptableObject.CreateInstance<MyOtherScriptableObjectClass>();
    14.  
    15.     }
    16.  
    17.     void OnDestroy() {
    18.         // If this object is destroyed, destroy the object we referenced in our variable
    19.         // Avoid accidentally destroy referenced assets in your project here :)
    20.         DestroyImmediate( m_Object );
    21.     }
    22.  
    23. }

    Hope it helps! Let me know if I got anything wrong or if you have any questions :)

    Links to further reading:
    Tim Cooper on editor script serialization
    Lucas Meijer on Unity's serialization
     
    Last edited: Oct 25, 2014
  2. SimonDarksideJ

    SimonDarksideJ

    Joined:
    Jul 3, 2012
    Posts:
    1,689
    That's a fantastic chart. Although I'd probably create a base class with all your common code in and then inherit all other classes from that base class. Could even extend it with generics for the "MyOtherScriptableObjectClass" part.
    Saves having to do it for every script.
    I do similar things with Singleton classes.
     
  3. VesuvianPrime

    VesuvianPrime

    Joined:
    Feb 26, 2013
    Posts:
    135
    Hi Acegikmo

    It is an absolute relief to see someone has the same pattern as me for instantiating ScriptableObjects.

    I do not have words for how much I hate Unity's serialization system. It's unintuitive, promotes bad code and is absolutely broken in places. I'll let AM explain:



    Anyway, venting aside, I would very much like to know how you access the properties of child ScriptableObjects via a SerializedProperty.

    I've just made a thread here with a specific problem I'm having, but to cut a long story short: using SerializedProperty.FindPropertyRelative will return null if you're trying to access the child of a serialized ScriptableObject.

    This makes using ScriptableObjects in object hierarchies an absolute nightmare.

    What are your experiences with this? Do you have any pearls of wisdom to save me from rewriting all of my custom inspector code?

    Thanks,
    Ves

    PS Shader Forge is an amazing package, nice work!
     
    Last edited: Oct 26, 2014
  4. Acegikmo

    Acegikmo

    Joined:
    Jun 23, 2011
    Posts:
    1,294
    Could you clarify what you mean by "child of a serialized ScriptableObject"?
    And thanks for the kind words :)
     
  5. VesuvianPrime

    VesuvianPrime

    Joined:
    Feb 26, 2013
    Posts:
    135
    Hey Acegikmo

    I figured I should probably just give you a simple example. Please find the attached zip.

    Alrighty, so check this out. I've given you a prefab called Test. If you add it to the scene and look at the inspector you should get this:



    Meanwhile, if you look at the prefab inspector you'll get:



    The TestA inspector checks to see if it can use FindPropertyRelative with a ScriptableObject. It fails EVERY time.

    The TestB inspector makes a new SerializedObject from the objectReferenceValue and accesses the relative property that way. It works in scene, but not for prefabs.

    The hierarchy I've built is extremely simple. If would be very greatful if you could take a quick look.

    Thanks,
    Ves
     

    Attached Files:

  6. Acegikmo

    Acegikmo

    Joined:
    Jun 23, 2011
    Posts:
    1,294
    That's because, while the ScriptableObject is serialized, it's never saved in the prefab itself. You recreate it every time in OnEnable, and in the prefab, OnEnable isn't called. When saving ScriptableObjects, you also need to save the SOs as assets.
     
  7. VesuvianPrime

    VesuvianPrime

    Joined:
    Feb 26, 2013
    Posts:
    135
    Alright, I think I understand what you're saying.

    In my situation I want to use a hierarchy of one MonoBehaviour and many ScriptableObjects. This avoids making one big god-object and lets me separate my logic.

    You're saying that for my ScriptableObjects to survive leaving the scene they need to be turned into assets.

    So I believe I have two options here:

    1) Figure out some ScriptableObject asset management workflow that is going to be ugly and hacky and probably cause more problems than it solves.

    2) Rewrite everything with good old "object" objects.

    I need to check to see if normal objects will work with SerializedProperty...
     
  8. Acegikmo

    Acegikmo

    Joined:
    Jun 23, 2011
    Posts:
    1,294
  9. VesuvianPrime

    VesuvianPrime

    Joined:
    Feb 26, 2013
    Posts:
    135
    I've just written TestC

    Scene:


    Prefab:


    TestC uses a child that is identical, it simply doesn't inherit from ScriptableObject. I believe this is going to be the correct course for my particular needs. I'm attaching the updated assets for posterity.

    I think my problem has been that articles on Unity serialization very often say "Always inherit from ScriptableObject!" without explaining why.

    Thank you so much for your guidance, I really hope I haven't derailed your thread.

    Thanks,
    Ves
     

    Attached Files:

  10. Acegikmo

    Acegikmo

    Joined:
    Jun 23, 2011
    Posts:
    1,294
    Awesome, glad it worked out!
    So, looking at the chart, if you aren't using ScriptableObjects, mind that arrays of a type will serialize the references as that specific type, ignoring inheritance. Also mind that multiple references to the same object will be duplicated in assembly reloads.
     
    VesuvianPrime likes this.
  11. tswalk

    tswalk

    Joined:
    Jul 27, 2013
    Posts:
    1,109
    I figured out that you don't necessarily have to make every class an SO, as even classes extending SO... there are a couple "gotcha's"... such as building in-code GUISkin's and Texture2D objects.

    short example:

    Code (CSharp):
    1.  
    2. public sealed class InCodeImages
    3. {
    4.         public Texture2D MyLine { get { return _myLine; } }
    5.         private static Texture2D _myLine;
    6.  
    7.         ~InCodeImages()
    8.         {
    9.             //Debug.Log("InCodeImages Destructor.");
    10.         }
    11.  
    12.         public InCodeImages()
    13.         {
    14.             //Debug.Log("InCodeImages Constructor.");
    15.             if (_myLine == null)
    16.             {
    17.                 _myLine = new Texture2D(3, 3, TextureFormat.ARGB32, false);
    18.                 ConstructImage(ref _myLine);
    19.                 _myLine.Apply(false);
    20.                 _myLine.hideFlags = HideFlags.HideAndDontSave;
    21.                 _myLine.filterMode = FilterMode.Bilinear;
    22.                 _myLine.wrapMode = TextureWrapMode.Repeat;
    23.                 _myLine.name = "MyLine";
    24.             }
    25.         }
    26.  
    27.         private void ConstructImage(ref Texture2D _texture)
    28.         {
    29.          
    30.             Color32[] _colors = new Color32[9];
    31.             _colors[0] = new Color(1, 1, 1, 1);
    32.             _colors[1] = new Color(1, 1, 1, 0);
    33.             _colors[2] = new Color(1, 1, 1, 1);
    34.             _colors[3] = new Color(1, 1, 1, 0);
    35.             _colors[4] = new Color(1, 1, 1, 0);
    36.             _colors[5] = new Color(1, 1, 1, 0);
    37.             _colors[6] = new Color(1, 1, 1, 1);
    38.             _colors[7] = new Color(1, 1, 1, 0);
    39.             _colors[8] = new Color(1, 1, 1, 1);
    40.             _texture.SetPixels32(_colors);
    41.         }
    42. }
    43.  

    Usage in other classes:

    Code (CSharp):
    1. .
    2. .
    3. .
    4. // ...  declaration
    5. private static InCodeImages cImages;
    6.  
    7. public void OnEnable()
    8. {
    9.             hideFlags = HideFlags.HideAndDontSave;  //this class is an SO
    10.             if (cImages == null)
    11.             {
    12.                 cImages = new InCodeImages();
    13.             }
    14. //... do more stuffs
    15. .
    16. .
    17. .
    18. }
    19.  

    Yes, I'm using statics and passing the reference so the first call into the constructor builds everything, then subsequent "new Object()" in essence are passing the reference around. The trick for skins and texture2d are the additional flags set after they're created (hideflags threw me off for a bit).

    I may go back and look at creating the classes as static, but sealed should be just as good.
     
  12. liucailin

    liucailin

    Joined:
    Sep 18, 2014
    Posts:
    1
    Hi, Acegikmo, I find it is not working by using custom editor
     
  13. marcospgp

    marcospgp

    Joined:
    Jun 11, 2018
    Posts:
    194
  14. marcospgp

    marcospgp

    Joined:
    Jun 11, 2018
    Posts:
    194
    1. Set hideFlags to HideFlags.HideAndDontSave in OnEnable()
    I have searched all over and it is still not clear to me why we should set hide flags to hide and don't save. Could anyone explain it?

    Edit: From what I understand, setting hide flags to hide and don't save prevents the object from being deleted when it is not referenced in a field of any object in the scene. I can't see a case where I would want to keep an object that was only being used in the "execution stack" (as mentioned in the Unity docs), so I'm not going to touch the flags for now and see what happens.
     
    Last edited: Feb 2, 2022