Метод 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 шт):

Автор решения: Yaroslav

Сериализация не занимается сохранением типа объекта! Только данные, это просто пары "имя : значения", тупо 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 для создания нужного объекта. Инспектор тут вообще не нужен, разве что в котором можно прописывать схему из эти пары, без каких либо значений. Но это тоже странно, потому что это только работа программиста, для которого не должно составлять труда описать эти схемы и в коде, а инспектор никак не ускорит или облегчит эту работу.

→ Ссылка
Автор решения: FruitGames537

Для решения этой проблемы мне просто пришлось создать новую структуру 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

→ Ссылка