La serialización de “cosas” está en el núcleo de Unity. Muchas de nuestras características están construidas encima del sistema de serialización:
Ventana del Inspector. La ventana del inspector no le habla al API de C# para averiguar qué valores de las propiedades de lo que se está viendo. Le pregunta al objeto a serializarse en sí mismo, y mostrar la información serializada.
Prefab. Internamente, un prefab es el flujo de datos serializado ( serialized data stream) de uno (o más) game objects y componentes. Una instancia de prefab es una lista de modificaciones que deberían ser hechas en la información serializada de esta instancia. El prefab concepto solo existe en el tiempo del editor. Las modificaciones del prefab son baked a un flujo de serialización (serialization stream) normal cuando Unity hace una construcción, y cuando eso es instanciando, los game objects instanciados no tienen idea de que eran un prefab cuando estaban en el editor.
Instanciación. Cuando usted llama Instantiate()
ya sea en un prefab, o un gameobject que está vivo en la escena, o cualquier otra cosa (cualquier cosa que derive de UnityEngine.Object
puede ser serializado), nosotros serializamos el objeto, luego creamos un nuevo objeto, y luego “deserializamos” la información al nuevo objeto. (Nosotros luego corremos el mismo código de serialización en una diferente variante, dónde nosotros lo utilizamos para reportar qué otros UnityEngine.Objects
están siendo referenciados. Luego revisamos para todos los UnityEngine.Objects
referenciados, si son parte de la información siendo Instantiated()
. Si la referencia está apuntando a algo “externo” (como una textura) nosotros mantenemos esa referencia como está, si está apuntando a algo “interno” (como un hijo gameobject), nosotros conectamos la referencia a la copia correspondiente).
Guardando. Si usted abre un archivo de escena .unity
con un editor de texto, y tiene configurado Unity a “force text serialization”, nosotros corremos el serializador con un yaml backend.
Cargando. Puede no resultar una sorpresa, pero para una carga al revés compatible es un sistema que está integrado encima de la serialización también. En el editor yaml, cargar utiliza el sistema de serialización, pero también la carga de escenas, assets y assetbundles en el tiempo de ejecución utilizan el sistema de serialización.
Cargar nuevamente el código del editor. Cuando usted cambia el script del editor, nosotros serializamos todas las ventanas del editor (ellas deriva de UnityEngine.Object
!). Luego nosotros destruimos todas las ventanas. Nosotros descargamos el código C# viejo, nosotros cargamos el nuevo código C#, nosotros recreamos las ventanas, y luego nosotros deserializamos los flujos de información de las ventanas a las nuevas ventanas.
Resource.GarbageCollectSharedAssets()
. Este es nuestro nativo recolector de basura (garbage collector). Es una cosa diferente que el recolector de C#. Es la cosa que corremos después de que cargue una escena, para encontrar qué cosas de la escena anterior no son referenciadas, para que las podamos descargar. El recolector de basura nativo corre el serializador en una variación dónde nosotros lo utilizamos para que los objetos reporten todas las referencias a UnityEngine.Objects
externos. Esto es lo que hace que las texturas que fueron utilizadas por la escena1, sean descargadas cuando usted cargue la escena2.
El sistema de serialización está escrito en C++. Nosotros lo utilizamos para todos nuestros tipos de objetos internos. (Texturas, Clip de Animación (AnimationClip), Cámara, etc). La serialización ocurre en el nivel de UnityEngine.Object
. Cada UnityEngine.Object
es siempre serializado como un entero. Estos pueden contener referencias a otros UnityEngine.Objects
, y esas referencias son serializadas de manera adecuada.
Ahora usted dice que ninguno de esto lo afecta a usted, y que está feliz que funcione y quiere seguir a crear algo de contenido.
Dónde le va a importar usted es que nosotros utilizamos este mismo serializador para serializar componentes MonoBehaviour
, que son respaldados por sus scripts. Debido a los altos requerimientos de rendimiento que el serializador tiene, éste en todos los casos no se comporta como un desarrollador en C# se esperaría que un serializador funcionara. En esta parte de los docs, nosotros describiremos cómo el serializador funciona, y algunas de las mejores prácticas de cómo sacarle el mejor uso.
public
, o tener un atributo [SerializeField]
static
const
readonly
fieldtype
necesita ser de un tipo el cual podemos serializar.[Serializable]
.[Serializable]
. (Agregado en Unity 4.5)UnityEngine.Object
int
, float
, double
, bool
, string
, etc.)[Serializable]
class Animal
{
public string name;
}
class MyScript : MonoBehaviour
{
public Animal[] animals;
}
Si usted llena el arreglo de animales con tres referencias a un solo objeto Animal, en el flujo de serialización (serialization stream), usted encontrará 3 objetos. Cuando sea deserializado, ahora hay tres diferentes objetos. Si usted necesita serializar un objeto de gráfica complejo con referencias, usted no puede depende de que el serializador de Unity haga todo esto automáticamente para usted, y necesite hace algo de trabajo para obtener la gráfica objeto serializada por usted mismo. Vea el ejemplo a continuación en cómo serializar cosas que Unity no serializa por sí.
Tenga en cuenta que esto es solo verdad para clases personalizadas, ya que estas fueron serializadas “en linea”, debido a que su información se vuelve parte de los datos completos serializados para el MonoBehaviour en el cual son utilizados. Cuando usted tiene campos que tienen una referencia a lgo que es una clase derivada de UnityEngine.Object
, como un public Camera myCamera
, los datos de esa cámara no son serializados en linea, y una referencia actual al UnityEngine.Object
de la cámara es serializado,
null
Quiz Sorpresa: Cuántas alocaciones son hechas cuando se deserializa un MonoBehaviour que utiliza este script:
class Test : MonoBehaviour
{
public Trouble t;
}
[Serializable]
class Trouble
{
public Trouble t1;
public Trouble t2;
public Trouble t3;
}
No sería raro esperar 1 alocación. Esa para el objeto Test. Tampoco sería raro esperar 2 alocaciones. Una para el objeto Test, y una para el objeto Trouble. La respuesta correcta es 729. El serializado no soporta null. Si serializa un objeto, y un campo es null, nosotros podemos instanciar un nuevo objeto de ese tipo, y serializar eso. Obviamente esto puede llevar a ciclos infinitos, por lo que tenemos un limite de profundidad mágica relativo de 7 niveles. En este punto nosotros simplemente paramos los campos de serialización que tienen tipos de struct/clases personalizadas y listas y arreglos
Debido a que muchos de nuestros sub-sistemas construidos encima del sistema de serialización, este inesperado flujo de serialización (serialization stream) para el Test monobehaviour va a causar que estos subsistemas sean más lentos de lo necesario. Cuando nosotros investiguemos los problemas de rendimiento en nuestros proyectos personalizados, casi siempre nosotros encontramos este problema. Nosotros hemos agregado una advertencia para esta situación en Unity 4.5.
si usted tiene un public Animal[] animals
y lo coloca en una instancia de un perro, o gato y una jirafa, después de la serialización, usted tendrá tres instancias de Animal.
Una manera de manejar esta limitación es darse cuenta que solo aplica a “clases personalizadas”, las cuales son serializadas en linea. Referencias a otros UnityEngine.Objects
son serializados como referencias actuales, y para estos los polimorfismos de verdad funciona. Usted hace un ScriptableObject
derivado de una clase o otro MonoBehaviour
derivado de una clase, y referencia esto. La desventaja de esto es que usted necesita almacenar ese objeto monobehaviour o scriptable en alguna parte, y no puede serializar en linea de manera optima.
La razón para estas limitaciones es que una de las bases fundamentales del sistema de serialización es que el diseño del flujo de información (datastream) para un objeto es conocido antes de tiempo, y depende en los tipos de los campos de la clase, en vez de lo que pasa a ser almacenado dentro de los campos.
En la mayoría de casos el mejor acercamiento es utilizar callbacks de serialización. Estas le permiten a usted ser notificado antes de que el serializador lea información de los capos y después que es hecho, escribir en ellos. Usted puede utilizar esto para tener diferentes representaciones de su data difícil de serializar en el tiempo de ejecución cuando usted de verdad serializa. Usted utiliza estas para transformar sus datos a algo que Unity entienda justo antes de que Unity quiera serializarlo, y usted lo utilice para transformar la forma del serializado a la forma que usted quiere tener sus datos en el tiempo de ejecución justo después de que Unity ha escrito datos a sus campos.
Digamos que usted quiere tener una estructura de datos árbol. Si usted le permite a Unity directamente serializar la estructura de datos, la limitación de “no support for null” (No hay soporte para null) va a causar que su flujo de datos (datastream) se vuelva muy grande, llevando a una degradación de rendimiento en muchos sistemas:
using UnityEngine;
using System.Collections.Generic;
using System;
public class VerySlowBehaviourDoNotDoThis : MonoBehaviour
{
[Serializable]
public class Node
{
public string interestingValue = "value";
//The field below is what makes the serialization data become huge because
//it introduces a 'class cycle'.
public List<Node> children = new List<Node>();
}
//this gets serialized
public Node root = new Node();
void OnGUI()
{
Display (root);
}
void Display(Node node)
{
GUILayout.Label ("Value: ");
node.interestingValue = GUILayout.TextField(node.interestingValue, GUILayout.Width(200));
GUILayout.BeginHorizontal ();
GUILayout.Space (20);
GUILayout.BeginVertical ();
foreach (var child in node.children)
Display (child);
if (GUILayout.Button ("Add child"))
node.children.Add (new Node ());
GUILayout.EndVertical ();
GUILayout.EndHorizontal ();
}
}
Más bien, usted le dice a Unity que no serialize el árbol directamente, y usted hace un campo separado para almacenar el árbol en formato serializado, adecuado para el serializador de Unity:
using UnityEngine;
using System.Collections.Generic;
using System;
public class BehaviourWithTree : MonoBehaviour, ISerializationCallbackReceiver
{
//node class that is used at runtime
public class Node
{
public string interestingValue = "value";
public List<Node> children = new List<Node>();
}
//node class that we will use for serialization
[Serializable]
public struct SerializableNode
{
public string interestingValue;
public int childCount;
public int indexOfFirstChild;
}
//the root of what we use at runtime. not serialized.
Node root = new Node();
//the field we give unity to serialize.
public List<SerializableNode> serializedNodes;
public void OnBeforeSerialize()
{
//unity is about to read the serializedNodes field's contents. lets make sure
//we write out the correct data into that field "just in time".
serializedNodes.Clear();
AddNodeToSerializedNodes(root);
}
void AddNodeToSerializedNodes(Node n)
{
var serializedNode = new SerializableNode () {
interestingValue = n.interestingValue,
childCount = n.children.Count,
indexOfFirstChild = serializedNodes.Count+1
};
serializedNodes.Add (serializedNode);
foreach (var child in n.children)
AddNodeToSerializedNodes (child);
}
public void OnAfterDeserialize()
{
//Unity has just written new data into the serializedNodes field.
//let's populate our actual runtime data with those new values.
if (serializedNodes.Count > 0)
root = ReadNodeFromSerializedNodes (0);
else
root = new Node ();
}
Node ReadNodeFromSerializedNodes(int index)
{
var serializedNode = serializedNodes [index];
var children = new List<Node> ();
for(int i=0; i!= serializedNode.childCount; i++)
children.Add(ReadNodeFromSerializedNodes(serializedNode.indexOfFirstChild + i));
return new Node() {
interestingValue = serializedNode.interestingValue,
children = children
};
}
void OnGUI()
{
Display (root);
}
void Display(Node node)
{
GUILayout.Label ("Value: ");
node.interestingValue = GUILayout.TextField(node.interestingValue, GUILayout.Width(200));
GUILayout.BeginHorizontal ();
GUILayout.Space (20);
GUILayout.BeginVertical ();
foreach (var child in node.children)
Display (child);
if (GUILayout.Button ("Add child"))
node.children.Add (new Node ());
GUILayout.EndVertical ();
GUILayout.EndHorizontal ();
}
}
Tenga cuidado que el serializador, incluyendo estos callbacks viniendo del serializador usualmente suceden no en el thread principal, por lo cual usted está muy limitado en lo que usted puede hacer en términos de invocar el Unity API. Usted, sin embargo, puede hacer las transformaciones necesarias de datos para obtener sus datos desde un formato serializado no amigable para Unity a un formato serializado amigable para Unity.
Serialización de Script When scripts call the Unity API from constructors or field initializers, or during deserialization (loading), they can trigger errors. This section demonstrates the poor practise that causes these errors.
You should call the majority of the Unity API from the main thread; for example, from Start
or Update
on MonoBehaviour. You should only call a subset of the Unity API from script constructors or field initializers; for example, Debug.Log
or Mathf
. This is because constructors are invoked when constructing an instance of a class during deserialization, and these should only run on a main thread but may end up running on a non-main thread. So, you will get errors if you call all the Unity API from script constructors or field initializers.
In Unity 5.4 these errors do not, in most cases, throw a managed exception, and do not interrupt the execution flow of your scripts. This eases the process of upgrading your project to Unity 5.4. However, these errors will throw a managed exception in subsequent releases of Unity. You should therefore fix any errors as soon as possible when upgrading to 5.4.
When Unity creates an instance of a MonoBehaviour or ScriptableObject derived class, it calls the default constructor to create the managed object. This happens before entering the main loop, and before the scene has been fully loaded. Field initializers are also called from the default constructor of a managed object. In general, do not call the Unity API from a constructor, as this is unsafe for the majority of the Unity API.
Examples of poor practice:
//NOTE: THIS IS A DELIBERATE BAD EXAMPLE TO DEMONSTRATE POOR PRACTISE - DO NOT REUSE
public class FieldAPICallBehaviour : MonoBehaviour
{
public GameObject foo = GameObject.Find("foo"); // This line will generate an error
// message as it should not be called from within a constructor
}
//NOTE: THIS IS A BAD EXAMPLE TO DEMONSTRATE POOR PRACTISE - DO NOT REUSE
public class ConstructorAPICallBehaviour : MonoBehaviour
{
ConstructorAPICallBehaviour()
{
GameObject.Find("foo"); // This line will generate an error message
// as it should not be called from within a constructor
}
}
Both these cases generate the error message: “Find is not allowed to be called from a MonoBehaviour constructor (or instance field initializer), call in in Awake or Start instead.”
Fix this by making the the call to the Unity API in MonoBehaviour.Start
.
When Unity loads a scene, it recreates the managed objects from the saved scene and populates them with the saved values (deserializing). In order to create the managed objects, call the default constructor for the objects. If a field referencing an object is saved (serialized) and the object default constructor calls the Unity API, you will get an error when loading the scene. As with the previous error, it is not yet in the main loop and the scene is not fully loaded. This is considered unsafe for the majority of the Unity API.
Example of poor practice:
//NOTE: THIS IS A BAD EXAMPLE TO DEMONSTRATE POOR PRACTISE - DO NOT REUSE
public class SerializationAPICallBehaviour : MonoBehaviour
{
[System.Serializable]
public class CallAPI
{
public CallAPI()
{
GameObject.Find("foo"); // This line will generate an error message
// as it should not be called during serialization
}
}
CallAPI callAPI;
}
This generates the error: “Find is not allowed to be called during serialization, call it from Awake or Start instead.”
To fix this, refactor your code so that no Unity API calls are made in any constructors for any serialized objects. If you need to call the Unity API for an object, do this in the main thread from one of the MonoBehaviour callbacks, such as Start
, Awake
or Update
.