xak的专栏

Sometimes when I look at the world, I can see a line behind it.

Serialization Best Practices

Unity Serialization

So you are writing a really cool editor extension in Unity and things seem to be going really well. You get your data structures all sorted out are really happy with how the tool you have written works.

Then you enter and exit play mode.

Suddenly all the data you had entered is gone and your tool is reset to the default, just initialized state. It’s very frustrating! “Why does this happen?” you ask yourself. The reason has to do with how the managed (mono) layer of Unity works. Once you understand it, then things get much easier :)

What happens when an assembly is reloaded?
When you enter / exit play mode or change a script Unity has to reload the mono assemblies, that is the dll's associated with Unity. 

On the user side this is a 3 step process:
  • Pull all the serializable data out of managed land, creating an internal representation of the data on the C++ side of Unity.
  • Destroy all memory / information associated with the managed side of Unity, and reload the assemblies.
  • Reserialize the data that was saved in C++ back into managed land.

What this means is that for your data structures / information to survive an assembly reload you need to ensure that can get serialized into and out of c++ memory properly. Doing this also means that (with some minor modifications) you can save this data structure to an asset file and reload it at a later date.

How do I work with Unity's serialization?
The easiest way to learn about Unity serialization is by working through an example. We are going to start with a simple editor window, it contains a reference to a class which we want to make survive an assembly reload.

Code (csharp):
  1.  
  2. using UnityEngine;
  3. using UnityEditor;
  4.  
  5. public class MyWindow : EditorWindow
  6. {
  7.     private SerializeMe m_SerialziedThing;
  8.  
  9.     [MenuItem ("Window/Serialization")]
  10.     static void Init () {
  11.         GetWindow <MyWindow>();
  12.     }
  13.  
  14.     void OnEnable ()
  15.     {
  16.         hideFlags = HideFlags.HideAndDontSave;
  17.         if (m_SerialziedThing == null)
  18.             m_SerialziedThing = new SerializeMe ();
  19.     }
  20.  
  21.     void OnGUI () {
  22.         GUILayout.Label ("Serialized Things"EditorStyles.boldLabel);
  23.         m_SerialziedThing.OnGUI ();
  24.     }
  25. }
  26.  
Code (csharp):
  1.  
  2. using UnityEditor;
  3.  
  4. public struct NestedStruct
  5. {
  6.     private float m_StructFloat;
  7.     public void OnGUI ()
  8.     {
  9.         m_StructFloat = EditorGUILayout.FloatField("Struct Float", m_StructFloat);
  10.     }
  11. }
  12.  
  13. public class SerializeMe
  14. {
  15.     private string m_Name;
  16.     private int m_Value;
  17.  
  18.     private NestedStruct m_Struct;
  19.  
  20.     public SerializeMe ()
  21.     {
  22.         m_Struct = new NestedStruct();
  23.         m_Name = "";
  24.     }
  25.  
  26.     public void OnGUI ()
  27.     {
  28.         m_Name = EditorGUILayout.TextField( "Name", m_Name);
  29.         m_Value = EditorGUILayout.IntSlider ("Value", m_Value, 010);
  30.  
  31.         m_Struct.OnGUI ();
  32.     }
  33. }
  34.  
When you run this and force an assembly reload you will notice that any value in the window you have changed will not survive. This is because when the assembly is reloaded the reference to the ‘m_SerialziedThing’ is gone. It is not marked up to be serialized.

There are a few things that need to be done to make this serialization work properly:
In MyWindow.cs:
  • The field ‘m_SerializedThing’ needs to have the attribute [SerializeField] added to it. What this tells Unity is that it should attempt to serialize this field on assembly reload or similar events.

In SerializeMe.cs:
  • The class ‘SerializeMe’ needs to have the [Serializable] attribute added to it. This tells Unity that the class is serializable.
  • The struct ‘NestedStruct’ needs to have the [Serializable] attribute added to it.
  • Each (non public) field that you want to be serialized needs to have the [SerializeField] attribute added to it.

After adding these flags open the window and modify the fields. You will notice that after an assembly reload that the fields retain their values; that is apart from the field that came from the struct. This brings up the first important point, structs are not very well supported for serialization. Changing ‘NestedStruct’ from a struct to a class fixes this issue.

