Философия Vortex

или зачем фреймворк, если есть Unity

Этот фреймворк не появился из желания «сделать ещё один инструмент».

Он появился как попытка решить одну и ту же проблему, повторявшуюся из проекта в проект: любая архитектура со временем начинает разваливаться.

Не сразу. Сначала всё кажется нормальным. Потом появляются костыли. Потом связи. Потом зависимости. И в какой-то момент любое изменение начинает тянуть за собой половину проекта.

Контекст. Vortex в текущей реализации построен под Unity — большая часть инфраструктуры (инспектор, MonoBehaviour, ScriptableObject, asmdef-границы) использует Unity API. Core-слой формально написан на чистом C# и теоретически портируется на другую среду, но прикладные слои этого пути сейчас не используют. Возможный порт — открытая, не закрытая возможность.

Откуда взялась идея

Стало очевидно, что проблема не в конкретных технологиях.

Не в Unity. Не в паттернах. Не в «неправильных решениях».

Проблема в другом: система не защищена от самой себя.

Любой код можно расширить не туда, связать не с тем, изменить без понимания последствий. И если архитектура это позволяет — она рано или поздно деградирует.

Что хотелось получить

Не «красивую архитектуру» и не «гибкость на всякий случай». А конкретные свойства, которые удерживают проект от деградации на длинной дистанции:

1. Предсказуемость

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

2. Ограничения

Чтобы нельзя было случайно связать несовместимые вещи, обойти ключевые правила, «сделать как быстрее, а потом разберёмся».

Архитектура должна не только помогать — она должна делать неправильные решения видимыми.

3. Минимальность

Любая сущность в системе должна иметь понятную роль, быть оправдана и не существовать «на всякий случай».

4. Масштабируемость без боли

Чтобы можно было добавлять новые системы, переписывать старые, расширять проект — и при этом не переписывать всё остальное.

Во что это вылилось

Вместо попытки «собрать идеальную архитектуру» получилась другая идея:

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

Базовое упрощение

Любая программа — это всего три вещи: получение данных, изменение данных, отображение данных.

Именно поэтому в Vortex три роли — Controller, Data, View, и поток между ними однонаправленный:

Controller → Data → View

Каждое нарушение этого потока — это и есть «незаметная поломка системы», которую Vortex старается сделать заметной.

Ключевая мысль

Vortex — это не про «удобно писать код».

Это про: незаметно сломать систему невозможно.

Принципы и их цена

Каждый принцип — связка из трёх частей: правило, механизм фреймворка, который его поддерживает, и цена за его нарушение. Над всеми ними стоит мета-правило, фильтрующее сами правила.


0. Каждая техника окупается

Правило: Vortex не добавляет паттерны «потому что они правильные» или «как принято в индустрии». Любая техника во фреймворке проходит фильтр: её стоимость внедрения и поддержания должна быть меньше профита, который она даёт. Иначе — не попадает, даже если она канонична.

Это не про «удобство ради удобства». Это инженерное наблюдение: неудобные решения обходят костылями, а костыли — главный источник тихих поломок. Чем меньше трения у канонического пути, тем меньше соблазна с него свернуть. Поэтому фильтр окупаемости — одна из основных линий защиты от эрозии: он не пускает в фреймворк техники, которые будут обходить под давлением сроков.

Остальные четыре принципа — конкретные правила, прошедшие этот фильтр. Каждое платит за себя: цена его нарушения выше, чем цена его соблюдения.


1. Системы не связываются напрямую

Механизм: Database как шина данных. Всё межмодульное взаимодействие идёт через неё — по типу или GUID.

Что ломается, если нарушить:

  • Изменение в одной системе тянет правки в соседях, которые на неё ссылаются.
  • Модуль становится неперемещаемым — нельзя вынести в другой проект, не утащив за ним половину сцены.
  • Связи в Inspector расползаются и обрываются при смене сцены или префаба.
  • Тесты невозможны — модуль не запускается без всех своих «соседей».

2. Менять данные имеет право только контроллер

Механизм: Data сделана без публичных сеттеров логики. View и сторонние компоненты физически не могут писать в неё.

Что ломается, если нарушить:

  • Источник истины размывается: на вопрос «кто поменял это поле» ответа нет.
  • Save/Load начинает грузить данные в неконсистентном виде — часть полей менялась мимо контроллера, инварианты нарушены.
  • Реактивные подписки не срабатывают — мутация в обход логики обходит и оповещения.
  • Появляются race conditions при асинхронных переходах состояний.

3. UI не содержит логики

Механизм: UIStateSwitcher для переключения состояний, UIComponent для привязки к данным, UIProvider для условного открытия экранов. Всё декларативно, без императивного кода в MonoBehaviour.

Что именно запрещено:

Под «логикой» здесь понимается логика изменения данных: запись в шину, мутация моделей, принятие решений «что произойдёт в игре». Это всё живёт в контроллере, не во View.

Что разрешено и нормально:

  • Внутренняя рефлексия View — реакция на анимационные события, коллайдеры, ввод пользователя, физику. Это поведение конкретного визуального объекта, а не игровая логика.
  • Внутренние состояния View — переключения по UIStateSwitcher, локальные анимации (TweenerHub), управление AudioSource, пулинг частиц. Они описывают, как View себя ведёт.
  • Сообщение наружу через события или вызов контроллера — View может сказать «игрок нажал на меня» / «анимация дошла до фазы N» / «коллайдер пересёкся с целью». Это сигнал, не запись.

Граница простая: View может содержать сколько угодно сложную внутреннюю реактивную логику, но не имеет права писать в данные напрямую. Изменение шины — только через контроллер, который владеет соответствующей моделью. Сложная анимация с коллбэками по фазам — это нормально и должно жить во View. Запись «прирост опыта» в PlayerModel из колбэка анимации — нарушение.

Что ломается, если нарушить:

  • Бизнес-правила дублируются в каждом экране, который их показывает.
  • Удаление или замена UI каскадом ломает игровую логику — она «жила» внутри view.
  • Невозможно выключить UI и убедиться, что игра работает — без UI ничего не работает.
  • A/B-тесты на оформлении требуют переписать игровые правила, а не только префабы.

4. Изменения должны быть явными, без скрытого поллинга

Механизм: ReactiveValue<T>IntData, BoolData, FloatData, StringData с явными подписками OnUpdate / OnUpdateData.

Что ломается, если нарушить:

  • Растёт нагрузка: десятки Update() каждый кадр сравнивают значения, которые меняются раз в секунду.
  • Реакция приходит с задержкой в кадр и нестабильно — порядок Update() не гарантирован.
  • Теряются промежуточные значения: если за кадр поле поменялось дважды, увидим только последнее.
  • Невозможно ответить на вопрос «кто отреагирует на это изменение» — нет явного списка подписчиков, всё разбросано по Update().

Отдельный случай — быстрые решения

«Временно подопру костылём, потом перепишу» — у этого правила нет механизма во фреймворке. Компилятор костыль не запретит, и это надо признать честно.

Vortex здесь работает иначе: он не запрещает костыли — он делает их видимыми.

Костыль в Vortex выглядит чужеродно. Он либо обходит шину, либо ломает поток Controller → Data → View, либо тащит прямую ссылку между системами. На code review такой код виден с первого взгляда, и решение «оставить как есть» становится осознанным выбором — а не следствием того, что никто не заметил.

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

Коротко

Vortex — это попытка построить архитектуру, в которой сложность проекта растёт линейно, а не лавиной. Где каждое нарушение правила имеет цену, известную заранее, и видно невооружённым глазом.