Перейти к основному содержимому

Serializable Type System

Тип класса как обычное поле: Unity его сохраняет, а в инспекторе он выбирается из списка.

Быстрый старт​

До — Unity APIПосле — FastTools
[SerializeField]
private string _primaryWeaponName;

public System.Type PrimaryWeapon =>
string.IsNullOrEmpty(
_primaryWeaponName)
? null
: System.Type.GetType(
_primaryWeaponName, false);
[TypeSelector(Allow = TypeAllow.None)]
[SerializeField]
private SerializableType<Weapon>
_primaryWeapon;

public System.Type PrimaryWeapon =>
_primaryWeapon;

Выбор сериализуемого типа в инспектореВыбор сериализуемого типа в инспекторе

SerializableType​

SerializableType хранит assembly-qualified name — имя типа вместе со сборкой.

ВариантОграничение выбора
SerializableTypeБез базового ограничения
SerializableType<T>Типы, совместимые с T

Из кода обёртку создаёт конструктор; тип, несовместимый с T, вызывает ArgumentException:

var primary = new SerializableType<Weapon>(typeof(Sword));
System.Type type = primary;

var empty = new SerializableType<Weapon>(null);

ToString() найденного типа возвращает Type.Name: у generic-типа это Amplify`1, а не подпись из окна выбора.

Потерянный тип​

Сохранённое имя, которое перестало находиться после переименования класса, namespace или сборки. Инспектор показывает его как <Missing …>.

Потерянный тип Game.Combat.Spear в поле инспектораПотерянный тип Game.Combat.Spear в поле инспектора

  • Type возвращает null.
  • AssemblyQualifiedName и ToString() возвращают сохранённое имя, по которому тип можно восстановить.
внимание

В плеере SerializableType и SerializableMonoScript ищут тип по имени — это строка в данных сцены, префаба или ассета. Managed code stripping такие строки не разбирает, поэтому начиная с Managed Stripping Level Low класс, выбранный только в инспекторе, может не попасть в билд, и .Type вернёт null, хотя в редакторе тип находится. Пометьте такие классы [Preserve] (UnityEngine.Scripting) или перечислите их в link.xml. То же относится к [TypeSelector] на string.

SerializableMonoScript​

То же поле, но выбор переживает переименование класса: поле помнит сам ассет скрипта. Тип выбирают в инспекторе или перетаскивают на поле .cs из Project.

После переименования Sword.cs → Blade.csSerializableTypeSerializableMonoScript
.Typenull — потерянный типBlade, в Play Mode тоже
Сохранённое имяSwordBlade, как только ассет пересохранят

Ограничения:

  • в списке только классы со своим .cs: верхнего уровня, не generic, с именем как у файла;
  • generic-типы, вложенные классы и типы из DLL выбрать нельзя;
  • публичного конструктора нет, из кода поле не создать;
  • если переименовать класс без файла или файл вне Unity без .meta, связь теряется и поле показывает потерянный тип.

TypeSelector​

ПолеРезультат выбора
stringЗаписывается assembly-qualified name
SerializableType / SerializableMonoScriptНастраивается выбор обёртки
[SerializeReference]Создаётся экземпляр выбранной реализации — см. SerializeReference Selector

Какие типы в списке​

public interface ITwoHanded { }

public abstract class Weapon { }
public abstract class MeleeWeapon : Weapon { }
public abstract class RangedWeapon : Weapon { }

public sealed class Sword : MeleeWeapon { }
public sealed class Axe : MeleeWeapon, ITwoHanded { }
public sealed class Bow : RangedWeapon, ITwoHanded { }
Поле[TypeSelector(…, Allow = TypeAllow.None)]Типы в списке
stringtypeof(Weapon)Axe, Bow, Sword
SerializableType<MeleeWeapon>typeof(ITwoHanded)Axe — единственный MeleeWeapon с ITwoHanded
SerializableType<Weapon>"MeleeWeapon, Assembly-CSharp"Axe, Sword
SerializableType<Weapon>typeof(Sword), typeof(Axe)Пусто, AFT0009 предупредит: ни один класс не наследует оба
SerializableType<Weapon>[]без аргументаAxe, Bow, Sword у каждого элемента

Чтобы разрешить набор классов, дайте им общий интерфейс или базовый класс и укажите его.

Свойства​

СвойствоПо умолчаниюПоведение
AllowTypeAllow.AllПускает в список абстрактные классы (Abstract), интерфейсы (Interface), оба вида или ни один. На [SerializeReference] игнорируется
RequiredfalseПредупреждает о пустом имени типа или null в managed-ссылке
примечание

В инспекторе runtime-объекта селектор не предлагает типы из editor-only сборок (UnityEditor, asmdef только для Editor и папки Editor): в билде плеера они не найдутся. Правило определяется классом объекта, поэтому поле runtime-объекта под #if UNITY_EDITOR их тоже не предлагает.

Обязательное поле​

[TypeSelector(typeof(Weapon), Required = true)]
[SerializeField] private string _secondaryWeapon;

Пустое обязательное поле показывает предупреждение под селекторомПустое обязательное поле показывает предупреждение под селектором

С Required = true пункт <None> остаётся доступным. У строки или обёртки проверяется пустое сохранённое имя; потерянный тип с непустым именем эту проверку проходит.

Настройка проверки по всему проекту и в CI описана в разделе проверки обязательных полей.

Ограничение из другого поля​

Передайте nameof(...), чтобы текущее значение поля или свойства управляло списком кандидатов:

[SerializeField] private SerializableType<Weapon> _weaponClass;

[TypeSelector(nameof(_weaponClass), Allow = TypeAllow.None)]
[SerializeField] private string _weaponName;

Выберите MeleeWeapon в Weapon Class — Weapon Name предложит Sword и Axe. Смена ограничения не очищает ранее выбранное имя.

Выбор MeleeWeapon в Weapon Class оставляет в Weapon Name только Axe и SwordВыбор MeleeWeapon в Weapon Class оставляет в Weapon Name только Axe и Sword

Источник ограниченияЧто ограничивает
System.TypeОдин тип
stringИмя типа, разрешаемое через Type.GetType()
SerializableType / SerializableMonoScriptРазрешённое значение .Type
Массив этих значенийНесколько ограничений одновременно; List<T> не поддерживается
  • Строка сначала ищется среди нестатических полей и читаемых свойств класса, где объявлено поле, включая унаследованные, затем — как имя типа.
  • У поля внутри [Serializable]-класса или элемента списка источник читается из того же экземпляра.
  • Пока источник пуст или не разрешился, ограничения от него нет: строковый Weapon Name предложит все неабстрактные классы проекта. У обёртки остаётся её собственный T.

Ошибки в строковых аргументах​

[TypeSelector("Spear, Assembly-CSharp")]
[SerializeField] private string _weaponName;

Ошибки в строках находят анализаторы:

  • AFT0006 — строка из одного слова, но такого члена у класса нет;
  • AFT0007 — член не может задать базовые типы;
  • AFT0008 — строка не похожа на имя типа.

Если имя типа записано верно, но такой тип не загружен, как Spear выше, предупреждение показывает инспектор:

Ограничение не разрешилось — инспектор показывает предупреждение под полемОграничение не разрешилось — инспектор показывает предупреждение под полем

TypeSelectorDisplay​

[TypeSelectorDisplay] на классе меняет только его строку в окне выбора:

Параметр на SwordВ окне выбора
Name = "Longsword"Longsword в списке и в закрытом поле; поиск находит и по Sword
Group = "Weapons/Melee"Weapons → Melee → Longsword вместо namespace
Tooltip = "A balanced blade"Подсказка при наведении
Icon = "d_ScriptableObject Icon"Иконка: встроенная по имени, ассет по пути с расширением или из Resources без расширения
Hidden = trueНет в списке; присваивание из кода и уже сохранённое значение работают

Подклассы настроек не наследуют.

Имя Longsword, иконка и группа Weapons/Melee в окне выбораИмя Longsword, иконка и группа Weapons/Melee в окне выбора

примечание

[TypeSelector] и [TypeSelectorDisplay] помечены [Conditional("UNITY_EDITOR")]. В классах из внешней DLL, собранной без этого символа, их настроек нет, включая Hidden.

Окно выбора​

Окно группирует типы по namespace или Group и различает одинаковые имена по сборкам.

Избранные и недавние типы на корневой странице окна выбораИзбранные и недавние типы на корневой странице окна выбора

На корневой странице окно держит типы, которые нужны чаще других:

  • Favorites — избранное. Чтобы добавить тип, нажмите звёздочку справа от его строки или Space, когда строка выделена.
  • Recent — последние выбранные типы.

Показ Favorites и длина Recent (0 скрывает раздел) настраиваются во вкладке Settings окна FastTools. Её открывает шестерёнка в правом нижнем углу окна выбора, там же оба списка очищаются.

Generic-типы​

При выборе открытого generic-типа окно предлагает выбрать аргументы и возвращает сконструированный закрытый тип:

public abstract class Enchantment { }
public sealed class Fire : Enchantment { }
public sealed class Frost : Enchantment { }

public sealed class Enchanted<T> : MeleeWeapon
where T : Enchantment { }

Выберите Enchanted<T> в поле _primaryWeapon — окно предложит наследников Enchantment, а после выбора Fire запишет Enchanted<Fire>.

Выбор аргумента generic-типа в окне выбораВыбор аргумента generic-типа в окне выбора

  • Generic-аргумент может сам быть generic-типом: окно сначала спросит его аргументы.
  • Если все аргументы выводятся из типа поля, закрытый тип возвращается сразу.
  • Интерфейсы, абстрактные классы и скрытые типы в аргументах не предлагаются.

TypeSelectorWindow​

TypeSelectorWindow открывает то же окно из кастомного инспектора или окна редактора, например по кнопке UI Toolkit:

using Aspid.FastTools.Types.Editors;

var button = new Button { text = "Select weapon" };
button.clicked += () => TypeSelectorWindow.Show(
GUIUtility.GUIToScreenRect(button.worldBound),
new TypeSelectorFilter { Types = new[] { typeof(Weapon) } },
currentAqn: selectedTypeName,
onSelected: aqn => selectedTypeName = aqn);

Обработчик получает assembly-qualified name или null при выборе <None>; закрытие окна без выбора его не вызывает.

currentAqnОтмечено при открытии
Имя типа из спискаЭтот тип; окно открывается в его группе
"" (по умолчанию)<None>
nullНичего
Имя, которого нет в спискеНичего: Enter сразу после открытия не сотрёт сохранённое имя

TypeSelectorFilter​

СвойствоПо умолчаниюНазначение
Typesпусто — любые типыБазовые типы; кандидат совместим с каждым
AllowNone; у [TypeSelector] — AllРазрешённые категории: абстрактные классы и интерфейсы
PredicatenullУсловие поверх Types и Allow
AdditionalTypesnullКандидаты, обходящие Types, Allow и Predicate; фильтр Hidden сохраняется
ArgumentFilternullУсловие для аргументов, выбираемых вручную, сверх ограничений where
InferredArgumentFilternullФильтр аргументов, выведенных из Types; получает generic-определение, параметр и аргумент
IncludeHiddenfalseПоказывать типы с Hidden = true, в том числе в аргументах
HideNoneOptionfalseСкрыть <None> на корневой странице

Пример в пакете​

Выбор типов врагов и паттерна расстановки в инспекторе показан в примере Types, а окно выбора, открытое из редакторского кода, — в EditorTools.

Волна обычных и элитных врагов движется к центру.Волна обычных и элитных врагов движется к центру.