danilaxxl danilaxxl

CollectableItemData.cs

[CreateMenuItem(fileName = "newItem", menuName = "Data/Items/Collectable", order = 51]

GoloGames GoloGames

vadya_ivan, рад, что вам игра показалась интересной : )

P.S. Кстати уже доступна бесплатная демо-версия в Steam

vadya_ivan vadya_ivan

Визуал, задумка, музыка , механики, все в цель

GoloGames GoloGames

Ato_Ome, спасибо за позитивные эмоции, будем стараться : )

Ato_Ome Ato_Ome

Потрясающий результат, все так четенько, плавненько)
То ли саунд, то ли плавность напомнили мне игрушку World of Goo, удачи вам в разработке и сил побольше дойти до релиза!)

Cute Fox Cute Fox

Graphics are a little cool, good HD content. But this game doesn't cause nary interest me.
However the game is well done.

GMSD3D GMSD3D

Почему действие после всех условий выполняется?
[step another object]

Zemlaynin Zemlaynin

Jusper, Везде, но наугад строить смысла нет. Нужно разведать сперва территорию на наличие ресурсов.

Jusper Jusper

Zemlaynin, а карьеры можно будет везде запихать?
Или под них "особые" зоны будут?

Zemlaynin Zemlaynin

Это так скажем тестовое строительство, а так да у города будет зона влияния которую нужно будет расширять.

Jusper Jusper

А ссылка есть?

Jusper Jusper

Я не оч понял из скриншота, как вообще работает стройка. У игрока будет как бы поле строительства?

split97 split97

в игру нужно добавить время песочные часы в инвентаре, пока бегаешь наберается усталость и ты очень тормозной мобильный враг просто убевает

split97 split97

в игру нужно добавить время песочные часы в инвентаре, пока бегаешь наберается усталость и ты очень тормозной мобильный враг просто убевает

ViktorJaguar ViktorJaguar

Почему я нигде не могу найти нормальный туториал, где покажут как экипировать предмет (например, меч) в определенную (выделенную под оружие) ячейку???

Логотип проекта Unity

Нативная сериализация и её подводные камни

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

Для того, чтобы понять почему происходит потеря ссылок, нам нужно понять сам алгоритм сериализации.
Для обычных классов он такой:

  1. Получено сообщение о перезагрузке сборки
  2. Все поля реализующие обычные классы сохраняются вне среды
  3. Сборка перезагружается
  4. Создаются *новые* экземпляры из сохраненной информации.
  5. Поля заполняются из сохраненной информации

У данного способа есть ряд грубых багов, которые могут вызывать краш Unity.
Взгляните на следующую конструкцию:

[Serializable]
public class TestScript
{
    public TestScript brother;
}

Данный код, несмотря на всю его кажущуюся безобидность способен заставить разработчика часами убиваться в дебаге. Чтобы получить тот "несбыточный вариант", достаточно написать код подобно следующему:

var a = new TestScript();
var b = new TestScript();
a.brother = b;
b.brother = a;

То есть вывести в классах ссылки друг на друга. Поведение в таких случаях бывает разным - в старых версиях движка это приводит к крашу и бесконечному циклу, в новых версиях это "закупорили", сделав подобные поля не сериализуемыми.
Почему же так происходило, и почему данная проблема пока не была решена иным путем? Механизм сериализации просто начинает "прыгать" между разными экземплярами.
Например так:

  1. Захожу в экземпляр a
  2. Среди полей нахожу brother
  3. Захожу в экземпляр b
  4. Среди полей нахожу brother
  5. И снова по кругу от 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> и тому подобные. Увы и ах, но на текущий момент как я не бился, я не нашел способа сериализовать данные классы.

Update: написал сериализацию функторов. Прошу любить и жаловать.

Смотрите также:


Комментарии



  • 1
  • 2 (Текущая страница)

alexprey, не должны по референсу в том и дело... Но уже не один раз наткнулся что сериализуются. Но [NonSerialize] спасает. Заметил что этот баг проявляется себя в ScriptableObject/EditorWindow как минимум

Extravert, ну с одной стороны если поле помечено как сериализуемое, то оно должно сохраняться, но с другой стороны... это проблема с безопасностью, если поле помечено как приватное

  • 1
  • 2 (Текущая страница)
Справка