Метод FindPropertyRelative не ищет сериализованое поле с типом object
У меня есть класс Container<TKey, TValue>
который представляет из себя объект с ключом TKey
и значением которое может быть либо TValue
либо списком таких-же контейнеров:
using System.Collections.Generic;
using System;
using UnityEngine;
namespace Adapter.Containers
{
[Serializable]
public class Container<TKey, TValue>
{
public Container() : this(default) { }
public Container(TKey key) : this(key, default) { }
public Container(TKey key, TValue value)
{
m_Key = key;
m_Value = value;
}
[SerializeField] private TKey m_Key;
[SerializeField] private object m_Value;
public object this[TKey name]
{
get
{
if (GetContainer(name) is Container<TKey, TValue> container)
return container;
else if (GetStore(name) is TValue elementary)
return elementary;
throw new ArgumentException("Cannot get value because store unknown type");
}
set
{
if (value is Container<TKey, TValue> container)
SetContainer(name, container);
else if (value is TValue elementary)
SetStore(name, elementary);
throw new ArgumentException("Cannot set value because this type not support");
}
}
public TKey name { get => m_Key; set => m_Key = value; }
public List<Container<TKey, TValue>> container
{
get
{
if (m_Value is List<Container<TKey, TValue>>)
return m_Value as List<Container<TKey, TValue>>;
return null;
}
set
{
if (value is not null)
m_Value = value;
throw new ArgumentException("Cannot set not container value by container property");
}
}
public TValue store
{
get
{
if (m_Value is TValue)
return (TValue)m_Value;
return default;
}
set
{
if (value is not null)
m_Value = value;
throw new ArgumentException("Cannot set unacceptable type value by store property");
}
}
public ContainerType type => m_Value is List<Container<TKey, TValue>> ? ContainerType.Container : m_Value is TValue ? ContainerType.Store : ContainerType.Unknown;
public Container<TKey, TValue> GetContainer(TKey name) => container[container.FindIndex(item => item.name.Equals(name))]; // Получение по индексу просто для эстетичности
public void SetContainer(TKey name, Container<TKey, TValue> value) => container[container.FindIndex(item => item.name.Equals(name))] = value;
public TValue GetStore(TKey name) => container.Find(item => item.name.Equals(name)).store;
public void SetStore(TKey name, TValue value) => container.Find(item => item.name.Equals(name)).store = value;
}
}
И также у меня есть класс ContainerPropertyDrawer
для отрисовки этого класса:
using UnityEditor;
using UnityEngine;
namespace Adapter.Containers.Editor
{
[CustomPropertyDrawer(typeof(Container<,>))]
public class ContainerPropertyDrawer : PropertyDrawer
{
private ContainerType m_ContainerType = ContainerType.Unknown;
public ContainerType containerType { get => m_ContainerType; set => m_ContainerType = value; }
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
SerializedProperty keyProperty = property.FindPropertyRelative("m_Key");
SerializedProperty valueProperty = property.FindPropertyRelative("m_Value");
EditorGUI.LabelField(position, label);
EditorGUILayout.PropertyField(keyProperty);
m_ContainerType = (ContainerType)EditorGUILayout.EnumPopup(m_ContainerType);
if (m_ContainerType != ContainerType.Unknown)
if (m_ContainerType == ContainerType.Container)
EditorGUILayout.PropertyField(valueProperty, true);
else
EditorGUILayout.ObjectField(valueProperty.objectReferenceValue, typeof(object), true);
}
}
}
Проблема у меня в том что во первых m_ContainerType
в одном и том же списке изменяется для всех элементов (не особо важно) и во вторых у меня не отображается поле значения (вылезает ошибка SerializedProperty is null
при попытке отрисовать поле m_Value
я так понял unity не хочет нормально получать поля с типом object
или не?)
Ответы (2 шт):
Сериализация не занимается сохранением типа объекта! Только данные, это просто пары "имя : значения", тупо json
.
Пример:
[Serializable]
public class Animal
{
public string Name;
public Animal (string name)
Name => name;
}
[Serializable]
public class Cat : Animal
{
public string Breed;
public Cat (string name, string breed) : base(name)
Breed=> breed;
}
[Serializable]
public class Test
{
public Animal MyAnimal;
public Cat MyCat;
}
var t = new Test()
{
MyAnimal = new Cat("Jafar", "Persian"),
MyCat = new Cat("Willem", "Siamese")
};
Debug.Log(JsonUtility.ToJson(t));
Результат:
{
"MyAnimal": {
"Name": "Jafar"
},
"MyCat": {
"Name": "Willem",
"Breed": "Siamese"
}
}
Как видишь, несмотря на то, что в качестве MyAnimal
мы указали объект типа Cat
, информации о поле Breed
отсутствует, потому что типом является Animal
!
При десиаризации, объекты создаются заново согласно типам полей. Но System.Object
вообще не сериализуем, не только потому что он не [Serializable]
, но и потому, что в нем нет никаких данных, только методы
ToString
, GetHashCode
, GetType
и Equals
.
Если бы Animal
и Test
был MonoBehaviour
или ScriptableObject
, то есть UnityEngine.Object
, то в инспекторе мы можем указать любого его наследника. Но в отличие от приведённого примера, этот Animal
не создавался бы заново в контексте Test
, он создавался бы независимо от него, а в самом Test
в виде значения был бы указатель, некий id
на UnityEngine.Object
.
Этот процесс можно наблюдать в процессе разработки. Иногда мы переименовываем компоненты и в инспекторе пропадает ссылка на скрипт, но не пропадают данные, из чего можно понять, что в инспекторе мы имеем дело вовсе не с классом, а с моделью данных построенной по классу, с чем и работает SerializedProperty
. Можно выбрать не только скрипт с новым именем, но и вообще любой, при этом он будет обладать всеми значениями тех полей, чьё имя совпало.
SerializedProperty.objectReferenceValue
содержит только UnityEngine.Object
, это можно увидеть, наведя курсор на свойство, нет никакого смысла кастить его до базового System.Object
. ObjectField
с ним естественно тоже не работает, в редакторе Unity
все ссылочные типы могут быть только от UnityEngine.Object
, потому что Unity
занимается хранением и жизненным циклом только его. Не понятно что делать с этим не понятным typeof(object)
, чьих будет и с чем его есть.
Сериализовать структуру из Container<TKey, TValue>
не получится, ты должен иметь типизированную модель для дисериализации на неё данных. То что ты пишешь, уже существует и это json
. В AssetStore
есть JSON Object
, где можно оперировать значениями и есть методы, помогающие определять их тип, вроде IsFloat
, но нет инспектора, потому что... а зачем он вообще нужен? это просто текст, можно и руками напечатать.
В рамках некого универсального адаптера, проще работать с парами путей, типа "vector.direction"
> "Rotation"
и по заданной схеме пересобирать json
для создания нужного объекта. Инспектор тут вообще не нужен, разве что в котором можно прописывать схему из эти пары, без каких либо значений. Но это тоже странно, потому что это только работа программиста, для которого не должно составлять труда описать эти схемы и в коде, а инспектор никак не ускорит или облегчит эту работу.
Для решения этой проблемы мне просто пришлось создать новую структуру Variation<TKey, TValue>
(вот почему в c# нету такого же класса как в c++? (std::variant
)) и заменить тип object
на тип Variation<TValue, List<Container<TKey, TValue>>
, в итоге классы изменились так:
Container.cs:
using System.Collections.Generic;
using System;
using UnityEngine;
namespace Adapter.Containers
{
[Serializable]
public class Container<TKey, TValue>
{
public Container() : this(default) { }
public Container(TKey key) : this(key, default) { }
public Container(TKey key, TValue value)
{
m_Key = key;
m_Value = value;
}
[SerializeField] private TKey m_Key;
[SerializeField] private Variation<TValue, List<Container<TKey, TValue>>> m_Value;
public Variation<TValue, Container<TKey, TValue>> this[TKey name]
{
get
{
if (GetContainer(name) is Container<TKey, TValue> container)
return container;
else if (GetStore(name) is TValue elementary)
return elementary;
throw new ArgumentException("Cannot get value because store unknown type");
}
set
{
if (value.value is Container<TKey, TValue> container)
SetContainer(name, container);
else if (value.value is TValue elementary)
SetStore(name, elementary);
throw new ArgumentException("Cannot set value because this type not support");
}
}
public TKey name { get => m_Key; set => m_Key = value; }
public List<Container<TKey, TValue>> container
{
get
{
if (m_Value.value is List<Container<TKey, TValue>>)
return m_Value.value as List<Container<TKey, TValue>>;
return null;
}
set
{
if (value is not null)
m_Value.value = value;
throw new ArgumentException("Cannot set not container value by container property");
}
}
public TValue store
{
get
{
if (m_Value.value is TValue)
return (TValue)m_Value.value;
return default;
}
set
{
if (value is not null)
m_Value.value = value;
throw new ArgumentException("Cannot set unacceptable type value by store property");
}
}
public ContainerType type => m_Value.value is List<Container<TKey, TValue>> ? ContainerType.Container : m_Value.value is TValue ? ContainerType.Store : ContainerType.Unknown;
public Container<TKey, TValue> GetContainer(TKey name) => container[container.FindIndex(item => item.name.Equals(name))];
public void SetContainer(TKey name, Container<TKey, TValue> value) => container[container.FindIndex(item => item.name.Equals(name))] = value;
public TValue GetStore(TKey name) => container.Find(item => item.name.Equals(name)).store;
public void SetStore(TKey name, TValue value) => container.Find(item => item.name.Equals(name)).store = value;
}
}
Variation.cs:
using System;
using UnityEngine;
namespace Adapter.Containers
{
[Serializable]
public struct Variation<T1, T2>
{
private Variation(T1 oneValue, T2 twoValue, Type storeType, VariationType type)
{
if (typeof(T1) == typeof(T2))
throw new ArgumentException("Cannot create variation because type one and type two not different");
m_OneValue = oneValue;
m_TwoValue = twoValue;
m_StoreType = storeType;
m_Type = type;
}
public Variation(T1 value) : this(value, default, typeof(T1), VariationType.One) { }
public Variation(T2 value) : this(default, value, typeof(T2), VariationType.Two) { }
[SerializeField] private T1 m_OneValue;
[SerializeField] private T2 m_TwoValue;
[SerializeField] private Type m_StoreType;
[SerializeField] private VariationType m_Type;
public T1 oneValue
{
get
{
if (m_Type is VariationType.One)
return m_OneValue;
return default;
}
set
{
m_OneValue = value;
m_TwoValue = default;
m_StoreType = typeof(T1);
m_Type = VariationType.One;
}
}
public T2 twoValue
{
get
{
if (m_Type is VariationType.Two)
return m_TwoValue;
return default;
}
set
{
m_OneValue = default;
m_TwoValue = value;
m_StoreType = typeof(T2);
m_Type = VariationType.Two;
}
}
public Type storeType => m_StoreType;
public VariationType type => m_Type;
public object value
{
get
{
return m_StoreType is T1 ? m_OneValue : m_TwoValue;
}
set
{
if (value is T1)
oneValue = (T1)value;
else if (value is T2)
twoValue = (T2)value;
else
throw new InvalidOperationException("Cannot set value because value type is unacceptable variation type");
}
}
public static implicit operator Variation<T1, T2>(T1 value) => new Variation<T1, T2>(value);
public static implicit operator Variation<T1, T2>(T2 value) => new Variation<T1, T2>(value);
public static implicit operator T1(Variation<T1, T2> value) => value.oneValue;
public static implicit operator T2(Variation<T1, T2> value) => value.twoValue;
}
}
ContainerPropertyDrawer.cs:
using UnityEditor;
using UnityEngine;
namespace Adapter.Containers.Editor
{
[CustomPropertyDrawer(typeof(Container<,>))]
public class ContainerPropertyDrawer : PropertyDrawer
{
private float singleLine => EditorGUIUtility.singleLineHeight;
private float spacing => EditorGUIUtility.standardVerticalSpacing;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
EditorGUI.LabelField(new Rect(position.x, position.y, position.width, singleLine), label);
if (property.FindPropertyRelative("m_Key") is SerializedProperty keyProperty)
EditorGUI.PropertyField(new Rect(position.x, position.y += singleLine + spacing, position.width, EditorGUI.GetPropertyHeight(keyProperty)), keyProperty);
if (property.FindPropertyRelative("m_Value") is SerializedProperty valueProperty)
EditorGUI.PropertyField(new Rect(position.x, position.y += singleLine + spacing, position.width, EditorGUI.GetPropertyHeight(valueProperty)), valueProperty);
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float height = singleLine + spacing;
if (property.FindPropertyRelative("m_Key") is SerializedProperty keyProperty)
height += EditorGUI.GetPropertyHeight(keyProperty);
if (property.FindPropertyRelative("m_Value") is SerializedProperty valueProperty)
height += EditorGUI.GetPropertyHeight(valueProperty);
if (property.FindPropertyRelative("m_Key") is not null && property.FindPropertyRelative("m_Value") is not null)
height += spacing;
return height;
}
}
}
VariationPropertyDrawer.cs:
using UnityEditor;
using UnityEngine;
namespace Adapter.Containers.Editor
{
[CustomPropertyDrawer(typeof(Variation<,>))]
public class VariationPropertyDrawer : PropertyDrawer
{
private float singleLine => EditorGUIUtility.singleLineHeight;
private float spacing => EditorGUIUtility.standardVerticalSpacing;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
EditorGUI.PropertyField(new Rect(position.x, position.y, position.width, singleLine), property.FindPropertyRelative("m_Type"));
VariationType type = (VariationType)property.FindPropertyRelative("m_Type").intValue;
if (type is VariationType.One && property.FindPropertyRelative("m_OneValue") is SerializedProperty oneValueProperty)
EditorGUI.PropertyField(new Rect(position.x, position.y += singleLine + spacing, position.width, EditorGUI.GetPropertyHeight(oneValueProperty)), oneValueProperty, new GUIContent("Value"));
else if (type is VariationType.Two && property.FindPropertyRelative("m_TwoValue") is SerializedProperty twoValueProperty)
EditorGUI.PropertyField(new Rect(position.x, position.y += singleLine + spacing, position.width, EditorGUI.GetPropertyHeight(twoValueProperty)), twoValueProperty, new GUIContent("Value"));
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float height = singleLine + spacing;
VariationType type = (VariationType)property.FindPropertyRelative("m_Type").intValue;
if (type is VariationType.One && property.FindPropertyRelative("m_OneValue") is SerializedProperty oneValueProperty)
height += EditorGUI.GetPropertyHeight(oneValueProperty);
else if (type is VariationType.Two && property.FindPropertyRelative("m_TwoValue") is SerializedProperty twoValueProperty)
height += EditorGUI.GetPropertyHeight(twoValueProperty);
return height;
}
}
}
Кстати этот проект есть на github