Сериализация данных в Unity

У меня есть список класса "MenuItem":

[SerializeField] protected List<ChoiceStructures.ButtonItem> menuItems;

от него наследуются еще 3 класса:

public enum ButtonState { common, characteristic, serious, relations, none }
    
    [Serializable]
    public class ButtonItem
    {
        public int index;
        public string optionText;
        public Block targetBlock;
        public ButtonState buttonState;

        public bool premiumButton = false;
        public int premiumCurrency;

        public virtual bool CheckOpportunity()
        {
            if (!premiumButton || premiumCurrency == 0)
                return true;

            return Communicator.TryMakePurchase(premiumCurrency);
        }

        public virtual void DrawGUI(ref List<ButtonItem> menuItems, in int index, Action middleSrc)
        {
            ButtonItem item = this;
            item.index = index;

            EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace();

            EditorGUILayout.LabelField($"Menu {index}");
            if (GUILayout.Button("-", GUILayout.MaxWidth(17)))
            {
                menuItems.Remove(item);
                return;
            }
            EditorGUILayout.EndHorizontal();
            EditorGUILayout.LabelField($"Class: {item.GetType().Name}");
            EditorGUILayout.Space(4);


            item.optionText = EditorGUILayout.TextField("option text", item.optionText);

            middleSrc();

            EditorGUILayout.Space(2);

            item.premiumButton = EditorGUILayout.Toggle("Is premium button", item.premiumButton);
            if (item.premiumButton)
            {
                item.premiumCurrency = EditorGUILayout.IntField("Premium currency", item.premiumCurrency);
            }
            EditorGUILayout.Space(3);
        }
    }

    [Serializable]
    public class ButtonCharacteristic : ButtonItem
    {
        public MainHero.Characteristic characteristic;
        public Operation operation;
        public int value;

        public override bool CheckOpportunity()
        {
            if (characteristic == null)
            {
                Debug.Log("Характеристика пустая!");
                return false;
            }

            switch (operation)
            {
                case Operation.add:
                    characteristic.value += value;
                    break;
                case Operation.remove:
                    characteristic.value -= value;
                    break;
                default:
                    return false;
            }

            return base.CheckOpportunity();
        }

        public override void DrawGUI(ref List<ButtonItem> menuItems, in int i, Action middleSrc)
        {
            base.DrawGUI(ref menuItems, i, middleSrc);

            characteristic = CommandEditorTools.CharacteristicField(characteristic,
                                   new GUIContent("Target Characteristic", "Characteristic for change"),
                                   new GUIContent("<None>"));
            if (characteristic != null)
            {
                operation = (Operation)EditorGUILayout.EnumPopup("Operation", operation);
                value = EditorGUILayout.IntField("Value", value);
            }
        }
    }

    [Serializable]
    public class ButtonSerious : ButtonItem
    {
        public string key;
        public bool WasChosen;

        public override bool CheckOpportunity()
        {
            // Позже доделать 
            return base.CheckOpportunity();
        }

        public override void DrawGUI(ref List<ButtonItem> menuItems, in int i, Action middleSrc)
        {
            base.DrawGUI(ref menuItems, i, middleSrc);

            key = EditorGUILayout.TextField("Key to save", key);
        }
    }

    [Serializable]
    public class ButtonRelation : ButtonItem
    {
        public MinorCharacter character;
        public Operation operation;
        public float value;

        public override bool CheckOpportunity()
        {
            if (character == null)
            {
                Debug.Log("Персонажа нет!");
                return false;
            }

            switch (operation)
            {
                case Operation.add:
                    character.relationPoints += value;
                    break;
                case Operation.remove:
                    character.relationPoints -= value;
                    break;
                default:
                    return false;
            }

            return base.CheckOpportunity();
        }

        public override void DrawGUI(ref List<ButtonItem> menuItems, in int i, Action middleSrc)
        {
            base.DrawGUI(ref menuItems, i, middleSrc);

            character = CommandEditorTools.MinorCharacterField(character,
                       new GUIContent("Character", "Character change sprite"),
                       new GUIContent("<None>"),
                       CharacterCD.GetObjectsList
                       .Where(x => x is MinorCharacter)
                       .Select(x => x as MinorCharacter)
                       .ToList());

            if (character != null)
            {
                operation = (Operation)EditorGUILayout.EnumPopup("Operation", operation);
                value = EditorGUILayout.FloatField("Value", value);
            }
        }
    }

Через инспектор я могу добавить все 4 класса в этот список (то есть родительский класс и классы наследники).

Скрипт кастомного инспектора:

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
 
namespace Fungus.EditorUtils
{
    [CustomEditor(typeof(Choices))]
    public class ChoicesEditor : CommandEditor
    {
        protected SerializedProperty questionTextProp;
        protected SerializedProperty decisionProp;
 
        protected SerializedProperty characterProp;
        protected SerializedProperty dialogWindowProp;
        protected SerializedProperty renderPointProp;
        protected SerializedProperty useCharacterSettingsProp;
 
        protected SerializedProperty menuItemsProp;
 
 
        public override void OnEnable()
        {
            base.OnEnable();
 
            questionTextProp = serializedObject.FindProperty("questionText");
            decisionProp = serializedObject.FindProperty("description");
 
            characterProp = serializedObject.FindProperty("character");
            dialogWindowProp = serializedObject.FindProperty("dialogWindow");
            renderPointProp = serializedObject.FindProperty("renderPoint");
            useCharacterSettingsProp = serializedObject.FindProperty("useCharacterSettings");
 
            menuItemsProp = serializedObject.FindProperty("menuItems");
        }
 