The code now looks like this:
Code (csharp):
  1.  
  2. using UnityEngine;
  3. using UnityEditor;
  4.  
  5. public class MyWindow : EditorWindow
  6. {
  7.     private SerializeMe m_SerialziedThing;
  8.  
  9.     [MenuItem ("Window/Serialization")]
  10.     static void Init () {
  11.         GetWindow <MyWindow>();
  12.     }
  13.  
  14.     void OnEnable ()
  15.     {
  16.         hideFlags = HideFlags.HideAndDontSave;
  17.         if (m_SerialziedThing == null)
  18.             m_SerialziedThing = new SerializeMe ();
  19.     }
  20.  
  21.     void OnGUI () {
  22.         GUILayout.Label ("Serialized Things"EditorStyles.boldLabel);
  23.         m_SerialziedThing.OnGUI ();
  24.     }
  25. }
  26.  
  27. using System;
  28. using UnityEditor;
  29. using UnityEngine;
  30.  
  31. [Serializable]
  32. public class NestedStruct
  33. {
  34.     [SerializeField]
  35.     private float m_StructFloat;
  36.     public void OnGUI ()
  37.     {
  38.         m_StructFloat = EditorGUILayout.FloatField("Struct Float", m_StructFloat);
  39.     }
  40. }
  41.  
  42. [Serializable]
  43. public class SerializeMe
  44. {
  45.     [SerializeField]
  46.     private string m_Name;
  47.     [SerializeField]
  48.     private int m_Value;
  49.     [SerializeField]
  50.     private NestedStruct m_Struct;
  51.  
  52.     public SerializeMe ()
  53.     {
  54.         m_Struct = new NestedStruct();
  55.         m_Name = "";
  56.     }
  57.  
  58.     public void OnGUI ()
  59.     {
  60.         m_Name = EditorGUILayout.TextField( "Name", m_Name);
  61.         m_Value = EditorGUILayout.IntSlider ("Value", m_Value, 010);
  62.  
  63.         m_Struct.OnGUI ();
  64.     }
  65. }
  66.  
Some Serialization Rules
  • Avoid structs
  • Classes you want to be serializable need to be marked with [Serializable]
  • Public fields are serialized (so long as they reference a [Serializable] class)
  • Private fields are serialized under some circumstances (editor).
  • Mark private fields as [SerializeField] if you wish them to be serialized.
  • [NonSerialized] exists for fields that you do not want to serialize

Scriptable Objects
So far we have looked at using normal classes when it comes to serialization. Unfortunately using plain classes has some issues when it comes to serialization in Unity. Lets take a look at an example.

Code (csharp):
  1.  
  2. using System;
  3. using UnityEditor;
  4. using UnityEngine;
  5.  
  6. [Serializable]
  7. public class NestedClass
  8. {
  9.     [SerializeField]
  10.     private float m_StructFloat;
  11.     public void OnGUI()
  12.     {
  13.         m_StructFloat = EditorGUILayout.FloatField("Float", m_StructFloat);
  14.     }
  15. }
  16.  
  17. [Serializable]
  18. public class SerializeMe
  19. {
  20.     [SerializeField]
  21.     private NestedClass m_Class1;
  22.  
  23.     [SerializeField]
  24.     private NestedClass m_Class2;
  25.  
  26.     public void OnGUI ()
  27.     {
  28.         if (m_Class1 == null)
  29.             m_Class1 = new NestedClass ();
  30.         if (m_Class2 == null)
  31.             m_Class2 = m_Class1;
  32.  
  33.         m_Class1.OnGUI();
  34.         m_Class2.OnGUI();
  35.     }
  36. }
  37.  
This is a contrived example to show a very specific corner case of the Unity serialization system that can catch you if you are not careful. You will notice that we have two fields of type NestedClass. The first time the window is drawn it will show both the fields, and as m_Class1 and m_Class2 point to the same reference, modifying one will modify the other.

Now try reloading the assembly by entering and exiting play mode... The references have been decoupled. This is due to how serialization of works when you mark a class as simply [Serializable]

When you are serializing standard classes Unity walks through the fields of the class and serializes each one individually, even if the reference is shared between multiple fields. This means that you could have the same object serialized multiple times, and on deserialization the system will not know they are really the same object. If you are designing a complex system this is a frustrating limitation because it means that complex interactions between classes can not be captured properly.

Enter ScriptableObjects! ScriptableObjects are a type of class that correctly serializes as references, so that they only get serialized once. This allows complex class interactions to be stored in a way that you would expect. Internally in Unity ScriptableObjects and MonoBehaviours are the same; in userland code you can have a ScriptableObject that is not attached to a GameObject; this is different to how MonoBehaviour works. They are great for general data structure serialization.

Let’s modify the example to be able to handle serialization properly:

