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

31 KiB
Raw Blame History

Клиент: сцены, 3D и анимации — план доработок

Дата: 2026-10-30 Анализ: DeepSeek выполнен; код, сцены, префабы, SO и конфиги не менялись. Решения: зафиксированы 2026-10-30 (раздел 8). Статус: TASK-2026-021 (B2, Login) реализован и ждёт ручной приёмки; TASK-2026-019 (A1 + C1–C3) — план готов, код не начат. Связанные документы: 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: 2D-арт по референсу 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 по референсу: 2D-арт + неоновый UI. Решение 2026-10-31: фон — 2D key art на весь экран, 3D-окружение в Login не строится (исключение из правила «3D в каждой сцене», см. 8.7). Состав экрана, размеры и состояния — в TASK-2026-021 (docs/Development/IMPLEMENTATION_PLAN.md). Зачем: текущий Login — карточка 620×820 с полем сервера и кнопкой регистрации, композиция не соответствует референсу. Критерий: композиция совпадает с референсом, вход и регистрация работают. Зависит: арт-файл от пользователя; TASK-2026-021.

  • 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.
  7. Login — исключение из «3D в каждой сцене»: фон — 2D key art на весь экран, 3D-окружение в сцене Login не строится. Правило «3D» остаётся для Lobby, DeckBuilder и Battle.
  8. Регистрация — вторичная ссылка «Создать аккаунт» под кнопкой «Войти».
  9. Адрес сервера — без UI: только AppConfig/PlayerPrefs, поле из панели убирается.
  10. «Забыли пароль?» — ссылка есть, по клику заглушка «сброс не поддерживается»: серверного endpoint сброса нет, отдельная задача.
  11. Референсы. Login получен 2026-10-31 (референс в чате). Для остальных экранов ждём.

Риски:

  • правки сцен ломают 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 и сборка релиза.