        public override void DrawCommandGUI()
        {
            serializedObject.Update();
 
            Choices choicesCD = target as Choices;
            var flowchart = FlowchartWindow.GetFlowchart();
            if (flowchart == null) return;
            ChoiceStructures.ButtonState buttonState = ChoiceStructures.ButtonState.none;
 
 
 
            EditorGUILayout.PropertyField(questionTextProp);
            EditorGUILayout.PropertyField(decisionProp);
 
            ObjectField<CharacterCD>(characterProp,
                                  new GUIContent("Character", "Character that is speaking"),
                                  new GUIContent("<None>"),
                                  CharacterCD.GetObjectsList);
 
            if (choicesCD.GetCharacter != null)
            {
                EditorGUILayout.PropertyField(useCharacterSettingsProp);
                EditorGUILayout.BeginVertical(); EditorGUILayout.Space(5); EditorGUILayout.EndVertical();
            }
            else useCharacterSettingsProp.boolValue = false;
            bool useCharacterSettings = useCharacterSettingsProp.boolValue;
 
 
            EditorGUILayout.Space(5);
            if (useCharacterSettings)
            {
                choicesCD.DialogWindow = choicesCD.GetCharacter.GetDialogWindow;
            }
            else
            {
                EditorGUILayout.PropertyField(dialogWindowProp);
                if (choicesCD.GetCharacter != null)
                {
                    ObjectField<CharacterRenderPoint>(renderPointProp,
                               new GUIContent("PointsToRenderCharacter"),
                               new GUIContent("<None>"),
                               CharacterRenderPoint.GetObjectsList);
                }
            }
 
            EditorGUILayout.Space(20);
 
            EditorGUILayout.BeginHorizontal();
            EditorGUILayout.LabelField("Add new menu item");
            EditorGUILayout.Space(2);
            buttonState = (ChoiceStructures.ButtonState)EditorGUILayout.EnumPopup(buttonState);
            List<ChoiceStructures.ButtonItem> menuItems = choicesCD.GetMenuItems;
 
            switch (buttonState)
            {
                case ChoiceStructures.ButtonState.common:
                    ChoiceStructures.ButtonItem buttonItem = new ChoiceStructures.ButtonItem
                    {
                        buttonState = buttonState
                    };
                    menuItems.Add(buttonItem);
                    break;
                case ChoiceStructures.ButtonState.characteristic:
                    ChoiceStructures.ButtonItem buttonCharacteristic = new ChoiceStructures.ButtonCharacteristic
                    {
                        buttonState = buttonState
                    };
                    menuItems.Add(buttonCharacteristic);
                    break;
                case ChoiceStructures.ButtonState.serious:
                    ChoiceStructures.ButtonItem buttonSerious = new ChoiceStructures.ButtonSerious
                    {
                        buttonState = buttonState
                    };
                    menuItems.Add(buttonSerious);
                    break;
                case ChoiceStructures.ButtonState.relations:
                    ChoiceStructures.ButtonItem buttonRelation = new ChoiceStructures.ButtonRelation
                    {
                        buttonState = buttonState
                    };
                    menuItems.Add(buttonRelation);
                    break;
                case ChoiceStructures.ButtonState.none:
                    break;
                default:
                    break;
            }
            EditorGUILayout.EndHorizontal();
            
            int index = 0;
            foreach (var item in menuItems)
            {
                index++; EditorGUILayout.Space(10);
 
                item.DrawGUI(ref menuItems, index, delegate 
                {
                    EditorGUILayout.BeginHorizontal();
                    item.targetBlock = CommandEditorTools.BlockField(item.targetBlock,
                                       new GUIContent("Target Block", "Block to call when option is selected"),
                                       new GUIContent("<None>"),
                                       flowchart);
                    const int popupWidth = 17;
                    if (item.targetBlock == null && GUILayout.Button("+", GUILayout.MaxWidth(popupWidth)))
                    {
                        var fw = EditorWindow.GetWindow<FlowchartWindow>();
                        var t = (Choices)target;
                        var activeFlowchart = t.GetFlowchart();
                        var newBlock = fw.CreateBlockSuppressSelect(activeFlowchart, t.ParentBlock._NodeRect.position - Vector2.down * 60);
                        item.targetBlock = newBlock;
                        activeFlowchart.SelectedBlock = t.ParentBlock;
                    }
                    EditorGUILayout.EndHorizontal();
                });
            }
 
            serializedObject.ApplyModifiedProperties();
        }
    }
}

И все это работает пока я нахожусь в эдиторе:

но если я запущу сцену, или выйду из unity, то все наследуемые классы от MenuItem в этом списке превратятся в этот же класс

Почему я думаю что проблемы в сериализации:

Я уже прочел много информации об этой проблеме, и решил проверить как unity сохраняет этот список в файле сцены (.unit)

и понял, что юнити просто не сохраняет классы наследники как надо, он их сохраняет как родительский класс.

Я пробовал добавлять ко всем классам атрибут [Serializable], но это не помогло

Я знаю множество решений этой проблемы, но они убивают всю красоту ООП. Так что я снова уточню, что мне нужно решить проблему с сериализацией unity, при этом оставив концепцию этих 4 классов.


Ответы (0 шт):