Code (csharp):
  1.  
  2. using System;
  3. using UnityEditor;
  4. using UnityEngine;
  5.  
  6. [Serializable]
  7. public class NestedClass : ScriptableObject
  8. {
  9.     [SerializeField]
  10.     private float m_StructFloat;
  11.  
  12.     public void OnEnable() { hideFlags = HideFlags.HideAndDontSave}
  13.  
  14.     public void OnGUI()
  15.     {
  16.         m_StructFloat = EditorGUILayout.FloatField("Float", m_StructFloat);
  17.     }
  18. }
  19.  
  20. [Serializable]
  21. public class SerializeMe
  22. {
  23.     [SerializeField]
  24.     private NestedClass m_Class1;
  25.  
  26.     [SerializeField]
  27.     private NestedClass m_Class2;
  28.  
  29.     public SerializeMe ()
  30.     {
  31.         m_Class1 = ScriptableObject.CreateInstance<NestedClass> ();
  32.         m_Class2 = m_Class1;
  33.     }
  34.  
  35.     public void OnGUI ()
  36.     {
  37.         m_Class1.OnGUI();
  38.         m_Class2.OnGUI();
  39.     }
  40. }
  41.  
The three changes of note here are that:
  • NestedClass is now a ScriptableObject.
  • We create an instance using the CreateInstance<> function instead of calling the constructor.
  • We also set the hide flags... this will be explained later

These simple changes mean that the instance of the NestedClass will only be serialized once, with each of the references to the class pointing to the same one.

ScriptableObject Initialization
So now we know that for complex data structures where external referencing is needed it is a good idea to use ScriptableObjects. But what is the correct way to work with ScriptableObjects from user code? The first thing to examine is HOW scriptable objects are initialized, especially from the Unity serialization system.
  1. The constructor is called on the ScriptableObject
  2. Data is serialized into the object from the c++ side of unity (if such data exists)
  3. OnEnable() is called on the ScriptableObject

Working with this knowledge there are some things that we can say:
  • Doing initialization in the constructor isn’t a very good idea as data will potentially be overridden by the serialization system.
  • Serialization happens AFTER construction, so we should do our configuration stuff after serialization.
  • OnEnable() seems like the best candidate for initialization.

Lets make some changes to the ‘SerializeMe’ class so that it is a ScriptableObject. This will allow us to see the correct initialization pattern for ScriptableObjects.

Code (csharp):
  1.  
  2. // also updated the Window to call CreateInstance instead of the constructor
  3. using System;
  4. using UnityEngine;
  5.  
  6. [Serializable]
  7. public class SerializeMe : ScriptableObject
  8. {
  9.     [SerializeField]
  10.     private NestedClass m_Class1;
  11.  
  12.     [SerializeField]
  13.     private NestedClass m_Class2;
  14.  
  15.     public void OnEnable ()
  16.     {
  17.         hideFlags = HideFlags.HideAndDontSave;
  18.         if (m_Class1 == null)
  19.         {
  20.             m_Class1 = CreateInstance<NestedClass> ();
  21.             m_Class2 = m_Class1;
  22.         }
  23.     }
  24.  
  25.     public void OnGUI ()
  26.     {
  27.         m_Class1.OnGUI();
  28.         m_Class2.OnGUI();
  29.     }
  30. }
  31.  
On the surface it seems that we have not really changed this class much, it now inherits from ScriptableObject and instead of using a constructor has an OnEnable(). The important part to take note of is slightly more subtle... OnEnable() is called AFTER serialization; because of this we can see if the [SerializedFields] are null or not. If they are null it indicates that this the first initialization, and we need to construct the instances. If they are not null then they have been loaded into memory, and do NOT need to be constructed. It is common in OnEnable() to also call a custom Initialization function to configure any private / non serialized fields on the object, much like you would do in a constructor.

HideFlags
In the examples using ScriptableObjects you will notice that we are setting the ‘hideFlags’ on the object to HideFlags.HideAndDontSave. This is a special setup that is required when writing custom data structures that have no root in the scene. This is to get around how the Garbage Collector works in Unity. 

When the garbage collector is run it (for the most part) uses the scene as ‘the root’ and traverses the hierarchy to see what can get GC’d. Setting the HideAndDontSave flag on a ScriptableObject tells Unity to consider that object as a root object. Because of this it will not just disappear because of a GC / assembly reload. The object can still be destroyed by calling Destroy().

