# Клиент: сцены, 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 и сборка релиза.