Всем добрый день. Эта статья про относительно новую фичу 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:
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;
}
}