Всем добрый день. Эта про относительно новую фичу 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.
Плюсы
Сериализация абстракций
Это отвязка от монобеха
Отказ от изменения реализации поведения через интерфейс и множество реализаций через компоненты
Это хранение данных в одном листе, вместо постоянного раздувания количества листов ради новых данных
Никаких кастомных решений для редактора. Отображение экземпляров происходит за счёт стандартных решений юнити.
Минусы
До 2021 версии юнити ломается сериализация при переименовании классов, т.к. они в явном виде сериализовывались.
<Сборка в которой находится класс>, <Название класса с его неймспейсом>
Юнитехи не предоставили удобного инструментария для работы в редакторе из под капота (Будет описано ниже)
Как с этим работать
Мы теперь знаем что наше любимое юнити умеет сериализовывать абстрактные поля, но как заставить всё это удобно работать?
public class NotExampleMonobehaviour : MonoBehaviour
{
// Данная сериализация работает, но если написать это без всяких махинаций
// То редактор вам выведет только название этого поля
[SerializeReference] private object _notYetWorkedSerialization;
}
В чем отличее данного кода от того, что был в начале? - Инициализация
Суть в том, что редактор сериализует в данном случае не только поле, но и вложенный в него экземпляр, но если в него ничего не было вложено, то собственно ему и нечего сериализовывать и отображать вам.
Но как вложить экземпляр в данное поле? Дефолтными способами из коробки только через дефолтную инициализацию. Юнитеки не предоставили удобного инструментария для работы с данными полями. Единственное что вам дали, это 3 поля у данных типа SerializeProperty:
property.managedReferenceValue - сеттер для вашего экземпляра (Геттера нет)
property.managedReferenceFieldTypename - имя типа поля (из названия думаю очевидно :) )
property.managedReferenceFullTypename - имя типа экземпляра, который лежит в данном сериазуемом поде. Возвращает null, если ничего не лежит.
property.propertyType - в данном случае равна SerializedPropertyType.ManagedReference
Для отрисовки данных экзмепляров юнити использует стандартную механиху 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.
Примеры
Статья от Pixonic - в данном случае они использовали эту фичу для реактивного связывания.
Я также для себя пытался реализовывать подобную фичу, но без некоторых элементов. Ничего сложного в этом нет. Код как это легко реализовать есть ниже:
Код
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;
}
}