Unity 6.3
0 онлайн 17 гостей 3 в системе
Вход

Пользовательская сериализация

Когда вы хотите серийизировать что-то, что не поддерживается серийизатором Unity (например, словарь C#), вы можете реализовать ISerializationCallbackReceiver интерфейс в своём классе. Так вы сможете реализовать обратные вызовы, которые Unity вызывает в ключевые моменты сериализации и десериализации.

Вы можете использовать обратные вызова сериализации, чтобы дать вашим трудносериализуемым данным другое представление во время выполнения, чем во время сериализации. Вы можете преобразовать ваши данные в то, что Unity понимает непосредственно перед тем, как Unity сериализует их. После того, как Unity запишет данные в ваши поля, вы можете преобразовать сериализированные данные обратно в форму, которую вы хотите, чтобы они имели во время выполнения.

  1. Когда объект готовится к серийизации, Unity вызывает обратный вызов OnBeforeSerialize(). В этом обратном вызове вы можете преобразовать ваши данные в то, что понимает Unity. Например, чтобы серийизировать словарь C#, скопируйте данные из словаря в массив ключей и массив значений.
  2. После завершения обратного вызова OnBeforeSerialize(), Unity сериализирует массивы.
  3. Позднее, когда объект десериализируется, 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;
            }
        }
    }
}

Дополнительные ресурсы