Serialization Best Practices
来源:互联网 发布:epson手机打印软件 编辑:程序博客网 时间:2024/05/23 16:54
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):
- using UnityEngine;
- using UnityEditor;
- public class MyWindow : EditorWindow
- {
- private SerializeMe m_SerialziedThing;
- [MenuItem ("Window/Serialization")]
- static void Init () {
- GetWindow <MyWindow>();
- }
- void OnEnable ()
- {
- hideFlags = HideFlags.HideAndDontSave;
- if (m_SerialziedThing == null)
- m_SerialziedThing = new SerializeMe ();
- }
- void OnGUI () {
- GUILayout.Label ("Serialized Things", EditorStyles.boldLabel);
- m_SerialziedThing.OnGUI ();
- }
- }
Code (csharp):
- using UnityEditor;
- public struct NestedStruct
- {
- private float m_StructFloat;
- public void OnGUI ()
- {
- m_StructFloat = EditorGUILayout.FloatField("Struct Float", m_StructFloat);
- }
- }
- public class SerializeMe
- {
- private string m_Name;
- private int m_Value;
- private NestedStruct m_Struct;
- public SerializeMe ()
- {
- m_Struct = new NestedStruct();
- m_Name = "";
- }
- public void OnGUI ()
- {
- m_Name = EditorGUILayout.TextField( "Name", m_Name);
- m_Value = EditorGUILayout.IntSlider ("Value", m_Value, 0, 10);
- m_Struct.OnGUI ();
- }
- }
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):
- using UnityEngine;
- using UnityEditor;
- public class MyWindow : EditorWindow
- {
- private SerializeMe m_SerialziedThing;
- [MenuItem ("Window/Serialization")]
- static void Init () {
- GetWindow <MyWindow>();
- }
- void OnEnable ()
- {
- hideFlags = HideFlags.HideAndDontSave;
- if (m_SerialziedThing == null)
- m_SerialziedThing = new SerializeMe ();
- }
- void OnGUI () {
- GUILayout.Label ("Serialized Things", EditorStyles.boldLabel);
- m_SerialziedThing.OnGUI ();
- }
- }
- using System;
- using UnityEditor;
- using UnityEngine;
- [Serializable]
- public class NestedStruct
- {
- [SerializeField]
- private float m_StructFloat;
- public void OnGUI ()
- {
- m_StructFloat = EditorGUILayout.FloatField("Struct Float", m_StructFloat);
- }
- }
- [Serializable]
- public class SerializeMe
- {
- [SerializeField]
- private string m_Name;
- [SerializeField]
- private int m_Value;
- [SerializeField]
- private NestedStruct m_Struct;
- public SerializeMe ()
- {
- m_Struct = new NestedStruct();
- m_Name = "";
- }
- public void OnGUI ()
- {
- m_Name = EditorGUILayout.TextField( "Name", m_Name);
- m_Value = EditorGUILayout.IntSlider ("Value", m_Value, 0, 10);
- m_Struct.OnGUI ();
- }
- }
- 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):
- using System;
- using UnityEditor;
- using UnityEngine;
- [Serializable]
- public class NestedClass
- {
- [SerializeField]
- private float m_StructFloat;
- public void OnGUI()
- {
- m_StructFloat = EditorGUILayout.FloatField("Float", m_StructFloat);
- }
- }
- [Serializable]
- public class SerializeMe
- {
- [SerializeField]
- private NestedClass m_Class1;
- [SerializeField]
- private NestedClass m_Class2;
- public void OnGUI ()
- {
- if (m_Class1 == null)
- m_Class1 = new NestedClass ();
- if (m_Class2 == null)
- m_Class2 = m_Class1;
- m_Class1.OnGUI();
- m_Class2.OnGUI();
- }
- }
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):
- using System;
- using UnityEditor;
- using UnityEngine;
- [Serializable]
- public class NestedClass : ScriptableObject
- {
- [SerializeField]
- private float m_StructFloat;
- public void OnEnable() { hideFlags = HideFlags.HideAndDontSave; }
- public void OnGUI()
- {
- m_StructFloat = EditorGUILayout.FloatField("Float", m_StructFloat);
- }
- }
- [Serializable]
- public class SerializeMe
- {
- [SerializeField]
- private NestedClass m_Class1;
- [SerializeField]
- private NestedClass m_Class2;
- public SerializeMe ()
- {
- m_Class1 = ScriptableObject.CreateInstance<NestedClass> ();
- m_Class2 = m_Class1;
- }
- public void OnGUI ()
- {
- m_Class1.OnGUI();
- m_Class2.OnGUI();
- }
- }
- 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.
- The constructor is called on the ScriptableObject
- Data is serialized into the object from the c++ side of unity (if such data exists)
- 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):
- // also updated the Window to call CreateInstance instead of the constructor
- using System;
- using UnityEngine;
- [Serializable]
- public class SerializeMe : ScriptableObject
- {
- [SerializeField]
- private NestedClass m_Class1;
- [SerializeField]
- private NestedClass m_Class2;
- public void OnEnable ()
- {
- hideFlags = HideFlags.HideAndDontSave;
- if (m_Class1 == null)
- {
- m_Class1 = CreateInstance<NestedClass> ();
- m_Class2 = m_Class1;
- }
- }
- public void OnGUI ()
- {
- m_Class1.OnGUI();
- m_Class2.OnGUI();
- }
- }
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):
- using System;
- using System.Collections.Generic;
- using UnityEditor;
- using UnityEngine;
- [Serializable]
- public class BaseClass
- {
- [SerializeField]
- private int m_IntField;
- public void OnGUI() {m_IntField = EditorGUILayout.IntSlider ("IntField", m_IntField, 0, 10);}
- }
- [Serializable]
- public class SerializeMe : ScriptableObject
- {
- [SerializeField]
- private List<BaseClass> m_Instances;
- public void OnEnable ()
- {
- hideFlags = HideFlags.HideAndDontSave;
- if (m_Instances == null)
- m_Instances = new List<BaseClass> ();
- }
- public void OnGUI ()
- {
- foreach (var instance in m_Instances)
- instance.OnGUI ();
- if (GUILayout.Button ("Add Simple"))
- m_Instances.Add (new BaseClass ());
- }
- }
General Array Serialization
Lets modify the example to serialize a list that contains members of a base class and child class:
Code (csharp):
- using System;
- using System.Collections.Generic;
- using UnityEditor;
- using UnityEngine;
- [Serializable]
- public class BaseClass
- {
- [SerializeField]
- private int m_IntField;
- public virtual void OnGUI() { m_IntField = EditorGUILayout.IntSlider("IntField", m_IntField, 0, 10); }
- }
- [Serializable]
- public class ChildClass : BaseClass
- {
- [SerializeField]
- private float m_FloatField;
- public override void OnGUI()
- {
- base.OnGUI ();
- m_FloatField = EditorGUILayout.Slider("FloatField", m_FloatField, 0f, 10f);
- }
- }
- [Serializable]
- public class SerializeMe : ScriptableObject
- {
- [SerializeField]
- private List<BaseClass> m_Instances;
- public void OnEnable ()
- {
- if (m_Instances == null)
- m_Instances = new List<BaseClass> ();
- hideFlags = HideFlags.HideAndDontSave;
- }
- public void OnGUI ()
- {
- foreach (var instance in m_Instances)
- instance.OnGUI ();
- if (GUILayout.Button ("Add Base"))
- m_Instances.Add (new BaseClass ());
- if (GUILayout.Button ("Add Child"))
- m_Instances.Add (new ChildClass ());
- }
- }
The way to work around this limitation of the serialization system is to once again use ScriptableObjects:
Code (csharp):
- using System;
- using System.Collections.Generic;
- using UnityEngine;
- using UnityEditor;
- [Serializable]
- public class MyBaseClass : ScriptableObject
- {
- [SerializeField]
- protected int m_IntField;
- public void OnEnable() { hideFlags = HideFlags.HideAndDontSave; }
- public virtual void OnGUI ()
- {
- m_IntField = EditorGUILayout.IntSlider("IntField", m_IntField, 0, 10);
- }
- }
- [Serializable]
- public class ChildClass : MyBaseClass
- {
- [SerializeField]
- private float m_FloatField;
- public override void OnGUI()
- {
- base.OnGUI ();
- m_FloatField = EditorGUILayout.Slider("FloatField", m_FloatField, 0f, 10f);
- }
- }
- [Serializable]
- public class SerializeMe : ScriptableObject
- {
- [SerializeField]
- private List<MyBaseClass> m_Instances;
- public void OnEnable ()
- {
- if (m_Instances == null)
- m_Instances = new List<MyBaseClass>();
- hideFlags = HideFlags.HideAndDontSave;
- }
- public void OnGUI ()
- {
- foreach (var instance in m_Instances)
- instance.OnGUI ();
- if (GUILayout.Button ("Add Base"))
- m_Instances.Add(CreateInstance<MyBaseClass>());
- if (GUILayout.Button ("Add Child"))
- m_Instances.Add(CreateInstance<ChildClass>());
- }
- }
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):
- using System;
- using UnityEditor;
- using System.Collections.Generic;
- using UnityEngine;
- [Serializable]
- public abstract class MyBaseClass : ScriptableObject
- {
- [SerializeField]
- protected int m_IntField;
- public void OnEnable() { hideFlags = HideFlags.HideAndDontSave; }
- public abstract void OnGUI ();
- }
- [Serializable]
- public class ChildClass : MyBaseClass
- {
- [SerializeField]
- private float m_FloatField;
- public override void OnGUI()
- {
- m_IntField = EditorGUILayout.IntSlider("IntField", m_IntField, 0, 10);
- m_FloatField = EditorGUILayout.Slider("FloatField", m_FloatField, 0f, 10f);
- }
- }
- [Serializable]
- public class SerializeMe : ScriptableObject
- {
- [SerializeField]
- private List<MyBaseClass> m_Instances;
- public void OnEnable ()
- {
- if (m_Instances == null)
- m_Instances = new List<MyBaseClass>();
- hideFlags = HideFlags.HideAndDontSave;
- }
- public void OnGUI ()
- {
- foreach (var instance in m_Instances)
- instance.OnGUI ();
- if (GUILayout.Button ("Add Child"))
- m_Instances.Add(CreateInstance<ChildClass>());
- }
- }
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/
0 0
- Serialization Best Practices
- Java Best Practices – High performance Serialization
- Best Practices -
- Web Services Best Practices
- JUnit best practices
- Javascript Best Practices
- CAB Best Practices
- 最佳实践(Best Practices)
- Java Database Best Practices
- 一些C# Best Practices
- LIVE Networking: Best Practices
- Scalability Best Practices
- Best Practices for WOW64
- Siebel Scripting Best Practices
- Javascript Best Practices
- Log4j Best Practices
- Best practices when developing
- Java Best Practices
- 神经网络模型算法与生物神经网络的最新联系
- CentOS7——解压7z文件——p7zip
- The Struts dispatcher cannot be found. This is usually caused by using Struts tags without the assoc
- 解决gradle DSL method not found: android()问题
- cengos 安装redis 并将redis启动添加为系统服务
- Serialization Best Practices
- 配置Subversion SVN
- android—mm—mmm—没有规则可以创建target/product/generic/obj/SHARED_LIBRARIES
- Fedora 24 Will Use Wayland By Default, Fedora 24 Server Drops 32 Bit Support
- mm—mmm—android源码—编译
- 两种Domain Entity生成方式
- Category: Android——porting
- 数字的拆分问题和换零钱问题
- 正则表达式pcre在Android下的移植