Пользовательская сериализация
Когда вы хотите серийизировать что-то, что не поддерживается серийизатором Unity (например, словарь C#), вы можете реализовать ISerializationCallbackReceiver интерфейс в своём классе. Так вы сможете реализовать обратные вызовы, которые Unity вызывает в ключевые моменты сериализации и десериализации.
Вы можете использовать обратные вызова сериализации, чтобы дать вашим трудносериализуемым данным другое представление во время выполнения, чем во время сериализации. Вы можете преобразовать ваши данные в то, что Unity понимает непосредственно перед тем, как Unity сериализует их. После того, как Unity запишет данные в ваши поля, вы можете преобразовать сериализированные данные обратно в форму, которую вы хотите, чтобы они имели во время выполнения.
- Когда объект готовится к серийизации, Unity вызывает обратный вызов
OnBeforeSerialize(). В этом обратном вызове вы можете преобразовать ваши данные в то, что понимает Unity. Например, чтобы серийизировать словарь C#, скопируйте данные из словаря в массив ключей и массив значений. - После завершения обратного вызова
OnBeforeSerialize(), Unity сериализирует массивы. - Позднее, когда объект десериализируется, Unity вызывает обратный вызов
OnAfterDeserialize(). В этом обратном вызове вы можете преобразовать данные обратно в форму, удобную для объекта в памяти. Например, используйте массивы ключей и значений для повторного заполнения словаря C#.
Проблемы производительности с сериализации по умолчанию Unity
Если вы хотите иметь деревообразную структуру данных и позволяете Unity сериализовать структуру данных напрямую, отсутствие поддержки ограничения нуля приводит к увеличению потока данных. Это приводит к снижению производительности во многих системах:
using UnityEngine;
using System.Collections.Generic;
using System;
// This example demonstrates why directly serializing a tree
// can lead to performance issues due to Unity's serializer limitations (e.g., no null support, deep copying).
// It's provided to illustrate a problematic pattern, not a recommended one.
public class ProblematicTreeSerialization : MonoBehaviour
{
[Serializable]
public class Node
{
public string interestingValue = "value";
// This field creates a class cycle (Node -> List<Node> -> Node),
// which can make serialization data very large and inefficient for Unity's default serializer.
public List<Node> children = new List<Node>();
}
// This root node will be serialized by Unity.
public Node root = new Node();
}
Чтобы решить эту проблему, вы можете попросить Unity не серийнизировать дерево напрямую и создать отдельное серийнизированное представление дерева, которое понимает Unity. Затем вы можете использовать ISerializationCallbackReceiver интерфейс, чтобы преобразовывать ваше дерево времени выполнения в этот сериализуемый формат и обратно.
using UnityEngine;
using System.Collections.Generic;
using System;
// This example demonstrates how to use ISerializationCallbackReceiver to custom serialize
// a tree structure, avoiding the performance issues of direct serialization shown in ProblematicTreeSerialization.
public class CustomTreeSerialization : MonoBehaviour, ISerializationCallbackReceiver
{
// Node class used at runtime. This is not directly serialized by Unity.
public class Node
{
public string interestingValue = "value";
public List<Node> children = new List<Node>();
}
// Serializable struct that represents a node for Unity's serializer.
[Serializable]
public struct SerializableNode
{
public string interestingValue;
public int childCount;
public int indexOfFirstChild;
}
// The root node used for runtime tree representation. Not directly serialized by Unity.
private Node root = new Node();
// This list is the only data Unity will serialize for the tree.
// It's marked [SerializeField] so Unity's serializer can access it.
[SerializeField] private List<SerializableNode> serializedNodes = new List<SerializableNode>();
// Called just before Unity serializes the object.
public void OnBeforeSerialize()
{
// Ensure root exists for serialization.
if (root == null)
{
root = new Node();
}
serializedNodes.Clear();
AddNodeToSerializedNodes(root);
}
// Recursively adds nodes to the 'serializedNodes' list in depth-first order.
private void AddNodeToSerializedNodes(Node n)
{
var serializedNode = new SerializableNode
{
interestingValue = n.interestingValue,
childCount = n.children.Count,
// The index of the first child will be the current size of 'serializedNodes' + 1
// (since the current node is added next, and then its children).
indexOfFirstChild = serializedNodes.Count + 1
};
serializedNodes.Add(serializedNode);
foreach (var child in n.children)
{
AddNodeToSerializedNodes(child);
}
}
// Called just after Unity deserializes the object.
public void OnAfterDeserialize()
{
// If there's serialized data, reconstruct the runtime tree.
if (serializedNodes != null && serializedNodes.Count > 0)
{
// Start reconstruction from the first serialized node (index 0).
ReadNodeFromSerializedNodes(0, out root);
}
else
{
root = new Node();
}
}
// Recursively reads nodes from 'serializedNodes' and reconstructs the runtime tree.
// Returns the next index to read from.
private int ReadNodeFromSerializedNodes(int index, out Node node)
{
var serializedNode = serializedNodes[index];
// Create a new runtime node and transfer data.
Node newNode = new Node()
{
interestingValue = serializedNode.interestingValue,
children = new List<Node>()
};
// The children were serialized immediately after their parent (depth-first).
// Increment the index and recursively read children.
int currentIndex = index + 1;
for (int i = 0; i < serializedNode.childCount; i++)
{
Node childNode;
currentIndex = ReadNodeFromSerializedNodes(currentIndex, out childNode);
newNode.children.Add(childNode);
}
node = newNode;
return currentIndex;
}
}
Непосредственная серийнизация словаря
Сериализатор по умолчанию Unity не поддерживает Dictionary<TKey, TValue>. Реализуя ISerializationCallbackReceiver, вы можете преобразовать словарь в два экземпляра List (один для ключей, другой для значений) до сериализации, а затем восстановить словарь из этих списков после десериализации.
using UnityEngine;
using System;
using System.Collections.Generic;
public class SerializationCallbackScript : MonoBehaviour, ISerializationCallbackReceiver
{
public List<int> keys = new List<int> { 3, 4, 5 };
public List<string> values = new List<string> { "I", "Love", "Unity" };
// Create a Dictionary. The Unity serializer doesn't support Dictionary types.
public Dictionary<int, string> myDictionary = new Dictionary<int, string>();
public void OnBeforeSerialize()
{
keys.Clear();
values.Clear();
// For each key/value pair in the dictionary, add the key to the keys list and the value to the values list
foreach (var kvp in myDictionary)
{
keys.Add(kvp.Key);
values.Add(kvp.Value);
}
}
public void OnAfterDeserialize()
{
myDictionary = new Dictionary<int, string>();
// Loop through the list of keys and values and add each key/value pair to the dictionary
for (int i = 0; i != Math.Min(keys.Count, values.Count); i++)
myDictionary.Add(keys[i], values[i]);
}
void OnGUI()
{
// This callback displays the following output on three separate labels in the GameView:
// "Key: 3 value: I"
// "Key: 4 value: Love"
// "Key: 5 value: Unity"
foreach (var kvp in myDictionary)
GUILayout.Label("Key: " + kvp.Key + " value: " + kvp.Value);
}
}
Полиморфная десериализация
Стандартный сериализатор Unity лишь ограниченно поддерживает полиморфизм для обычных классов C#. Чтобы сериализовать список объектов, наследующих общий базовый класс, обычно приходится вручную хранить сведения о типе и данные каждого производного типа. В этом примере показано, как это сделать с помощью ISerializationCallbackReceiver.
using UnityEngine;
using System.Collections.Generic;
using System;
// Base class for polymorphic objects. Not directly serializable by Unity.
public abstract class Animal
{
public string Name { get; set; }
public abstract string GetSound();
}
// Derived class
public class Dog : Animal
{
public int BarkVolume { get; set; }
public Dog(string name, int barkVolume)
{
Name = name;
BarkVolume = barkVolume;
}
public override string GetSound() => $"Woof! (Volume: {BarkVolume})";
}
// Derived class
public class Cat : Animal
{
public bool IsCute { get; set; }
public Cat(string name, bool isCute)
{
Name = name;
IsCute = isCute;
}
public override string GetSound() => $"Meow! (Cute: {IsCute})";
}
// Serializable wrapper struct for Dog's data.
// Marked [Serializable] for Unity.
[Serializable]
public struct SerializableDog
{
public string name;
public int barkVolume;
}
// Serializable wrapper struct for Cat's data.
// Marked [Serializable] for Unity.
[Serializable]
public struct SerializableCat
{
public string name;
public bool isCute;
}
// Enum to identify the type of animal for deserialization.
public enum AnimalType
{
Dog,
Cat
}
// Example demonstrating polymorphic deserialization using ISerializationCallbackReceiver.
// This approach requires manual management of type information during serialization.
public class PolymorphicAnimalSerialization : MonoBehaviour, ISerializationCallbackReceiver
{
// The runtime list of polymorphic Animal objects. Not directly serialized by Unity.
public List<Animal> animals = new List<Animal>();
// These lists store the serializable data for each derived type,
// along with a list of types to reconstruct the original order.
// Marked [SerializeField] to be saved by Unity.
[SerializeField] private List<AnimalType> _animalTypes = new List<AnimalType>();
[SerializeField] private List<SerializableDog> _dogs = new List<SerializableDog>();
[SerializeField] private List<SerializableCat> _cats = new List<SerializableCat>();
// Called just before Unity serializes the object.
public void OnBeforeSerialize()
{
_animalTypes.Clear();
_dogs.Clear();
_cats.Clear();
foreach (var animal in animals)
{
if (animal is Dog dog)
{
_animalTypes.Add(AnimalType.Dog);
_dogs.Add(new SerializableDog { name = dog.Name, barkVolume = dog.BarkVolume });
}
else if (animal is Cat cat)
{
_animalTypes.Add(AnimalType.Cat);
_cats.Add(new SerializableCat { name = cat.Name, isCute = cat.IsCute });
}
}
}
// Called just after Unity deserializes the object.
public void OnAfterDeserialize()
{
animals.Clear();
// Keep track of which serialized item to read next from each list.
int dogIndex = 0;
int catIndex = 0;
// Iterate through the type list to reconstruct the animals in the correct order.
foreach (var type in _animalTypes)
{
switch (type)
{
case AnimalType.Dog:
if (dogIndex < _dogs.Count)
{
var sDog = _dogs[dogIndex];
animals.Add(new Dog(sDog.name, sDog.barkVolume));
dogIndex++;
}
else
{
Debug.LogError("Mismatch in serialized Dog data during deserialization.");
}
break;
case AnimalType.Cat:
if (catIndex < _cats.Count)
{
var sCat = _cats[catIndex];
animals.Add(new Cat(sCat.name, sCat.isCute));
catIndex++;
}
else
{
Debug.LogError("Mismatch in serialized Cat data during deserialization.");
}
break;
}
}
}
}