Some ScriptableObject Rules
  • ScriptableObjects will only be serialized once, allowing you to use references properly
  • Use OnEnable to initialize ScriptableObjects
  • Don’t ever call the constructor of a ScriptableObject, use CreatInstance instead.
  • For nested data structures that are only referenced once don’t use ScriptableObject as they have more overhead.
  • If your scriptable object is not rooted in the scene set the hideFlags to HideAndDontSave

Concrete Array Serialization
Lets have a look at a simple example that serializes a range of concrete classes.

Code (csharp):
  1.  
  2. using System;
  3. using System.Collections.Generic;
  4. using UnityEditor;
  5. using UnityEngine;
  6.  
  7. [Serializable]
  8. public class BaseClass
  9. {
  10.     [SerializeField]
  11.     private int m_IntField;
  12.     public void OnGUI() {m_IntField = EditorGUILayout.IntSlider ("IntField", m_IntField, 010);}
  13. }
  14.  
  15. [Serializable]
  16. public class SerializeMe : ScriptableObject
  17. {
  18.     [SerializeField]
  19.     private List<BaseClass> m_Instances;
  20.  
  21.     public void OnEnable ()
  22.     {
  23.         hideFlags = HideFlags.HideAndDontSave;
  24.         if (m_Instances == null)
  25.             m_Instances = new List<BaseClass> ();
  26.     }
  27.  
  28.     public void OnGUI ()
  29.     {
  30.         foreach (var instance in m_Instances)
  31.             instance.OnGUI ();
  32.  
  33.         if (GUILayout.Button ("Add Simple"))
  34.             m_Instances.Add (new BaseClass ());
  35.     }
  36. }
  37.  
This basic example has a list of BaseClasses, by clicking the ‘Add Simple’ button it creates an instance and adds it to the list. Due to the SerializeMe class being configured properly for serialization (as discussed before) it ‘just works’. Unity sees that the List is marked for serialization and serializes each of the List elements.

General Array Serialization
Lets modify the example to serialize a list that contains members of a base class and child class:

Code (csharp):
  1.  
  2. using System;
  3. using System.Collections.Generic;
  4. using UnityEditor;
  5. using UnityEngine;
  6.  
  7. [Serializable]
  8. public class BaseClass
  9. {
  10.     [SerializeField]
  11.     private int m_IntField;
  12.     public virtual void OnGUI() { m_IntField = EditorGUILayout.IntSlider("IntField", m_IntField, 010)}
  13. }
  14.  
  15. [Serializable]
  16. public class ChildClass : BaseClass
  17. {
  18.     [SerializeField]
  19.     private float m_FloatField;
  20.     public override void OnGUI()
  21.     {
  22.         base.OnGUI ();
  23.         m_FloatField = EditorGUILayout.Slider("FloatField", m_FloatField, 0f, 10f);
  24.     }
  25. }
  26.  
  27. [Serializable]
  28. public class SerializeMe : ScriptableObject
  29. {
  30.     [SerializeField]
  31.     private List<BaseClass> m_Instances;
  32.  
  33.     public void OnEnable ()
  34.     {
  35.         if (m_Instances == null)
  36.             m_Instances = new List<BaseClass> ();
  37.  
  38.         hideFlags = HideFlags.HideAndDontSave;
  39.     }
  40.  
  41.     public void OnGUI ()
  42.     {
  43.         foreach (var instance in m_Instances)
  44.             instance.OnGUI ();
  45.  
  46.         if (GUILayout.Button ("Add Base"))
  47.             m_Instances.Add (new BaseClass ());
  48.         if (GUILayout.Button ("Add Child"))
  49.             m_Instances.Add (new ChildClass ());
  50.     }
  51. }
  52.  
The example has been extended so that there is now a ChildClass, but we are serializing using the BaseClass. If you create a few instance of the ChildClass and the BaseClass they will render properly. Issues arise when they are placed through an assembly reload. After the reload completes every instance will be a BaseClass, with all the ChildClass information stripped. The instances are being sheared by the serialization system.

The way to work around this limitation of the serialization system is to once again use ScriptableObjects:

