Всем добрый день. Эта статья про относительно новую фичу Serialize Reference. Появилась она в 2019 версии.

Оглавление:

  • Что это такое и зачем оно нужно

  • Плюсы и минусы

  • Как работать с этим

  • Примеры

Serialize Reference - это атрибут, который позволяет Unity сериализовывать данные в новом формате. Основной хак этой сериализации, это то, что он позволяет сериализовывать данные в абстракциях. Пример:

public class ExampleFirstMonoBehaviour : MonoBehaviour
{
			[SerializeReference] private object _customObject = new ExampleFirst();
}

public class ExampleSecondMonoBehaviour : MonoBehaviour
{
			[SerializeReference] private object _customObject = new ExampleSecond();
}
  

public class ExampleFirst
{
  		[SerializeField][Range(0, 5)] private float _floatValue;
}

public class ExampleSecond
{
  		[SerializeField] private string _stringValue = "Hello";
}

В данных примерах под типом object скрывается сериализация ExampleFirst и ExampleSecond.

Плюсы

  • Сериализация абстракций

    • Это отвязка от монобеха

    • Отказ от изменения реализации поведения через интерфейс и множество реализаций через компоненты

    • Это хранение данных в одном листе, вместо постоянного раздувания количества листов ради новых данных

  • Никаких кастомных решений для редактора. Отображение экземпляров происходит за счёт стандартных решений юнити.

Минусы

  • При изменении имени и неймспейса, ломается сериализация. Для того чтобы починить сериализацию после изменения имении и неймспейса, требуется использовать movefromattribute в котором будет указано, как раньше назывался данный тип, и где находился

<Сборка в которой находится класс>, <Название класса с его неймспейсом>

  • Юнитехи не предоставили удобного инструментария для работы в редакторе из под капота (Будет описано ниже)

Как с этим работать

Мы теперь знаем что наше любимое юнити умеет сериализовывать абстрактные поля, но как заставить всё это удобно работать?

public class NotExampleMonobehaviour : MonoBehaviour
{
			// Данная сериализация работает, но если написать это без всяких махинаций
      // То редактор вам выведет только название этого поля
			[SerializeReference] private object _notYetWorkedSerialization;
}

В чем отличее данного кода от того, что был в начале? - Инициализация

Суть в том, что редактор сериализует экземпляр, а не поле. И если в него ничего не было вложено, то собственно ему и нечего сериализовывать и отображать вам. В случае обычного SerializeField, редактор автоматически инициализирует ваше поле, даже если вы не укажете это в коде

Но как вложить экземпляр в данное поле? Дефолтными способами из коробки только через дефолтную инициализацию. Юнитеки не предоставили удобного инструментария для работы с данными полями. Единственное что вам дали, это 3 поля у данных типа SerializeProperty:

Для отрисовки данных экзмепляров юнити использует стандартную механиху PropertyDrawer. Т.е. если вам нужно будет заоверрайдить отрисовку, либо сделать декорацию с помощью PropertyAttribute, то это всё также работает в SerializeReference.

