Инспектор-нативная производственная среда

Большинство 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.sirenixdefineConstraints: ["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'ы в инспекторе, это не недостаток и не «странный стиль». Это сознательное распределение ответственности между ролями.

Если в проекте есть дизайнер — он становится полноценным участником производства, не пассивным потребителем. Если дизайнера нет — те же механизмы работают и для разработчика (просто без выигрыша от разделения ролей). В обоих случаях работает.


Связанные страницы: