Статус: Ready
Утверждённый технологический стек и архитектурные решения проекта. Основано на анализе концепции, дизайн-доков и разборе предыдущего проекта автора (FewSeconds-ManyDeaths-Unity) — см. раздел «Уроки прошлого проекта».
Контекст решений
| Параметр | Значение | Влияние на стек |
|---|---|---|
| Жанр | Ко-оп рогалик-автобатлер 1–4 игрока | Host-authoritative сеть, без античита |
| Модель боя | Реальное время + пауза (не пошагово) | Тиковая симуляция с фиксированным шагом |
| Поле боя | Непрерывное 2D (не клеточное) | Своя детерминированная математика, без Unity-физики |
| Платформа | Steam (PC), глобальный релиз | Steamworks, локализация, Steam Cloud |
| Мультиплеер | Ко-оп; PvP «с друзьями» возможен позже | Детерминируемая симуляция, но без fixed-point/lockstep сейчас |
| Команда | Соло-код; арт/UX — заказ; музыка — Suno (платный тир!) | Минимум кастомной инфраструктуры, максимум готовых решений |
| Цель | Вертикальный срез к фестивалю «Играм быть» (окт. 2026) на правильной архитектуре | Тяжёлые системы — каркасом сейчас, наполнение позже |
Технологический стек (фазированно)
Фаза 1 — Ядро (закладываем сейчас)
| Пакет | Назначение |
|---|---|
| VContainer | DI-контейнер. Композиционные корни: bootstrap → забег → бой. Замена синглтонам. |
| UniTask | Zero-alloc async/await: тиковый цикл боя, кулдауны, загрузки. |
| MessagePipe | Типизированный pub/sub EventBus через DI. Замена глобальному GlobalEventSystem. |
| LitMotion | Zero-alloc твины: UI, HP-бары, damage numbers, фидбэк. |
| Odin Inspector | Редакторы SO-контента, валидация, дропдауны для [SerializeReference]-поведения. |
| Newtonsoft.Json | Сериализация DTO сейвов (через Easy Save). |
| Multiplayer Play Mode (MPPM) | Тест ко-опа в редакторе (до 4 виртуальных игроков). Замена ParrelSync для Unity 6. |
Фаза 2 — Контент (стадии 2–3 роадмапа)
| Пакет | Назначение | Примечание |
|---|---|---|
| Addressables | Загрузка контента, основа для локализации, DLC после фестиваля | Вводить, когда контента станет много |
| Unity Localization | Локализация (мин. EN + RU) | Ключи закладывать в SO сразу; пакет — на этой фазе |
| Easy Save 3 | Плумбинг сохранений на диск/Steam Cloud | Архитектуру DTO продумываем сами |
| Netcode for GameObjects (NGO) | Сетевой слой | Каркас раньше, включение фичи — стадия 3 |
| Facepunch.Steamworks + Facepunch transport | Steam-интеграция и транспорт (relay/NAT бесплатно) | Автор уже работал с этой связкой |
| R3 (опционально) | Реактивные биндинги UI, если MVVM начнёт болеть | Преемник UniRx (UniRx — легаси, не брать) |
Фаза 3 — Полировка (стадия 4)
| Пакет | Назначение | Примечание |
|---|---|---|
| FMOD | Адаптивная боевая музыка (слои по интенсивности) | Усложнит GameCI — заложить время. До этого — Unity Audio за интерфейсом IAudioService |
Отвергнутые варианты: Zenject (тяжелее VContainer), Mirror/FishNet (NGO достаточно для ко-опа и уже знаком автору), UniRx (легаси → R3), DOTween (LitMotion новее и zero-alloc), Steamworks.NET (Facepunch удобнее), ParrelSync (MPPM в Unity 6).
Обоснование ключевых решений
| Решение | Почему |
|---|---|
| NGO (не Mirror/FishNet) | Ко-оп host-authoritative; автор уже работал с NGO + Facepunch в прошлом проекте. FishNet мощнее (rollback/prediction), но это для соревновательного PvP — у нас PvP «с друзьями», без античита. |
| MPPM (не ParrelSync) | Unity 6 даёт Multiplayer Play Mode (виртуальные игроки в редакторе); ParrelSync (клон папки) стал легаси-обходом. |
| R3 (не UniRx) | UniRx — легаси; R3 — официальный преемник (Cysharp). Брать точечно для реактивных биндингов UI. |
| Easy Save 3 (не ручной JSON) | Соло-разработчику убирает boilerplate сериализации/шифрования. Формат сейва (DTO + строковые ID) проектируем сами. |
| UI Toolkit + MVVM | Биндинги UI Toolkit созданы под MVVM; гигантский конфиг-UI AI-редактора требует дизайн-системы. uGUI-гибрид — для боевого world-space HUD (бары, damage numbers). |
| LitMotion (не DOTween) | Новее, zero-alloc, из той же экосистемы (Cysharp). |
MVP vs MVVM (разница): в MVP связь View↔данные обновляется вручную через Presenter (проще, привычнее в Unity, хорош для uGUI/world-space). В MVVM View подписан на наблюдаемые свойства ViewModel и обновляется сам (нужны биндинги — UI Toolkit или R3). Выбран MVVM на UI Toolkit; MVP-стиль уместен для uGUI-HUD.
Ключевой принцип «один механизм — три фичи»: детерминированная симуляция + лог команд + сид дают сразу реплеи, сравнение AI-профилей и синхронный мультиплеер. Поэтому детерминизм — несущая стена, а не страховка под мультиплеер.
UI-воркфлоу: связка Figma → UI Toolkit (UXML/USS) рабочая (есть Figma MCP + Code Connect); под AI-редактор обязательна дизайн-система (переиспользуемые компоненты: Filter-чип, Score-слайдер с min/max, список целей).
Референсы механик: Gladiabots и FF12 Gambits (программируемое поведение юнитов), Teamfight Manager (читаемый автобой), Skul (реликвии-черепа), Slay the Spire (структура карты), Across the Obelisk / BG3 (ко-оп-распределение).
Архитектурные принципы
- Разделение симуляции и презентации. Боевая логика (
Combat) — чистые C#-данные/правила, безMonoBehaviourвнутри и без обращений к UI. Презентация (спрайты, бары, числа) только читает состояние и реагирует на события. - Инверсия зависимостей через DI. Никаких
static instance-синглтонов иFind*ObjectOfTypeв рантайме. Зависимости инъектируются VContainer’ом. - Детерминизм. Весь рандом — через инъектируемый
IRngService(seeded). ГлобальныйUnityEngine.Randomзапрещён в игровой логике. - Команды как единица действия. Действия игрока = команды. Это одновременно даёт replay, воспроизводимость по сиду и синхронизацию в мультиплеере.
- SO — иммутабельные определения, рантайм-стейт — в POCO. SO никогда не мутируется в рантайме.
- Границы сборок. Зависимости только вниз по графу (см. 1. Сборки (Assembly Definitions)).
Паттерны
| Паттерн | Где применяем |
|---|---|
| DI (VContainer) | Везде вместо синглтонов. Композиционные корни на bootstrap/забег/бой. |
| State Machine | Флоу игры (Карта → Подготовка → Бой → Награды) и состояние боя/юнита. |
| Command | Действия игрока, replay, синхронизация по сети, воспроизводимость по сиду. |
| EventBus (MessagePipe) | Развязка Combat → UI/VFX/Audio. Подписки с дисциплиной отписки (через DI lifetime). |
| Object Pool | UnityEngine.Pool.ObjectPool<T> для damage numbers, снарядов, VFX. |
Strategy / [SerializeReference] | Полиморфное поведение эффектов и способностей (см. ниже). |
| MVVM | UI на UI Toolkit (см. ниже). |
Боевая симуляция
- Поле боя — непрерывное 2D (не клеточное). Дальности, AOE, снаряды — в мировых координатах (радиусы, точки, swept-рейкасты). Размер юнита влияет на дальность атаки.
- Тиковая симуляция с фиксированным шагом ≈ 30 Гц (
dt≈ 0.0333с), отделена от частоты кадров. Снаряды — swept-проверкой (отрезок пути за тик), чтобы быстрые снаряды не «протыкали» цель между тиками. - Чистая C#-логика в сборке
Combat, управляется потоком команд. - Никакой Unity-физики (
Physics2D/Rigidbody2D) в симуляции — она недетерминирована (зависит от кадров, решателя, версии). Движение и попадания — ручная математика (интегрирование, circle-circle, рейкаст). Коллайдеры допустимы только в презентации (наведение курсора, хитбоксы для кликов). IRngService(seeded,System.Random/ xorshift) инъектируется в симуляцию. Все криты, лут, генерация — через него. ГлобальныйUnityEngine.Randomзапрещён.- Спатиальные запросы («кто в радиусе X», точки AOE) ускоряются внутренним spatial hash / uniform grid поверх непрерывных координат — поле остаётся непрерывным, сетка только ради перфоманса. AOE-точки сначала ищутся на юнитах и серединах между парами целей, а не сплошным перебором.
Чек-лист детерминизма
Симуляция детерминирована на одной машине (→ реплеи, сравнение AI-профилей) и при host-authoritative (→ мультиплеер), если:
- фиксированный
dt, безTime.deltaTimeв логике; - весь рандом через
IRngService; - детерминированный порядок итерации (не
foreachпоDictionary/HashSetбез сортировки); - ноль Unity-физики и ноль зависимости от порядка кадров / корутин.
Float ок: на одной машине и при host-authoritative float-математика детерминирована при соблюдении чек-листа. Fixed-point понадобился бы только для lockstep / кросс-машинной воспроизводимости в соревновательном PvP — не наш случай.
🔴 Первый рефактор:
DamageCalculator.RollCritиспользуетUnityEngine.Random.value— заменить наIRngServiceс EditMode-тестом на детерминизм. ЗаодноUnitDataподразумевает сетку («дальность в клетках») — пересмотреть под непрерывное поле (радиус, float).
AI «Сосудов» (Filter / Score / Override)
Поведение юнитов в бою — основной слой реиграбельности (профили, шаринг, сравнение до 4 штук, Workshop — сырые заметки в GDD 0.3. Черновик). Архитектура интерфейс-ориентированная — каждый интерфейс это реальная точка полиморфной вариативности, тестируемая и сериализуемая:
ITargetFilter— кого вообще можно рассматривать (живой, враг, в радиусе, без эффекта X, из списка).ITargetScorer— насколько цель хороша (ближе / ниже HP / ниже броня / +за цель из списка), с min/max-клампом.IOverrideRule— что перебивает обычный выбор (self HP < 10% → отступать; союзник < 15% → хилить; добить < 5%).IPositioningMode— режим передвижения (к цели, стоять, держаться у союзника с гистерезисом, смещение поддержки к лоу-HP).IAIAction— действие (бить / бежать / каст 1–2 / стоять).
AI-профиль = сериализуемые ДАННЫЕ, а не код: набор этих кирпичиков, собранный игроком в редакторе. Это даёт шаринг файлом / через Workshop, честное сравнение профилей и сохранение между забегами. При старте боя профиль «компилируется» один раз в объекты-стратегии (кэш), а не парсится каждый тик.
Исполнение на AI-тике: Override → Filter → Score → Action/Positioning → формируется AIIntent → команда в общий лог. AIContext (срез боя: я, союзники, враги, спатиальный индекс) передаётся in-параметром, без аллокаций.
Частоты: симуляция 30 Гц; переоценка AI 10 Гц базово, со stagger (юниты размазаны по окну, без пика CPU) и событийными прерываниями (цель умерла / вышла из фильтра / крупный урон / сработал Override → немедленная переоценка).
Объём к фестивалю уточняется: движок Filter/Score/Override — полноценный сразу; полный редактор-конструктор (classic/extra-режимы), шаринг, сравнение, полигон, Workshop — могут переехать на post-release («не горит»).
Дисциплина интерфейсов: интерфейс ставим там, где есть несколько реализаций или граница для теста/мока/подмены. Не плодим
IFoo→Foo1:1 — это засоряет. Интерфейсы на швах, конкретные типы внутри.
Моделирование контента (реликвии, способности, эффекты)
Подход: «данные в SO + поведение в коде».
- SO = только данные-определения (иммутабельный контент): цифры, ссылки, ключи локализации, список применяемых эффектов.
- Поведение = полиморфные plain-C# классы через
[SerializeReference](IEffect,ISkillBehavior), вложенные в SO. Odin даёт удобный выбор конкретного типа в инспекторе. - Логику исполняет боевая симуляция, передавая эффектам
IRngService. Эффект сам по себе не дёргаетUnityEngine.Random. - Рантайм-состояние — в POCO (
RuntimeUnit,RuntimeEffect). SO не мутируется и не клонируется как стейт.
Это сознательный уход от прошлого подхода «один SO/скрипт с логикой на каждый скилл», который вёл к взрыву классов и протеканию рантайм-данных в SO.
UI
- Основа: UI Toolkit + MVVM. ViewModel держит наблюдаемые свойства, View подписан через биндинги.
- Гибрид: для боевого HUD и world-space элементов (бары над юнитами, damage numbers), где UI Toolkit неудобен, допускается uGUI.
- При усложнении биндингов — точечно подключить R3.
Мультиплеер
- Модель: host-authoritative. Хост — источник истины, клиенты шлют команды-интенты. Античит не нужен (ко-оп / PvP «с друзьями»); приоритет — комфорт игроков.
- Сеть: NGO + Facepunch transport. Steam relay/NAT-punch бесплатно, выделенный сервер не нужен.
- Тестирование: MPPM (виртуальные игроки в редакторе).
- Host migration — прагматичный вариант (не бесшовный): при выходе хоста сессия завершается, последний автосейв расшарен (Steam Cloud), другой игрок перехостит и загрузит с последней точки. Опирается на модель сейвов (см. 2. Сохранения). Бесшовная runtime-миграция — post-release стретч, не к фестивалю.
- Реализация — последней (стадия 3), но код пишем «multiplayer-ready» с первого дня (симуляция отделена, RNG инъектируем, действия — команды).
Сохранения
- Easy Save 3 для плумбинга (диск, шифрование, Unity-типы).
- Свой слой DTO (
RunSaveData,UnitSaveData): контент хранится по строковым ID ("relic_flame_sword"), а не ссылками на SO. - Steam Cloud — Auto-Cloud по маске файлов в Steamworks-партнёрке. Фиксированный путь сейвов задать с самого начала.
- Детали и точки автосейва — в 2. Сохранения.
Локализация
- Unity Localization — выбранное решение (мин. EN + RU).
- Сейчас (бесплатно по усилиям): в SO реликвий/перков/событий закладывать строковые ключи, а не хардкод-текст.
- Пакет интегрировать на стадии 2–3 (тянет Addressables, которые всё равно нужны).
Уроки прошлого проекта (FewSeconds-ManyDeaths)
Метрики по 191 файлу _Project и принятые контрмеры:
| Проблема в прошлом проекте | Масштаб | Решение в Guildmaster |
|---|---|---|
| Нет Assembly Definitions | 0 .asmdef (монолит) | Граф сборок Core←Units←Combat/Guild←UI уже есть ✅ |
| Синглтоны | 18 классов, 480 обращений к .instance | DI (VContainer) |
Find*ObjectOfType | 87 вызовов в 54 файлах, в т.ч. в геттерах | Инъекция зависимостей, явные ссылки |
Глобальный UnityEngine.Random | 17 вызовов; SessionSeed → Random.InitState глобально | IRngService (seeded, инъектируемый) |
| God-object стейт | CombatPlayerDataInSession: параллельные массивы, SO.CreateInstance для рантайма | POCO рантайм-стейт, SO иммутабельны |
| God-object EventBus | GlobalEventSystem: 60+ статических UnityEvent | MessagePipe через DI с дисциплиной отписки |
| Логика в SO/скриптах на каждый скилл | взрыв классов (SkillScript 186 стр. и т.п.) | Данные в SO + поведение через [SerializeReference] |
Главный вывод: прошлые проблемы шли не от незнания C#, а от отсутствия границ (asmdef) и инверсии зависимостей (DI). Оба пункта закрыты в фундаменте.
Дорожная карта внедрения
Все пакеты установлены (май 2026). Активация фич — по мере готовности систем.
| Пакет | Статус | Следующий шаг |
|---|---|---|
| VContainer | ✅ Установлен | Написать первый LifetimeScope (bootstrap) |
| UniTask | ✅ Установлен | Заменить корутины при написании тиковой симуляции |
| MessagePipe | ✅ Установлен | Зарегистрировать в LifetimeScope; заменить будущий EventBus |
| LitMotion | ✅ Установлен | Подключать при реализации UI-фидбэка |
| Odin Inspector | ✅ Установлен | Использовать для SO-контента и [SerializeReference]-поведений |
| Newtonsoft.Json | ✅ Установлен | Подключать при проектировании DTO сейвов |
| MPPM | ✅ Установлен | Настроить виртуальные игроки для кооп-тестирования |
| Addressables | ✅ Установлен | Вводить когда контента станет много (стадия 2–3) |
| Unity Localization | ✅ Установлен | Ключи в SO закладывать сразу; таблицы — стадия 2–3 |
| Easy Save 3 | ✅ Установлен | Спроектировать DTO-слой (RunSaveData, UnitSaveData) |
| NGO | ✅ Установлен | Каркас сети — стадия 3; код писать multiplayer-ready с первого дня |
| Facepunch.Steamworks | ✅ Установлен (DLL) | Инициализировать при добавлении Steam-фич |
| FMOD | ✅ Установлен | За интерфейсом IAudioService. Подключать на стадии 4 |
| R3 (опционально) | ⏳ Не установлен | Добавить если MVVM-биндинги начнут болеть |
Приоритеты к фестивалю «Играм быть» (окт. 2026):
- Рефактор
DamageCalculatorнаIRngService - Каркас тиковой симуляции + Command-слой (VContainer + UniTask + MessagePipe)
- Базовый UI (LitMotion + Odin)
- Кооп-тест через MPPM
- Steam + сохранения — стадия 3
Открытые вопросы
Suno: платный тир— решено: берётся платный тир (коммерческое использование). Раскрыть AI-контент в анкете Steam при публикации.Частота тиков— решено: симуляция 30 Гц, AI 10 Гц (stagger + событийные прерывания).- Нужен ли R3 на старте UI или достаточно ручных биндингов UI Toolkit — решить по факту боли.
- Объём AI-редактора к фестивалю (движок vs полный конструктор) — уточняется в процессе; допустим перенос на post-release.
- Точная модель стат-системы (~30 статов из
0.3. Черновик: слои base/flat/percent, источники модификаторов) — спроектировать перед боевым кодом.