Serializable Type System
Тип класса как обычное поле: Unity его сохраняет, а в инспекторе он выбирается из списка.
Быстрый старт
| До — Unity API | После — FastTools |
|---|---|
| |


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 …>.


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.cs | SerializableType | SerializableMonoScript |
|---|---|---|
.Type | null — потерянный тип | Blade, в Play Mode тоже |
| Сохранённое имя | Sword | Blade, как только ассет пересохранят |
Ограничения:
- в списке только классы со своим
.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)] | Типы в списке |
|---|---|---|
string | typeof(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 у каждого элемента |
Чтобы разрешить набор классов, дайте им общий интерфейс или базовый класс и укажите его.
Свойства
| Свойство | По умолчанию | Поведение |
|---|---|---|
Allow | TypeAllow.All | Пускает в список абстрактные классы (Abstract), интерфейсы (Interface), оба вида или ни один. На [SerializeReference] игнорируется |
Required | false | Предупреждает о пустом имени типа или 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. Смена ограничения не очищает ранее выбранное имя.


| Источник ограничения | Что ограничивает |
|---|---|
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 | Нет в списке; присваивание из кода и уже сохранённое значение работают |
Подклассы настроек не наследуют.


[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-типом: окно сначала спросит его аргументы.
- Если все аргументы выводятся из типа поля, закрытый тип возвращается сразу.
- Интерфейсы, абстрактные классы и скрытые типы в аргументах не предлагаются.
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 | пусто — любые типы | Базовые типы; кандидат совместим с каждым |
Allow | None; у [TypeSelector] — All | Разрешённые категории: абстрактные классы и интерфейсы |
Predicate | null | Условие поверх Types и Allow |
AdditionalTypes | null | Кандидаты, обходящие Types, Allow и Predicate; фильтр Hidden сохраняется |
ArgumentFilter | null | Условие для аргументов, выбираемых вручную, сверх ограничений where |
InferredArgumentFilter | null | Фильтр аргументов, выведенных из Types; получает generic-определение, параметр и аргумент |
IncludeHidden | false | Показывать типы с Hidden = true, в том числе в аргументах |
HideNoneOption | false | Скрыть <None> на корневой странице |
Пример в пакете
Выбор типов врагов и паттерна расстановки в инспекторе показан в примере Types, а окно выбора, открытое из редакторского кода, — в EditorTools.

