Сегодня я хотел бы камень за камушком разобрать механизм сериализации в Unity и показать вам с чем его едят, а с чем есть категорически не стоит.
Что такое сериализация?
Сериализация - это по сути любой процесс "обертки данных". Например, тип Vector3 может быть сохранен как три числа x, y, z и затем восстановлен из этих чисел обратно в Vector3. В грубом виде - это и есть та самая сериализация.
В Unity, когда ваш код комплируется после внесенных изменений (в народе это называют ребилдом), либо когда вы входите/выходите из игрового режима - данные, введенные ранее, остаются, но вместе с тем добавляются новые поля. Фактически, происходит программирование "налету" и этому Unity обязан своей сериализации. Но естественно, у такой системы есть свои тонкости, о которых здесь и сейчас я вам расскажу.
Сериализация классов
Если вы пишете проект, пользуясь лишь стандартными средствами Unity - вы можете даже не заметить наличия сериализации. Все это происходит потому, что стандартные классы библиотеки Unity в большинстве своем уже сериализованы.
Однако, не будем забывать, что основная фишка здесь - это ООП, а вместе с ней - возможность создавать собственные классы, не только наследники MonoBehaviour.
И вот мы решили создать собственный класс. В качестве моего примера - хранящий информацию о предмете:
public class ItemInfo { public string name; public int price; }
Мы добавили его экземпляр в наш компонент, но на странность он не отображается в инспекторе, а особо наблюдательный заметит - он сбрасывается при каждом ребилде.
Это происходит по причине того, что мы еще не сериализовали наш класс. Однако тут нас поджидает коварный выбор - мы можем сделать это несколькими разными путями, каждый из которых имеет свои плюсы и минусы.
Первый способ заключается в том, чтобы добавить к нашему классу атрибут [System.Serializable]. Кратко описать функционал этого способа можно как "я сохраню значения полей".
[System.Serializable] public class ItemInfo ...
Второй - пронаследовать наш класс от UnityEngine.ScriptableObject. Краткое описание этого способа "я сохраню ссылку".
public class ItemInfo : ScriptableObject ...
В чем же их отличие?
Предположим, что мы наследуем несколько классов от уже созданного нами ItemInfo
public class PotionInfo : ItemInfo {} public class WeaponInfo : ItemInfo {}
В таком случае, чтобы хранить список совместно, мы будем использовать конструкцию, подобную такой:
public List<ItemInfo> infoList;
- В случае с атрибутом - сериализация сотрет из класса всю информацию выше ItemInfo, единственный способ хранить информацию, это задавать явные списки для PotionInfo и WeaponInfo
- В случае с наследованием - сериализация полноценно сохранит данные в нашем списке.
Данный пример хорошо иллюстрирует то, как сериализация обрабатывает обычные классы и объекты, наследуемые от Object. После того как вы ознакомились с примером, подчеркивающим явное различие в поведении - есть смысл рассказать вам о плюсах и минусах каждого способа и поговорить о каждом из них отдельно.
Сериализация через наследование от ScriptableObject ===
Плюсы:
* Экземпляры сохраняют ссылки после ребилда
* Отлично подходят для замкнутых и рекурсивных систем
Минусы:
* Нельзя создавать стандартным конструктором, нужно использовать ScriptableObject.CreateInstance<T>()
* Не удаляются автоматически, если скажем мы приравняли поле к null - нужно удалять вручную.
* Нельзя создавать Generic-типы напрямую
Если бы не ручное управление экземплярами, то есть постоянный контроль на тему их уничтожения - наверное данный способ был бы идеальным. Кстати, удалить инстанс можно одной из двух комманд:
Object.Destroy(script); //Удалить в игре Object.DestroyImmediate(script); //Удалить в редакторе
Однако, если вы все-таки что-то упускаете вы можете воспользоваться командой Resources.UnloadUnusedAssets(). Она довольно-таки затратная при большом количестве разных asset'ов, потому не советую применять ее слишком часто.
Учтите, что обеспечение ссылочности и наследование таким способом делает класс немного тяжелее.
С Generic-типами история простая - движок не считает их за конечные скрипты и не добавляет в общий список.
Самый легкий способ обойти ограничение Generic это пронаследовать от него новый пустой класс:
public class ValueBool : Value<bool> {}
Сериализация через атрибут [Serializable] ===
Плюсы:
* Удаление экземпляров контролируется средой, вы не обязаны удалять их вручную
* Легковесность - отлично подходит для классов, которые просто хранят в себе информацию
Минусы:
* Ссылочность часто стирается при ребилде
* Та ссылочность, которая остается реализована весьма костыльно
* Generic-типы не сериализуются
Для того, чтобы понять почему происходит потеря ссылок, нам нужно понять сам алгоритм сериализации.
Для обычных классов он такой:
- Получено сообщение о перезагрузке сборки
- Все поля реализующие обычные классы сохраняются вне среды
- Сборка перезагружается
- Создаются *новые* экземпляры из сохраненной информации.
- Поля заполняются из сохраненной информации
У данного способа есть ряд грубых багов, которые могут вызывать краш Unity.
Взгляните на следующую конструкцию:
[Serializable] public class TestScript { public TestScript brother; }
Данный код, несмотря на всю его кажущуюся безобидность способен заставить разработчика часами убиваться в дебаге. Чтобы получить тот "несбыточный вариант", достаточно написать код подобно следующему:
var a = new TestScript(); var b = new TestScript(); a.brother = b; b.brother = a;
То есть вывести в классах ссылки друг на друга. Поведение в таких случаях бывает разным - в старых версиях движка это приводит к крашу и бесконечному циклу, в новых версиях это "закупорили", сделав подобные поля не сериализуемыми.
Почему же так происходило, и почему данная проблема пока не была решена иным путем? Механизм сериализации просто начинает "прыгать" между разными экземплярами.
Например так:
- Захожу в экземпляр a
- Среди полей нахожу brother
- Захожу в экземпляр b
- Среди полей нахожу brother
- И снова по кругу от 1 до 4.
Далее - generic-типы. Здесь мы конечно спокойно создадим экземпляр, однако он не будет сериализовываться, а значит не пригоден для хранения информации.
Исключением из этого правила является тип List<T>.
Сериализация полей
Я намеренно начал говорить про сериализацию классов, несмотря на то, что объяснение сериализации полей выглядит более простым. Но теперь, ознакомившись с функционалом сериализации классов можно сделать некоторые уточнения.
В проекте по умолчанию сериализуются только поля с доступом public. Если поле сериализуется - оно появится в стандартном инспекторе и его свойства можно будет настраивать.
public int count = 0; //Это поле сериализуется и будет продолжать хранить значение после ребилда или перезапуска
Бывают случаи, при которых поле должно быть сериализовано, но не должно отмечаться в инспекторе. Для этих целей применяют атрибут [HideInInspector]:
[HideInInspector] public int count = 0; //Это поле все еще сериализуется, но не будет отмечаться в инспекторе
Однако, вы можете сериализовать и поля имеющие другой уровень доступа. Сделать это очень просто - нужно приписать атрибут [UnityEngine.SerializeField]:
[SerializeField] private int a = 0; //Это поле сериализуется, несмотря на то что оно private [SerializeField][HideInInspector] private int b = 0;//Комбинируя атрибуты вы можете сериализовать и спрятать поле в инспекторе
Тут важно понимать - если вы пытаетесь сериализовать свой собственный класс, не наследуемый от Object - никакой SerializeField вам не поможет, если класс не имеет атрибута [System.Serializable].
Случается, что вам нужно наоборот избежать сериализации поля с доступом public. Для этих целей есть атрибут [NonSerialized]:
[NonSerialized] public int count = 0; //Это поле будет сбрасывать при ребилде
Сериализацию поддерживают не все поля. Вы не сможете сериализовать:
- статические поля
- свойства (которые get;set;)
- поля, помеченные как readonly
- прямоугольные массивы
- интерфейсы
Что делать с классами которые не сериализуются?
Часто среди стандартных библиотек Unity встречаются классы, которые не сохраняются и не могут быть нами отредактированы. Например к таким типам относятся типы Type, FieldInfo, MethodInfo, которые могут пригодиться при работе с рефлексией.
Однако, у большинства подобных классов есть свои "рычаги" для восстановления.
Например:
- тип Type может быть восстановлен через Type.GetType(). Для этого понадобится название класса, представленное строкой. Строки - сериализуемы, а значит и класс Type реально восстановить по этой информации
- тип MethodInfo может быть восстановлен по типам и названию метода
Таким образом, такие данные реально закешировать.
Пример сериализации для типа Type:
[Serializable] public class SerializeType { [SerializeField] private string _typeName; private Type _value; public Type value { get { if (_value == null) _value = Type.GetType(_typeName); return _value; } set { _typeName = value.AssemblyQualifiedName; _value = value; } } }
Однако и на этом еще не все. Для любого класса при помощи рефлексии вы можете устроить глубокое копирование полей, используя в качестве хранилища собственный ресурс. Но велосипед здесь изобретать нет никакого смысла - для хранения данных вам отлично подойдет следующая наработка:
Save and load binary/xml
Что осталось?
Я был бы рад, если бы кто-то мог поделиться способами для сериализации функторов, такие как Action<T>, Func<T> и тому подобные. Увы и ах, но на текущий момент как я не бился, я не нашел способа сериализовать данные классы.
Смотрите также:
Комментарии
- 1 (Текущая страница)
- 2
Это конечно все круто, но я так и не понял в чем соль сериализации? Может покажешь реальный пример?
alexprey, о, да запросто.
Сериализация нужна в проекте для того чтобы у нас в принципе сохранялась информация.
Дело в том, что когда мы пишем новый код и юнити его компилирует, происходит вынужденное уничтожение этих объектов. Соответсвенно, после ребилда часть данных просто исчезает, если мы не сериализовали ее.
Вот простой пример. Есть у нас класс
public class Example : MonoBehaviour { [NonSerialized] public int count = 0; [ContextMenu("Plus")] public void Plus() { Debug.Log(count++); } }
- как выглядит скрипт в инспекторе?
http://prntscr.com/39vejl
А он пустой! Нет там переменной count.
- Но мы добавили соответствующий пункт 'Plus' в контекстное меню, чтобы посмотреть как ведет себя переменная.
- Трех раз я думаю достаточно, чтобы увидеть тенденцию ;)
- Теперь сделаем какие-нибудь изменения в коде чтобы вызвать ребилд (лично я поставил пробел и сохранил). И повторим пункт 2. В чем резон? А в том что мы увидим тоже самое снова:
http://prntscr.com/39vgku
Мы снова начали с нуля.
А теперь посмотрим как это выглядело, если бы мы сейчас убрали атрибут [NonSerialized]. То есть что нужно сделать: удалить атрибут и сделать все тоже самое что я писал выше. Результат будет такой:
http://prntscr.com/39vhb8
Хотите пример пореальней:
- создайте с нуля любой класс, не наследуемый ни от чего. Просто класс.
- используйте его как поле в MonoBehaviour
- теперь попробуйте с простого - сделать отображение этого класса в редакторе, чтобы настроить его поля.
А в целом - если вы не сталкиваетесь в повседневном программировании с механизмом сериализации - вам пока рано читать эту статью. Но не забывайте про нее, она вам понадобится когда вы начнете писать редакторы под свои игры, для которых сами создаете какие-то данные. И когда вы с подобным столкнетесь - первое ваше разочарование может быть связано как раз с тем, что данные не сохранились. И отгадайте, кто этому будет виной. Вот тогда вы перечитаете то что я тут написал и сразу все станет ясно - зачем вам этот зверь.
Extravert, ну да интересная штука, но пока что еще не придумал как это можно использовать. А так я использую сериализацию, но на основе рефлексии и для совсем других целей, например для передачи по сети.
для работы по сети помимо сериализации надо ещё уметь высчитывать разницу между данными на сервере и на клиенте, чтобы не дать лихаря)
alexprey, я так понимаю, лучше использовать предназначенные для этого директивы, нежели медленную рефлексию
ScorpioT1000, немного не те дебри ) Речь шла о стандартной сериализации Unity, позволяющей хранить объекты на сцене и обновлять данные о них по мере того как мы программируем клиент.
Сериализация в сети, или что правильней сказать - работа с протоколами, это совсем другая тема. Могу написать статью о сериализации в сети, как это делается на быстрых серверах, но немного позже.
alexprey, придумывать способы - не понадобится. Сериализация является неотъемлимой частью работы с движком Unity. Она везде, где сохраняются данные. Но когда данные не сохраняются - приходится вникать в механизм как это работает, и вот об этом эта статья
есть прекрасная статья https://developer.valvesoftware.com/wiki/Source_Multiplayer_N...:ru
тут всё расписано) а уж как это построить и на какой платформе - дело авторов
здесь бы лучше устроить что-то вроде склада наработок для юнити, где есть готовые вещи
ScorpioT1000, ну там вообще не рассмотрены практические примеры, только базовые знания по интерполяции и экстраполяции можно подчерпнуть. По протоколам я где-то на хабре видел хорошую абстрактную статью, но лично с себя - я ее не понял, пока не столкнулся с этим сам.
Могу написать статью о сериализации в сети, как это делается на быстрых серверах, но немного позже
Пиши, лишним не будет)
Какие-то изменения в сериализации произошли в Unity5. Пока толком не понял, но по какой-то причине классы помеченные атрибутом [Serializable] сериализуются даже если юзаются в приватных полях. Непонятно пока, баг это или фича, исправят или оставят.
Extravert, похоже на баг, по идее приватные поля не должны сериализоваться... или, постой!!! Почему они не должны? В общем надо читать референсы
- 1 (Текущая страница)
- 2
CollectableItemData.cs
[CreateMenuItem(fileName = "newItem", menuName = "Data/Items/Collectable", order = 51]