Инспектор-нативная производственная среда
Большинство Unity-фреймворков ориентированы на разработчика: цель — сделать код чище, тесты проще, зависимости явнее. Геймдизайнер и художник в такой модели — пассивные потребители результата.
Vortex — другой случай. Он проектировался как production pipeline для смешанных команд, где разработчик пишет логику, дизайнер собирает поведение в инспекторе, художник работает с префабами и анимациями. Editor-tooling в Vortex — не «удобный бонус сверху», а часть архитектуры.
Эта страница объясняет, какие механизмы делают Vortex inspector-native фреймворком, и что эта ориентация даёт командам.
Базовый тезис
Vortex делит ответственность между ролями явно:
- Разработчик пишет код — контроллеры, стратегии, драйверы, шины.
- Дизайнер конфигурирует поведение через ScriptableObject-конфиги и инспектор.
- Художник работает с префабами, анимациями, материалами.
Каждый владеет своим слоем и не блокирует остальных. Дизайнер не ждёт «когда разработчик добавит новый dropdown в редактор» — нужные dropdown'ы появляются сами через рефлексию. Художник не вмешивается в код, чтобы изменить как выглядит UI — структура префаба определяет внешний вид без правок логики.
Это работает, потому что в Vortex есть набор канонических механизмов, каждый из которых явно ориентирован на инспектор как первоклассный API.
EditorTools-атрибуты как канон
Vortex'овские EditorTools-атрибуты построены поверх Sirenix Odin Inspector — отдельной обязательной зависимости фреймворка. По происхождению атрибуты делятся на три группы:
- Vortex (V) — собственные атрибуты пакетов
Vortex.Unity.EditorToolsи доменных систем (Audio, Database, Localization и др.). - Odin (O) —
Sirenix.OdinInspector, используются напрямую. - Unity (U) — нативные атрибуты
UnityEngine.
В примерах ниже источник указан комментарием рядом с атрибутом.
Пакет Vortex.Unity.EditorTools — это базовый набор атрибутов, который вместе с Odin'ом превращает Unity-инспектор из «технического представления полей класса» в интерфейс настройки.
Несколько ключевых атрибутов:
public class EnemyConfig : ScriptableObject
{
[AutoLink] // V: авто-привязка компонента с того же GameObject
private Animator _animator;
[ClassFilter(typeof(IDamageable))] // V: ObjectField принимает только IDamageable
[SerializeField] // U: стандартная Unity-сериализация
private MonoBehaviour _target;
[ToggleButton("LabelsMethod", "ColorsMethod")] // V: enum как кнопки-переключатели
[SerializeField]
private DamageType _damageType;
[ValueSelector("GetAvailableSkills")] // V: dropdown из произвольного списка
[SerializeField]
private string _skillId;
[DbRecord(typeof(WeaponPreset))] // V: фильтрованный picker GUID'ов из Database
[SerializeField]
private string _weaponGuid;
[AudioChannelName] // V: dropdown из AudioChannelsConfig
[SerializeField]
private string _channel;
}
Каждый атрибут заменяет типичную ручную работу дизайнера:
| Атрибут | Источник | Что снимает с дизайнера |
|---|---|---|
[AutoLink] |
Vortex | Перетаскивание компонента в Inspector-поле, когда он рядом |
[ClassFilter] |
Vortex | Запоминание совместимости типов при drag-and-drop |
[ToggleButton] |
Vortex | Раскрытие enum'ов в стандартный dropdown |
[ValueSelector] |
Vortex | Ввод строк руками или копипаста из других мест |
[ClassLabel("$Method")] |
Vortex | Стандартное «Element 0, Element 1» в массивах |
[DateTimeDraw], [TimeDraw] |
Vortex | Отображение long-полей как дат и времени вместо сырых чисел |
[TimerDraw], [DateTimerDraw] |
Vortex | Read-only таймер/дата с автообновлением |
[DbRecord] |
Vortex (Database) | Поиск GUID'а нужной записи в Database |
[AudioChannelName] |
Vortex (Audio) | Запоминание имени канала из AudioChannelsConfig |
[EffectKey] |
Vortex (EffectSpawn) | Поиск ключа эффекта в EffectsCatalog |
[OnValueChanged] |
Odin | Реактивную перепроверку при изменении поля |
[OnInspectorInit] |
Odin | Инициализацию вычисляемых полей при открытии инспектора |
[ShowInInspector] |
Odin | Показ свойств (не только полей) в инспекторе |
[InfoBox] |
Odin | Контекстные подсказки рядом с полем |
[HorizontalGroup], [FoldoutGroup] |
Odin | Группировку полей в инспекторе |
[HideReferenceObjectPicker] |
Odin | Скрытие стандартного reference picker'а у [SerializeReference]-полей |
Без этих атрибутов дизайнер видит технический интерфейс Unity — со строковыми GUID'ами, неудобными ObjectField, безымянными элементами массивов. С ними — доменный интерфейс, где каждый выбор фильтрован, типизирован и подсказан.
Важно. Атрибуты пакета EditorTools используют Odin-API через свои drawer'ы в сборке
ru.vortex.unity.editortools.sirenix(сdefineConstraints: ["ODIN_INSPECTOR"]) — без Odin они не работают. Доменные атрибуты живут в своих пакетах и могут использовать обычный Unity-PropertyDrawer (например[AudioChannelName]) либо собственные drawer'ы ([DbRecord],[EffectKey]). См. Сравнение Vortex и VContainer о статусе Odin как обязательной зависимости.
Условная видимость
Для управления видимостью полей в Vortex используются Odin-атрибуты напрямую — собственных обёрток у Vortex для этого нет:
[SerializeField] private bool _useAdvanced;
[ShowIf("_useAdvanced")] // O: поле показывается только при _useAdvanced == true
[SerializeField] private float _multiplier;
[HideInEditorMode] // O: поле скрыто в Edit Mode, видно в Play
[SerializeField] private float _runtimeValue;
[ShowInPlayMode] // O: поле видно только в Play Mode
[SerializeField] private int _debugCounter;
Это снимает у дизайнера типичную проблему «вижу 30 полей, не знаю, какие из них релевантны прямо сейчас». Через Odin доступны также [HideIf], [DisableIf], [EnableIf] и более сложные условные выражения — полный набор см. в документации Odin.
[SerializeReference] через Odin
Один из самых мощных приёмов Vortex — полиморфизм через инспектор.
Когда поле помечено [SerializeReference] (нативный Unity-атрибут с версии 2020.1), Unity сериализует значение по runtime-типу, а не по объявленному. Это и есть основа полиморфизма в инспекторе. Сам dropdown выбора конкретного наследника рисует Odin:
public class RewardData
{
[SerializeReference] // U: сериализация по runtime-типу
[HideReferenceObjectPicker] // O: скрыть стандартный picker, оставить только Odin-меню
private RewardStrategy strategy;
}
В инспекторе на пустом поле появляется right-click меню Odin'а «Set instance → выбор конкретного типа», либо специальная кнопка добавления, в зависимости от настройки. Дизайнер выбирает тип из dropdown'а, Odin создаёт инстанс, Unity сериализует его по runtime-типу. При добавлении нового наследника в коде он автоматически появляется в выборе.
Этот приём применяется по всему Vortex для расширяемых точек:
RewardStrategy— стратегии выдачи наград.QuestConditionLogic— условия квестов.QuestLogic— логические шаги квеста.BattleAi— варианты ИИ в боевых системах.- любая другая extension point, где нужен «список вариантов с собственными полями».
Дизайнер не знает, что под капотом — Unity [SerializeReference] + Odin-рефлексия. Он видит dropdown с понятными именами и работает с ним как с любым другим выбором.
В инспекторе:
RewardData
└── Strategy: [ ▼ Choose type... ]
├── GiveItemStrategy
├── GiveHealStrategy
├── SpawnMimicStrategy
└── (новые добавляются автоматически)
Когда разработчик добавляет четвёртый тип награды — он автоматически появляется в dropdown без правки RewardData, без RegisterStrategy<> call, без installer'а. Это [SerializeReference] + рефлексия по наследникам.
Этот приём применяется по всему Vortex для расширяемых точек:
RewardStrategy— стратегии выдачи наград.QuestConditionLogic— условия квестов.QuestLogic— логические шаги квеста.BattleAi— варианты ИИ в боевых системах.- любая другая extension point, где нужен «список вариантов с собственными полями».
Дизайнер не знает, что под капотом — [SerializeReference] + type scanning. Он видит dropdown с понятными именами и работает с ним как с любым другим выбором.
Template generation
Vortex включает .vtp-формат шаблонов и инструмент их разворачивания (VtpTemplateGenerator).
.vtp-шаблон — это архивированная структура файлов с плейсхолдерами для подстановки. Пример: шаблон мини-игры:
minigames.vtp
├── {!ClassName!}Controller.cs.template
├── {!ClassName!}Data.cs.template
├── {!ClassName!}Hub.cs.template
├── {!ClassName!}View.cs.template
├── Handlers/
│ ├── {!ClassName!}WinHandler.cs.template
│ └── {!ClassName!}FailHandler.cs.template
├── Presets/
│ └── {!ClassName!}GeneralConfig.cs.template
└── Editor/
└── {!ClassName!}TemplateMenu.cs.template
Через MiniGameTemplateMenu дизайнер или разработчик вводит имя новой мини-игры — и Vortex разворачивает полную структуру файлов с заменой плейсхолдеров. После этого у проекта появляются:
PuzzleController(контроллер игры) с заглушками методов.PuzzleData(данные сессии) с базовыми полями.PuzzleHub(шина мини-игры).PuzzleView(контейнер представления) с привязками.- Win/Fail handler'ы с правильными подписками.
PuzzleGeneralConfig(SO-конфиг) с настройками сложности.- Меню в Unity для создания тестового пресета.
Разработчик пишет только специфичную логику (правила игры, рисование экрана). Вся обвязка — controller'ы, data, view, handler'ы, регистрации — генерируется из шаблона.
Это даёт экспоненциальное ускорение для типовых сущностей. Команда из 5 человек может добавлять по новой мини-игре в день, потому что каркас одинаков и собирается автоматически.
Editor-time валидация
Vortex много вкладывает в обнаружение ошибок до запуска.
Подсветка несовместимых типов
[ClassFilter(typeof(IDamageable))] (Vortex) не только фильтрует drag-and-drop — он визуально подсвечивает поле красным, если в нём оказался объект неподходящего типа:
EnemyConfig
├── Target: [ ▌ MyComponent ] ⚠ MyComponent does not implement IDamageable
Дизайнер сразу видит ошибку, не дожидаясь запуска.
Синхронизация массивов
В сложных конфигах (например, MiniGameGeneralConfig с массивами таймеров под N уровней сложности) Vortex автоматически подгоняет длины массивов:
[OnValueChanged("OnLevelsChanged")] // O: Odin'овский хук, дёргает метод при изменении
[SerializeField] private int _levelsCount = 3;
[SerializeField] private float[] _timers; // длина синхронизируется в OnLevelsChanged
[SerializeField] private Vector2[] _fieldSizes;
Изменил _levelsCount с 3 на 5 — массивы _timers и _fieldSizes сами расширятся до 5 элементов с сохранением старых значений. Дизайнер не редактирует размер каждого массива отдельно.
[OnValueChanged] — Odin-атрибут. Vortex использует его в десятках конфигов для editor-side реакции на изменения.
Пересчёт процентов и других производных значений
В взвешенных конфигах (RewardPreset, SpineAnimationRandomLogic) Vortex пересчитывает проценты выпадения при изменении весов и показывает их рядом со слайдером:
RewardPack
├── Weight: [▓▓▓▓▓░░░░░] 50 Percent: 33%
├── Weight: [▓▓░░░░░░░░] 20 Percent: 13%
└── Weight: [▓▓▓▓▓▓▓▓░░] 80 Percent: 54%
Дизайнер видит сразу, какие шансы получает каждый pack. Не нужно считать в уме 50 / (50+20+80) = 33% для каждой строки.
Pre-runtime проверки структуры пресета
Конфиги вроде EffectsCatalog имеют editor-кнопки валидации структуры:
EffectsCatalog
├── effects: [...]
├── [ Scan Project ] ← найти все эффект-префабы в проекте
├── [ Validate ] ← проверить, что все указанные префабы существуют
└── [ Show Index ] ← показать ключи и связи
Дизайнер запускает Validate, видит список проблем (отсутствующие префабы, дубли ключей, неверные ссылки), исправляет до запуска. В runtime эти проблемы уже не возникают.
Prefab-driven view
UI и эффекты в Vortex собираются из готовых префабов, не из кода.
UIComponent + Parts
UIComponent — это контейнер, в котором View собирается из специализированных частей:
UserInterface (UIComponent on root)
├── UIComponentText[] — все TextMeshPro/Text-объекты экрана
├── UIComponentButton[] — все кнопки
├── UIComponentGraphic[] — все Image/SpriteRenderer
└── UIComponentSwitcher[] — все UIStateSwitcher
Дизайнер собирает UI как обычные префабы Unity, перетаскивает в UIComponent-массивы нужные элементы — порядок в массиве задаёт позицию каждой части. Логика обращается к UI через номер позиции (int) либо через enum-состояние свитчера; строковой адресации частей нет:
view.SetText("1000", scorePosition); // текст — первый аргумент, позиция — второй
view.SetSprite(playerIcon, avatarPosition);
view.SetAction(OnSubmit, submitPosition);
view.SetSwitcher(SwitcherState.On, statusPosition);
Контроллер не знает, как устроен экран. Он знает только позиции: «текст под позицией score, графика под позицией avatar, кнопка submit, свитчер status». Дизайнер может перерисовать экран полностью — пока порядок частей в массивах сохранён, логика не меняется.
UserInterface — корневой шаблон UI
UserInterface.prefab (входит в UI-примитивы Vortex) — это готовый корень экрана с настроенным UIProvider-биндингом и анимацией показа/скрытия. Дизайнер делает из него Prefab Variant, наполняет содержимым, регистрирует в UIProvider — и экран работает.
EffectSpawnSystem
Эффекты собираются по тому же принципу — префаб с TweenerHub и звуком:
Effect (prefab root)
├── EffectView + TweenerHub
├── SkeletonGraphic / ParticleSystem / Image
└── [Sound]
└── AudioHandler (Play On Enable: ✓)
Дизайнер делает эффект как обычный префаб, регистрирует в EffectsCatalog. Спавн идёт через EffectSpawn.Spawn(target, "hit") — конкретный визуал, длительность, звук уже определены префабом.
Editor synchronization
В фоне Vortex автоматизирует то, что разработчик иначе делал бы вручную.
Авто-создание ассетов
CoreAssetsController сканирует наследников ICoreAsset и при первой компиляции автоматически создаёт SO-ассеты для каждого из них в Assets/Resources/Settings/:
Assets/Resources/Settings/
├── SdkSettings.asset ← создаётся автоматически
├── DriverConfig.asset ← создаётся автоматически
├── AudioChannelsConfig.asset ← создаётся автоматически
├── DatabaseSettings.asset ← создаётся автоматически
└── StartSettings.asset ← создаётся автоматически
Разработчик не пишет код вроде «если ассет не существует — создать». Он просто наследует от ICoreAsset и пишет поля.
То же самое с SettingsPreset-наследниками — SettingsDriver сам создаёт пресеты при их отсутствии.
Singleton-ассет паттерн
AssetDatabaseExt.GetSingletonAsset<T>() — утилита, которая ищет в проекте единственный ассет нужного типа (с гарантией, что он там один и валидный):
var config = AssetDatabaseExt.GetSingletonAsset<CharactersConfiguration>();
В Editor — через AssetDatabase.FindAssets. В Runtime — через Resources.LoadAll. Один API для обоих контекстов. Если ассет в проекте дублирован — выдаётся понятная ошибка. Разработчик не пишет boilerplate поиска.
Авто-импорт пар (.asset + .meta)
При генерации шаблонов и авто-создании ассетов Vortex заботится о .meta-файлах: новые ассеты сразу получают GUID, сохраняются в проект, и Unity не теряет ссылок при перекомпиляции.
Что это даёт системно
Всё вместе — атрибуты, dropdown'ы, шаблоны, валидация, prefab-driven view, авто-синхронизация — даёт четыре системных эффекта.
1. Дизайнер не блокирован разработчиком. Большинство настроек поведения (баланс, dropdown'ы вариантов, наполнение каталогов) делается в инспекторе. Разработчик подключается только когда нужна новая логика, не новая настройка существующей.
2. Меньше «передачи задач туда-обратно». Когда дизайнер обнаруживает «у этого моба badly выставлен damage», он правит SO-конфиг прямо в редакторе и коммитит. Без задачи разработчику, без билда, без code review. Цикл правки балансов сокращается с дней до часов.
3. Pre-runtime валидация ловит ошибки до билда. Подсветка несовместимых типов в ClassFilter, проверка структуры через Validate-кнопки в каталогах, дропдауны с фильтрацией — всё это отлавливает баги в момент настройки, не в runtime. Это особенно ценно для билдов «без разработчика рядом» (CI/CD пайплайны, проверка перед релизом).
4. Advanced designer/QA-сценарии становятся возможны. Опытный дизайнер может самостоятельно сделать новую мини-игру из шаблона, новый квест из конфига, новый эффект из префаба. QA может настроить тестовые сцены с собственными конфигами и пресетами. Это размывает границу «программист vs контентщик» — оба работают с одним фреймворком на разных уровнях абстракции.
Цена
Inspector-native подход не бесплатный.
Зависимость от Odin Inspector. EditorTools-атрибуты Vortex построены поверх Odin (SirenixOdinDrawers — отдельная сборка с defineConstraints: ["ODIN_INSPECTOR"]). Без Odin половина атрибутов не работает. Это обязательная зависимость, не опциональная — что прописано в установочной документации.
Атрибутов много, без знакомства легко не увидеть половину возможностей. Новый разработчик в Vortex-проекте часто не использует [AutoLink], [ClassFilter], [ValueSelector] просто потому, что не знает о них. Это лечится обучением и code review — но не само собой.
SO-конфиги нужно поддерживать в чистоте. Когда конфигов становится много (DriverConfig + SdkSettings + AudioChannelsConfig + DatabaseSettings + по одному на каждый SDK-пакет), дизайнер легко теряется. Vortex кладёт всё в Resources/Settings/ и автогенерирует имена ассетов, но визуальная навигация всё равно требует дисциплины.
Editor-time валидация — не runtime-валидация. Если кто-то намеренно обойдёт инспектор (например, через рефлексию или ручную правку YAML-ассета), validation не сработает. В команде это редко случается, но возможно.
Сравнение с programmer-first подходами
В DI-фреймворках (Zenject, VContainer) и в чистом коде без фреймворка инспектор используется в основном для drag-and-drop ссылок на компоненты. Логика композиции живёт в installer'ах, dependencies резолвятся в коде, dropdown'ы редко выходят за пределы стандартных Unity-полей.
Дизайнер в такой модели — пассивный потребитель: он работает с тем, что разработчик уже настроил. Любая новая опция требует кода. Любой dropdown требует кастом-drawer'а, который сам кто-то пишет.
Vortex явно делит ответственность:
| Слой | Programmer-first подходы | Vortex |
|---|---|---|
| Код архитектуры | Разработчик | Разработчик |
| Конфигурация сервисов | Разработчик (installer.cs) | Дизайнер (SO-конфиг) |
| Выбор вариантов поведения | Разработчик (registration calls) | Дизайнер (dropdown) |
| Создание нового контента | Разработчик (новый класс) | Дизайнер (новый ассет) |
| Балансы и параметры | Разработчик (константы в коде) | Дизайнер (поля в SO) |
| Сборка экранов UI | Разработчик (бинды в коде) | Дизайнер (UIComponent в префабе) |
Это не оценочное «лучше/хуже». Это разные оси приложения архитектуры. Programmer-first работает лучше там, где команда состоит из разработчиков и контент минимален (системные утилиты, технические демо). Vortex работает лучше там, где есть дизайнерская команда и контента много (игры, конфигураторы, инструменты с богатой настройкой).
Заключение
Inspector-native production pipeline — это архитектурный выбор, проявляющий себя в дизайне всех пакетов Vortex. Не «приятный бонус editor-tooling-а», а основная производственная парадигма.
Когда новый разработчик приходит в Vortex-проект и видит, что половина решений выражена через ScriptableObject-ассеты и dropdown'ы в инспекторе, это не недостаток и не «странный стиль». Это сознательное распределение ответственности между ролями.
Если в проекте есть дизайнер — он становится полноценным участником производства, не пассивным потребителем. Если дизайнера нет — те же механизмы работают и для разработчика (просто без выигрыша от разделения ролей). В обоих случаях работает.
Связанные страницы:
- Композиция и пакеты — где находятся SO-конфиги и как они складываются.
- EditorTools — детальный reference по атрибутам.
- UI Components — конкретика модульной UI-системы.
- MiniGames — пример template generation.
- EffectSpawn — prefab-driven эффекты.
- Сравнение Vortex и VContainer — programmer-first альтернатива.