Code (csharp):
  1.  
  2. using System;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. using UnityEditor;
  6.  
  7. [Serializable]
  8. public class MyBaseClass : ScriptableObject
  9. {
  10.     [SerializeField]
  11.     protected int m_IntField;
  12.  
  13.     public void OnEnable() { hideFlags = HideFlags.HideAndDontSave}
  14.  
  15.     public virtual void OnGUI ()
  16.     {
  17.         m_IntField = EditorGUILayout.IntSlider("IntField", m_IntField, 010);
  18.     }
  19. }
  20.  
  21. [Serializable]
  22. public class ChildClass : MyBaseClass
  23. {
  24.     [SerializeField]
  25.     private float m_FloatField;
  26.  
  27.     public override void OnGUI()
  28.     {
  29.         base.OnGUI ();
  30.         m_FloatField = EditorGUILayout.Slider("FloatField", m_FloatField, 0f, 10f);
  31.     }
  32. }
  33.  
  34. [Serializable]
  35. public class SerializeMe : ScriptableObject
  36. {
  37.     [SerializeField]
  38.     private List<MyBaseClass> m_Instances;
  39.  
  40.     public void OnEnable ()
  41.     {
  42.         if (m_Instances == null)
  43.             m_Instances = new List<MyBaseClass>();
  44.  
  45.         hideFlags = HideFlags.HideAndDontSave;
  46.     }
  47.  
  48.     public void OnGUI ()
  49.     {
  50.         foreach (var instance in m_Instances)
  51.             instance.OnGUI ();
  52.  
  53.         if (GUILayout.Button ("Add Base"))
  54.             m_Instances.Add(CreateInstance<MyBaseClass>());
  55.         if (GUILayout.Button ("Add Child"))
  56.             m_Instances.Add(CreateInstance<ChildClass>());
  57.     }
  58. }
  59.  
After running this, changing some values, and reloading assemblies you will notice that ScriptableObjects are safe to use in arrays even if you are serializing derived types. The reason is that when you serialize a standard [Serializable] class it is serialized ‘in place’, but a ScriptableObject is serialized externally and the reference inserted into the collection. The shearing occurs because the type can not be properly be serialized as the serialization system thinks it is of the base type.

Serializing Abstract Classes
So now we have seen that it’s possible to serialize a general list (so long as the members are of type ScriptableObject). Lets see how abstract classes behave:

Code (csharp):
  1.  
  2. using System;
  3. using UnityEditor;
  4. using System.Collections.Generic;
  5. using UnityEngine;
  6.  
  7. [Serializable]
  8. public abstract class MyBaseClass : ScriptableObject
  9. {
  10.     [SerializeField]
  11.     protected int m_IntField;
  12.  
  13.     public void OnEnable() { hideFlags = HideFlags.HideAndDontSave}
  14.  
  15.     public abstract void OnGUI ();
  16. }
  17.  
  18. [Serializable]
  19. public class ChildClass : MyBaseClass
  20. {
  21.     [SerializeField]
  22.     private float m_FloatField;
  23.  
  24.     public override void OnGUI()
  25.     {
  26.         m_IntField = EditorGUILayout.IntSlider("IntField", m_IntField, 010);
  27.         m_FloatField = EditorGUILayout.Slider("FloatField", m_FloatField, 0f, 10f);
  28.     }
  29. }
  30.  
  31. [Serializable]
  32. public class SerializeMe : ScriptableObject
  33. {
  34.     [SerializeField]
  35.     private List<MyBaseClass> m_Instances;
  36.  
  37.     public void OnEnable ()
  38.     {
  39.         if (m_Instances == null)
  40.             m_Instances = new List<MyBaseClass>();
  41.  
  42.         hideFlags = HideFlags.HideAndDontSave;
  43.     }
  44.  
  45.     public void OnGUI ()
  46.     {
  47.         foreach (var instance in m_Instances)
  48.             instance.OnGUI ();
  49.  
  50.         if (GUILayout.Button ("Add Child"))  
  51.             m_Instances.Add(CreateInstance<ChildClass>());
  52.     }
  53. }
  54.  
This code much like the previous example works. But it IS dangerous. Lets see why.

The function CreateInstance<>() expects a type that inherits from ScriptableObject, the class ‘MyBaseClass’ does in fact inherit from ScriptableObject. This means that it’s possible to add an instance of the abstract class MyBaseClass to the m_Instances array. If you do this and then try and access an abstract method bad things will happen because there is no implementation of that function. In this specific case that would be the OnGUI method.

Using abstract classes as the serialized type for lists and fields DOES work, so long as they inherit from ScriptableObject, but it is not a recommended practice. Personally I think it’s better to use concrete classes with empty virtual methods. This ensures that things will not go bad for you.

From:http://forum.unity3d.com/threads/serialization-best-practices-megapost.155352/

阅读更多
个人分类: Unity开发
想对作者说点什么? 我来说一句

Git.Best.Practices.Guide.1783553731

2015年04月15日 2.17MB 下载

React Design Patterns and Best Practices

2017年04月20日 3.78MB 下载

没有更多推荐了,返回首页

不良信息举报

Serialization Best Practices

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