VanguardReMaster/docs/Development/CLIENT-SCENES-3D-ANIMATIONS.md

410 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Клиент: сцены, 3D и анимации — план доработок
**Дата:** 2026-10-30
**Анализ:** DeepSeek выполнен; код, сцены, префабы, SO и конфиги не менялись.
**Решения:** зафиксированы 2026-10-30 (раздел 8).
**Статус:** реализация не начата.
**Связанные документы:** `docs/client-visual-direction.md`,
`docs/3d-ux-backlog.md`, `docs/Development/ANIMATION-INVENTORY.md`,
`docs/Development/IMPLEMENTATION_PLAN.md` (TASK-2026-001…018).
Документ — приоритизированный бэклог по трём направлениям: разбиение на сцены,
3D-наполнение каждой сцены, оптимизация анимаций. Он не заменяет
`IMPLEMENTATION_PLAN.md`: при старте работ пункты переносятся туда отдельными
TASK с полями «Реализация» и «Проверка» по `AGENT-WORKFLOW.md`.
Первый блок (A1 + C1–C3) оформлен как `TASK-2026-019` в
`docs/Development/IMPLEMENTATION_PLAN.md`; код ещё не менялся.
## 0. Границы
- без новых пакетов: DOTween/UniTask запрещены (решение 2026-10-24);
- правила, сервер, протокол и YAML-контент не меняются;
- `CardMoveAnimator` остаётся единственным планировщиком перемещений карт;
- сцены и префабы меняются только в рамках конкретного пункта, не попутно;
- геймплей вручную проверяет пользователь.
## 1. Текущее состояние (факты)
### 1.1 Сцены
| Сцена | В `.unity` | Создаётся кодом | 3D-содержимое |
|---|---|---|---|
| `Boot` | камера, свет, Global Volume | контроллера и UI нет; `VanguardApp` из `RuntimeInitializeOnLoadMethod` | нет |
| `Login` | контроллер, камера, свет | Canvas + `LoginView`, `SceneEnvironment` | пол, 14 кубов (колонны, неон, задник, стол, 4 карты, 2 панели), 2 point light |
| `Lobby` | контроллер, камера, свет | Canvas + `MetaView`/очередь, `SceneEnvironment` | пол, 2 колонны, 2 стола, стена, световая полоса |
| `DeckBuilder` | контроллер, камера, свет | Canvas + `DeckBuilderView`, `SceneEnvironment` | пол, 3 куба |
| `Battle` | контроллер, камера, свет | Canvas + `BattleView`, `SceneEnvironment`, `GameBoard3D` | 4 фоновых куба + процедурное поле |
| `GameBoard3D` | камера/свет из конфига, `PlaymatSandbox` | песочница поля без сервера | поле целиком |
Переходы: `SceneFlow.LoadAsync` → `LoadSceneAsync(Single)`, ожидание `isDone`,
без fade, прогресса и `allowSceneActivation` (`SceneFlow.cs:63-80`).
`GameBoard3D.unity` отсутствует в Build Settings; константа `PlaymatSandbox`
метода загрузки не имеет.
### 1.2 3D и ассеты
- Окружение одно на все сцены: `SceneEnvironment.Build(SceneTheme)` +
`switch` по теме (`SceneEnvironment.cs:22`, декорации `:110-159`).
- Камера и свет заданы дважды: `SceneBuilder` пишет их в `.unity`, а
`SceneEnvironment` перезаписывает в рантайме другими значениями
(FOV 60 в сцене против 46/52 в рантайме; интенсивность света 0.9 против 1.15).
- Каждый примитив фона получает собственный `Material` в рантайме
(`SceneEnvironment.cs:177-202`); ассетов `.mat` в проекте нет.
- Ассеты: 246 `.jpg` арта карт в `Resources/CardArt`, 0 `.fbx`, 0 `.mat`,
0 `.shader`, 0 `.anim`, 0 `.controller`, 4 prefab-оверлея.
- Addressables `4.1.1` установлен, но `Addressables.*` в коде не используется.
- `AudioListener` есть только в `Boot` (в остальных сценах — 0).
- Global Volume есть только в `Boot`; в сценах-экранах нет
`UniversalAdditionalCameraData` — нужна визуальная проверка пост-обработки.
- `LODGroup`, baked-свет и occlusion отсутствуют; материалы и меши стола
создаются процедурно (`BoardMaterials`, `BoardMeshLibrary`).
### 1.3 Анимации
Уже сделано и переделывать не нужно: пул `Card3D`, `sharedMaterial` +
`MaterialPropertyBlock`, кеши мешей и текстур, единый `Tick` из
`GameBoard3D.Update`, FPS-независимая интерполяция (экспонента и smoothstep
на `Time.unscaledDeltaTime`), отсутствие `Update` на каждой карте.
Проблемы, подтверждённые кодом:
- `UpdateBoostArrows` создаёт `new List<(Vector3, Vector3)>` каждый кадр, а
`BattleCard` — `Players.FirstOrDefault(lambda)` 2–4 раза за кадр; якоря
стрелок ищутся линейным перебором `_cards.Values`.
- Стрелки обновляются дважды за кадр: `_arrowDirector.Tick` и
`UpdateAttackArrowPoints` (`GameBoard3D.Update`), плюс
`AttackArrowView.ApplyColor` создаёт массив `new[] { _shaft, _head }`.
- `UpdateHandView` пересчитывает позы обеих рук каждый кадр и вызывается
повторно из `Render`, hover и колбэков (`GameBoard3D.cs:183, 511, 607,
732, 1163, 2230, 2372`) — в одном кадре возможен двойной `SetPose`.
- Строки в кадре: таймер и банк (`BattleView.UpdateClock`), `Describe()` у
`InputLockService` при активном локе, `LocalizedText` для каждого лейбла.
- Hover/drag используют `Physics.RaycastAll` с сортировкой через лямбду и
`GetComponentInParent` на каждый хит.
- `AnimationConfig` содержит ~15 полей, которые код не читает
(`ANIMATION-INVENTORY.md`, раздел I): риск неверной настройки.
## 2. Сводка приоритетов
| ID | Что | Приоритет | Зачем |
|---|---|---|---|
| A1 | Загрузочная сцена и fade | P0 | убрать вспышку при смене сцены |
| A2 | Песочница в Build Settings | P0 | `RunPlaymat` и песочница в билде |
| A3 | Один владелец камеры/света | P0 | убрать рассинхрон `.unity` и рантайма |
| A4 | `SceneProfile` на сцену | P1 | настройка сцены без правки кода |
| A5 | AudioListener на персистентном объекте | P1 | звук во всех сценах |
| A6 | Пост-обработка в сценах-экранах | P1 | одинаковый look |
| A7 | Additive-слой общих сервисов | P2 | не пересоздавать Canvas |
| A8 | SceneSmoke для DeckBuilder/Battle | P2 | покрытие смоуком |
| B1 | Общие материалы вместо `new Material` | P0 | батчинг, редактируемость |
| B2 | Login: полноценная 3D-сцена | P0 | первый экран |
| B3 | Lobby и DeckBuilder: 3D-сцены | P1 | сейчас 6 и 3 куба |
| B4 | Battle: окружение вокруг поля | P1 | задник/свет без правки поля |
| B5 | Prefab-экраны UI | P1 | уйти от code-generated |
| B6 | Addressables для арта | P1 | разгрузить `Resources` |
| B7 | Light/reflection probes, baked-свет | P2 | качество света |
| B8 | Живое окружение | P2 | неон, частицы, скайбокс |
| C1 | Аллокации в стрелках | P0 | 0 B/кадр в бою |
| C2 | Один тик руки | P0 | убрать двойной `SetPose` |
| C3 | Строки не каждый кадр | P0 | GC и цена UI |
| C4 | Событийный `InputLock` | P1 | паузы по факту, не по таймеру |
| C5 | Raycast без аллокаций | P1 | hover/drag |
| C6 | Синхронизировать `AnimationConfig` | P1 | убрать мёртвые поля |
| C7 | Профилирование и бюджет кадра | P2 | 60 fps, метрики |
| C8 | Анимационный LOD | P2 | не тикать невидимое |
## 3. A — Разбиение на сцены
- [ ] **A1. P0 — Загрузочная сцена и плавный переход.**
Файлы: `Runtime/App/SceneFlow.cs`, новый `Runtime/UI/LoadingOverlay.cs`,
`Runtime/App/VanguardApp.cs`, `Scenes/Boot.unity`.
Сделать: `Boot` становится загрузочной сценой — прогревает `VanguardApp` и
сервисы, показывает прогресс `operation.progress` и fade 0.15–0.25 с, затем
грузит `Login`; для переходов между экранами используется тот же overlay.
Зачем: переход идёт без fade и прогресса — сцена меняется одним кадром,
экран загрузки не показывается.
Критерий: Login → Lobby → Battle без вспышки; прогресс виден на первом
входе в Lobby.
Зависит: нет.
- [ ] **A2. P0 — Песочница поля в сборке.**
Файлы: `ProjectSettings/EditorBuildSettings.asset` (через
`Editor/SceneBuilder.cs`), `Runtime/App/SceneFlow.cs`.
Сделать: добавить `Assets/Vanguard/Scenes/GameBoard3D.unity` после `Battle`,
метод `LoadPlaymatAsync`, доступ только в dev-сборке.
Зачем: константа `PlaymatSandbox` не имеет загрузчика, сцены нет в билде.
Критерий: песочница открывается в standalone, `SceneSmoke.RunPlaymat`
проходит.
Зависит: A1.
- [ ] **A3. P0 — Один владелец камеры и света.**
Файлы: `Editor/SceneBuilder.cs`, `Runtime/App/SceneEnvironment.cs`,
`*.unity`.
Сделать: выбрать одну модель — сцены хранят камеру/свет, а
`SceneEnvironment` их не перезаписывает, либо сцены пустые и всё создаёт
окружение. Зафиксировать в `client-visual-direction.md`.
Зачем: сейчас FOV 60 в сцене против 46/52 в рантайме, свет 0.9 против 1.15;
правки в сцене не действуют.
Критерий: значения в инспекторе совпадают с рантаймом;
`SceneBuilder.BuildAll` не создаёт расхождений.
Зависит: нет (A4 опирается на это решение).
- [ ] **A4. P1 — `SceneProfile` (ScriptableObject) на сцену.**
Файлы: новый `Runtime/App/SceneProfile.cs` + 4 ассета,
`Runtime/App/SceneEnvironment.cs`, контроллеры сцен.
Сделать: вынести камеру, свет, палитру, туман, декор и пост в профиль;
`Build(SceneProfile)` вместо `switch(SceneTheme)`.
Зачем: настройка сцены сейчас требует правки кода; поля
`ScenePalette.Zone/EnemyZone` не читаются.
Критерий: внешний вид сцены меняется правкой ассета, без компиляции.
Зависит: A3.
- [ ] **A5. P1 — AudioListener и аудио-корень.**
Файлы: `Runtime/App/VanguardApp.cs`, `*.unity`.
Сделать: один `AudioListener` на персистентном объекте, убрать дубль из
`Boot`-камеры.
Зачем: единственный listener в `Boot` уничтожается при Single-загрузке —
при добавлении звука его не будет слышно.
Критерий: ровно один `AudioListener` в любой сцене, звук слышен.
Зависит: нет.
- [ ] **A6. P1 — Пост-обработка в сценах-экранах.**
Файлы: `*.unity`, `Assets/Settings/SampleSceneProfile.asset`.
Сделать: `UniversalAdditionalCameraData` и Volume (или общий профиль через
Global Settings) на всех экранах; сверить bloom/vignette/tonemapping.
Зачем: Global Volume есть только в `Boot`, в остальных сценах
пост-обработка, вероятно, не применяется.
Критерий: одинаковый look `Boot`/`Login`/`Lobby`/`DeckBuilder`/`Battle`.
Зависит: A3.
- [ ] **A7. P2 — Additive-слой общих объектов.**
Сделать: Canvas оверлеев, EventSystem и аудио грузить один раз и не
пересоздавать при смене экрана.
Зачем: каждый экран заново строит Canvas через `UiKit.CreateCanvas`.
Критерий: Lobby ↔ DeckBuilder без пересоздания оверлеев и мигания.
Зависит: A1.
- [ ] **A8. P2 — Расширить `SceneSmoke`.**
Файлы: `Editor/SceneSmoke.cs`.
Сделать: проверять `DeckBuilder` и `Battle` (контроллер, окружение, камера,
`AudioListener`), сверять FOV/интенсивность света с профилем.
Зачем: сейчас проверяются только `Login`/`Lobby`.
Критерий: смоук падает при потере окружения или listener в любой сцене.
Зависит: A3–A6.
## 4. B — 3D в каждой сцене
- [ ] **B1. P0 — Общие материалы вместо `new Material`.**
Файлы: `Runtime/App/SceneEnvironment.cs`, новый `Assets/Vanguard/Art/…`.
Сделать: 3–5 общих URP-материалов ассетами; фон и декор используют их.
Зачем: сейчас каждый примитив получает инстанс материала в рантайме
(`:177-202`), это ломает батчинг и не редактируется в инспекторе.
Критерий: Frame Debugger без лишних `SetPass`, SRP Batcher активен.
Зависит: A4.
- [ ] **B2. P0 — Login: полноценная 3D-сцена.**
Сделать: authored-стол, 4–6 карт, неоновые направляющие, задник; свет и
камера из профиля; геометрия — префабы, а не код.
Зачем: первый экран сейчас собран из 14 кубов в рантайме.
Критерий: визуально читаемый стол; в рантайме не создаётся статическая
геометрия.
Зависит: B1, A4, референс экрана от пользователя.
- [ ] **B3. P1 — Lobby и DeckBuilder: 3D-сцены.**
Сделать: Lobby — зал с постаментом/витриной и световыми акцентами;
DeckBuilder — стол с матом, стопками и подсветкой зон.
Зачем: сейчас 6 и 3 куба, экраны визуально пустые.
Критерий: у каждой сцены своя композиция, а не общий `switch` с кубами.
Зависит: B1, A4, референс экрана от пользователя.
- [ ] **B4. P1 — Battle: окружение вокруг поля.**
Сделать: задник, постамент, световые акценты вокруг `GameBoard3D`, не
вмешиваясь в геометрию поля и `PlaymatLayout`.
Зачем: сейчас 4 фоновых куба; поле выглядит висящим в пустоте.
Критерий: поле читается как стол в помещении; `GameBoard3DTests` без
изменений.
Зависит: B1, A4, референс боевого экрана от пользователя.
- [ ] **B5. P1 — Prefab-экраны UI.**
Сделать: перенести code-generated панели в префабы (по одному экрану за
шаг), `UiKit` оставить фабрикой для мелких элементов.
Зачем: `client-visual-direction.md` фиксирует это как следующий шаг;
сейчас UI собирается кодом и не редактируется дизайнером.
Критерий: экран собирается из префаба, поведение и тесты не меняются.
Зависит: приёмка TASK-2026-003 (HUD), иначе конфликт правок.
- [ ] **B6. P1 — Addressables для арта.**
Сделать: инициализировать Addressables-группы, перенести арт карт из
`Resources/CardArt` в адресуемые группы, грузить по `cardId`
(`CardImageService`), оставить процедурный fallback по клану.
Зачем: 246 `.jpg` лежат в `Resources` и грузятся целиком; пакет установлен,
но не используется.
Критерий: арт грузится адресуемо и выгружается, `Resources` не растёт;
контракт `BoardArt`/`CardArt` не меняется.
Зависит: нет; модели карт и стола остаются процедурными (решение 8.1).
- [ ] **B7. P2 — Light/reflection probes и baked-свет.**
Сделать: запечь статику сцен, добавить probes для карт и стола.
Зачем: `m_LightingDataAsset` пустой, освещение только realtime.
Критерий: статичный свет не считается каждый кадр, карты получают
корректные отражения.
Зависит: B2–B4 (финальная геометрия).
- [ ] **B8. P2 — Живое окружение.**
Сделать: анимация неона, лёгкие частицы, скайбокс/градиент, дыхание камеры
в Login/Lobby.
Зачем: окружение статично; при этом правила запрещают DOTween — только
существующий ручной тик.
Критерий: эффекты не блокируют ввод и не добавляют аллокаций в кадре.
Зависит: B2–B4, C1–C3.
## 5. C — Анимации: плавность и стоимость кадра
- [ ] **C1. P0 — Убрать покадровые аллокации в стрелках.**
Файлы: `Board3D/View/GameBoard3D.cs` (`UpdateBoostArrows`,
`UpdateAttackArrowPoints`, `BattleCard`), `Animation/ArrowDirector.cs`,
`View/AttackArrowView.cs`.
Сделать: кешировать список пар, заменить LINQ на индексированные циклы,
кешировать якоря на `Render`, обновлять геометрию один раз за кадр, убрать
`new[] { _shaft, _head }` в `ApplyColor`.
Зачем: `new List` каждый кадр, 2–4 LINQ-делегата, 4 `UpdateGeometry` на
стрелку за кадр.
Критерий: 0 B/кадр в бою без движения (Profiler), визуал не меняется.
Зависит: нет.
- [ ] **C2. P0 — Один тик и один `SetPose` руки.**
Файлы: `Board3D/View/GameBoard3D.cs` (`UpdateHandView`), `Board3D/View/Card3D.cs`.
Сделать: dirty-флаг на изменение состава руки, камеры, hover и staged-списка;
убрать повторный вызов из `Render`/колбэков, если позы уже посчитаны.
Зачем: позы обеих рук пересчитываются каждый кадр, возможен двойной
`SetPose` в одном кадре.
Критерий: при неподвижной камере и руке расчёт не выполняется; hover,
добор, муллиган и reflow без регрессий.
Зависит: C1 (общий бюджет кадра).
- [ ] **C3. P0 — Строки не каждый кадр.**
Файлы: `UI/BattleView.cs` (`UpdateClock`), `UI/Localization.cs`
(`LocalizedText`), `Battle/InputLockService.cs` (`Describe`).
Сделать: обновлять текст только при изменении значения; кешировать перевод
до смены языка; убрать `Describe`/LINQ из кадрового пути.
Зачем: таймер и банк пишутся каждый кадр, `LocalizedText` проходит
`Loc.Translate` на каждый лейбл, при локе — `string.Join`/LINQ.
Критерий: в покое нет аллокаций от UI; переключение RU/EN по-прежнему
мгновенное.
Зависит: нет.
- [ ] **C4. P1 — Событийный `InputLock` вместо таймера.**
Файлы: `Board3D/Animation/CardMoveAnimator.cs`,
`Board3D/View/GameBoard3D.cs`, `Battle/InputLockService.cs`.
Сделать: снимать `board`-лок по завершении очереди и stagger, таймер
оставить только страховкой.
Зачем: лок держится `cardMoveDuration * 3 + tail` и не совпадает с фактом
окончания анимаций — лишние паузы или преждевременный ввод.
Критерий: ввод открывается сразу после последней карты; зависаний нет
(`CancelAll`, reconnect, отмена).
Зависит: C2.
- [ ] **C5. P1 — Raycast без аллокаций.**
Файлы: `Board3D/View/GameBoard3D.cs` (`UpdateHandHover`, drag, targeting).
Сделать: `RaycastNonAlloc` с переиспользуемым буфером, выбор ближайшего без
`Array.Sort` с лямбдой, кеш `GetComponentInParent`.
Зачем: `RaycastAll` + сортировка + `GetComponentInParent` на каждый хит
каждый кадр при активном указателе.
Критерий: наведение и drag без аллокаций; выбор карты не изменился.
Зависит: C1.
- [ ] **C6. P1 — Синхронизировать `AnimationConfig` с кодом.**
Файлы: `Battle/AnimationConfig.cs`, `Resources/AnimationConfig.asset`,
`docs/Development/ANIMATION-INVENTORY.md`.
Сделать: удалить или явно пометить поля, которые не читаются или читаются
не по назначению: `cardFlipDuration`, `callMoveDuration`,
`cameraRezoomDuration`, `arrowHideDuration`, `retireDuration`,
`attackDeclare` и паузы feedback — не читаются; `cardMoveDuration` —
только таймаут board-lock; `arrowFadeDuration` и `boostThickness` дублируют
`ArrowConfig`. Asset привести к фактическим значениям.
Зачем: ~15 мёртвых полей — источник неверной настройки.
Критерий: каждое поле читается кодом или удалено; тест на загрузку ассета.
Зависит: нет.
- [ ] **C7. P2 — Профилирование и бюджет кадра.**
Сделать: маркеры Profiler в `CardMoveAnimator`, `GameBoard3D.Update`,
`UpdateHandView`; зафиксировать бюджет (60 fps, доска ≤ 1 мс на референсном
ПК); выровнять `shadowDistance` (40 в QualitySettings против 50 в URP asset);
MSAA оставить выключенным (решение 2026-10-30) и проверить края карт с TAA.
Зачем: сейчас метрик нет, настройки противоречат друг другу.
Критерий: отчёт «до/после» с числами, зафиксированный бюджет в документе.
Зависит: C1–C5.
- [ ] **C8. P2 — Анимационный LOD.**
Сделать: не тикать карты вне экрана и стопки, обновлять их по dirty;
уменьшить частоту обновления рук противника и дальних стопок.
Зачем: `GameBoard3D.Update` тикает все карты в `_cards` каждый кадр.
Критерий: то же визуальное поведение, меньше работы в кадре при 40+ картах.
Зависит: C2, C7.
## 6. Порядок работ
1. **Старт (P0):** A1 → C1 → C2 → C3 → A2. Загрузочная `Boot` и чистый кадр
дают сразу видимый эффект и не конфликтуют с приёмкой HUD.
2. **3D-база (P0):** A3 → A4 → B1 → B6.
3. **Экраны (P1):** B2 → A5 → A6 → B3 → B4; B2–B4 стартуют после референсов.
4. **Анимации (P1):** C4 → C5 → C6.
5. **UI (P1):** B5 — после приёмки TASK-2026-003.
6. **Полировка (P2):** A7 → A8 → B7 → B8 → C7 → C8.
Правило: этапы 1–2 не меняют компоновку HUD из TASK-2026-003, пока идёт его
ручная приёмка; в `BattleView` допускаются только точечные правки вне
разметки (например, `UpdateClock` в C3).
## 7. Проверки
- Клиентские EditMode в копии проекта без `Library` (команда в
`docs/SESSION-STATE.md`).
- `SceneSmoke.Run` и `SceneSmoke.RunPlaymat` после каждого блока сцен.
- `SceneBuilder.BuildAll` — сцены пересобираются без ошибок, `.meta`
контроллеров на месте.
- Standalone-сборка на копии: переходы, пост-обработка, звук, песочница.
- Profiler: Allocation Call Stacks (0 B/кадр в бою без движения), Frame
Debugger для батчинга.
- Регрессии: drag&drop, ride/call/move, добор, муллиган, trigger, урон,
hover руки.
- Ручная приёмка визуала и геймплея — пользователем.
## 8. Решения и риски
**Зафиксировано пользователем 2026-10-30:**
1. **3D-арт.** Работаем на примитивах с нормальными URP-материалами; закупка
и генерация моделей в объём не входят. Модели карты/стола можно добавить
позже без смены контрактов `BoardArt`/`CardArt`.
2. **`Boot`.** Остаётся первой сценой и становится загрузочной: прогрев
сервисов, прогресс, fade, затем `Login` (входит в A1).
3. **Addressables.** Арт переводим сейчас, отдельным шагом (B6), не дожидаясь
prefab-экранов.
4. **MSAA.** Остаётся выключенным; края карт сглаживаются TAA. Пересматривать
только по результатам C7.
5. **Референсы.** Пользователь готовит мокапы для всех экранов: `Login`,
`Lobby`, `DeckBuilder` (включая экран колод), `Battle`. B2–B4 стартуют
после получения соответствующего референса.
6. **Первый блок.** A1 + C1–C3.
**Риски:**
- правки сцен ломают GUID-ссылки — сцены пересобираются `SceneBuilder`,
копировать `.meta` вместе со скриптами;
- перенос UI в префабы (B5) конфликтует с TASK-2026-003, пока HUD не принят;
- `new Material` в рантайме нельзя заменить на ассеты, пока не решён A3/A4
(кто владеет материалами сцены);
- референсы для `Battle` могут потребовать правок HUD из TASK-2026-003 —
тогда это отдельная задача, а не попутная правка.
## 9. Вне объёма
- боевые правила, сервер, протокол, баланс, YAML-контент;
- HUD по мокапу (TASK-2026-003) и боевые анимации из
`docs/3d-ux-backlog.md` (cost overlay, attack resolution queue,
world-space Power/Shield);
- звук и настройки как отдельная система (кроме A5 — listener);
- лаунчер WPF и сборка релиза.