Для инициализации данных полей можно писать кастомный инстурментарий используя поля SerializeProperty описанные выше, либо использовать данный [ассет](https://github.com/elmortem/serializereferenceeditor)

Самое важное ещё понимать с чем это сериализация может работать, а с чем нет. Данная шпаргалка есть в доке [юнитехов](https://docs.unity3d.com/ScriptReference/SerializeReference.html)

  • Тип поля не должен наследоваться от UnityEngine.Object

  • Тип поля может быть абстрактным классом/интерфейсом

  • Применение аттрибута SerializeReference к листу/массиву приводит его применение ко всем его элементам. Т.е. List<object> работает.

  • Референсы данных полей не могут быть у разных UnityEngine.Object. Т.е. если вы присваивали в редакторе один и тот же экземпляр, то после десериализации это будут разные экземпляры но с одним и тем же наполнением.

  • Тип данных который пытаетесь сериализовать, должен быть почемен атрибутом [Serializable]

  • Значение поля не может быть кастомным дженерик типом. Т.е. если у вас есть тип данных Foo<T>, то при попытке записать в любое поле подобного типа, данные типа Foo<int> или что то подобное, вылезет ошибка в редакторе. Если вам нужно записать Foo<int>, создайте наследника IntFoo.

Примеры

  1. Статья от Pixonic - в данном случае они использовали эту фичу для реактивного связывания.

  2. Я также для себя пытался реализовывать подобную фичу, но без некоторых элементов. Ничего сложного в этом нет. Код как это легко реализовать есть ниже:

Код
public class ViewModelEditor : Editor
    {
        private static readonly Type[] _types;
        private static readonly GUIContent[] _keys;
        private ReorderableList _reorderableList;
        private SerializedProperty _propertyList;

        static ViewModelEditor()
        {
            _types = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(assembly => assembly.GetTypes())
                .Where(t => t.IsSubclassOfGenericTypeDefinition(typeof(ReactiveProperty<>)) && t != typeof(ReactiveProperty<>))
                .ToArray();
            _keys = _types.Select(t => new GUIContent(t.Name)).ToArray();
        }
        
        public override void OnInspectorGUI()
        {
            if (_reorderableList == null)
            {
                _propertyList = serializedObject.FindProperty("_propertiesList");
                _reorderableList = new ReorderableList(serializedObject, _propertyList, true, true, true, true);
                _reorderableList.onAddDropdownCallback = ONAddCallbackHandler;
                _reorderableList.elementHeightCallback = ElementHeightCallback;
                _reorderableList.drawElementCallback = DrawElementCallback;
            }
            
            _reorderableList.DoLayoutList();
        }

        private void DrawElementCallback(Rect rect, int index, bool isactive, bool isfocused)
        {
            var prop = _propertyList.GetArrayElementAtIndex(index);
            EditorGUI.PropertyField(new Rect(rect.x + 40f, rect.y, rect.width - 40f, rect.height), prop, prop.isExpanded);
            serializedObject.ApplyModifiedProperties();
        }

        private float ElementHeightCallback(int index)
        {
            var prop = _propertyList.GetArrayElementAtIndex(index);
            return EditorGUI.GetPropertyHeight(prop);
        }

        private void ONAddCallbackHandler(Rect rect, ReorderableList list)
        {
            EditorUtility.DisplayCustomMenu(rect, _keys, -1, CallbackHandler, null);
        }

        private void CallbackHandler(object userdata, string[] options, int selected)
        {
            var count = _propertyList.arraySize;
            _propertyList.InsertArrayElementAtIndex(count);
            _propertyList.GetArrayElementAtIndex(count).managedReferenceValue =
                new global::ViewModel.Pair()
                    {key = $"Element {count}", property = Activator.CreateInstance(_types[selected])};
            serializedObject.ApplyModifiedProperties();
        }
    }
    
    public static class TypeExtensions
    {
        public static bool IsSubclassDeep(this Type type, Type parenType)
        {
            while (type != null)
            {
                if (type.IsSubclassOf(parenType))
                    return true;
                type = type.BaseType;
            }

            return false;
        }

        public static bool TryGetGenericTypeOfDefinition(this Type type, Type genericTypeDefinition,
            out Type generictype)
        {
            generictype = null;
            while (type != null)
            {
                if (type.IsGenericType && type.GetGenericTypeDefinition() == genericTypeDefinition)
                {
                    generictype = type;
                    return true;
                }
                type = type.BaseType;
            }

            return false;
        }
        
        public static bool IsSubclassOfGenericTypeDefinition(this Type t, Type genericTypeDefinition)
        {
            if (!genericTypeDefinition.IsGenericTypeDefinition)
            {
                throw new Exception("genericTypeDefinition parameter isn't generic type definition");
            }
            if (t.IsGenericType && t.GetGenericTypeDefinition() == genericTypeDefinition)
            {
                return true;
            }
            else
            {
                t = t.BaseType;
                while (t !=null)
                {
                    if (t.IsGenericType && t.GetGenericTypeDefinition() == genericTypeDefinition)
                        return true;

                    t = t.BaseType;
                }
            }

            return false;
        }
    }

Комментарии (0)