29 KiB
Клиент: сцены, 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, в остальных сценах пост-обработка, вероятно, не применяется. Критерий: одинаковый lookBoot/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-делегата, 4UpdateGeometryна стрелку за кадр. Критерий: 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. Порядок работ
- Старт (P0): A1 → C1 → C2 → C3 → A2. Загрузочная
Bootи чистый кадр дают сразу видимый эффект и не конфликтуют с приёмкой HUD. - 3D-база (P0): A3 → A4 → B1 → B6.
- Экраны (P1): B2 → A5 → A6 → B3 → B4; B2–B4 стартуют после референсов.
- Анимации (P1): C4 → C5 → C6.
- UI (P1): B5 — после приёмки TASK-2026-003.
- Полировка (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:
- 3D-арт. Работаем на примитивах с нормальными URP-материалами; закупка
и генерация моделей в объём не входят. Модели карты/стола можно добавить
позже без смены контрактов
BoardArt/CardArt. Boot. Остаётся первой сценой и становится загрузочной: прогрев сервисов, прогресс, fade, затемLogin(входит в A1).- Addressables. Арт переводим сейчас, отдельным шагом (B6), не дожидаясь prefab-экранов.
- MSAA. Остаётся выключенным; края карт сглаживаются TAA. Пересматривать только по результатам C7.
- Референсы. Пользователь готовит мокапы для всех экранов:
Login,Lobby,DeckBuilder(включая экран колод),Battle. B2–B4 стартуют после получения соответствующего референса. - Первый блок. 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 и сборка релиза.