Философия 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 — это попытка построить архитектуру, в которой сложность проекта растёт линейно, а не лавиной. Где каждое нарушение правила имеет цену, известную заранее, и видно невооружённым глазом.