Система экранов, окон и диалогов¶
Текущий этап: feature. Добавлены типизированные контексты и результаты диалогов, очередь и асинхронная загрузка Addressables поверх системы экранов из предыдущего этапа.
Владение¶
Boot создаёт один MenuRoot и сохраняет его между сценами. На корне находятся:
- FlowSystem: загрузка локаций, прогресс, маршруты и история возвратов.
- WindowSystem: экземпляры UI, Addressables handles, слои, приоритеты, доступность ввода и пауза.
- DialogSystem: очередь уведомлений/подтверждений и их результаты.
Singleton и собственного статического изменяемого состояния нет. Асинхронные UI API вызываются на основном потоке Unity; CancellationToken можно отменять извне. Нет Task.Run, блокирующего ожидания Addressables или runtime-сборки GameObject. Все окна и их иерархии — готовые префабы.
Классы и каталог¶
| Класс | Префаб | Загрузка |
|---|---|---|
| HudScreen | GameplayHud | Прямая ссылка |
| LoadingScreen | LoadingScreen | Прямая ссылка |
| MapScreen | MapScreen | Addressables |
| TankScreen | TankScreen | Addressables |
| UpgradesScreen | UpgradesScreen | Addressables |
| PassesScreen | PassesScreen | Addressables, экземпляр закреплён |
| ShopScreen | ShopScreen | Addressables |
| SettingsScreen | SettingsScreen | Addressables |
| BriefingScreen | MissionBriefing | Addressables |
| DetailsScreen | DetailScreen | Addressables |
| InventoryScreen | InventoryPopup | Addressables, экземпляр закреплён |
| ConfirmationScreen | ConfirmationScreen | Addressables, очередь |
| NotificationScreen | NotificationScreen | Addressables, очередь |
Все конкретные классы оканчиваются на Screen и наследуют GameScreen. ConfirmationScreen и NotificationScreen используют промежуточный типизированный DialogScreen
GameScreen управляет fade, масштабом, появлением/скрытием, CanvasGroup и подписками кнопок. Анимации используют unscaledDeltaTime и работают при timeScale = 0. OnShowing/OnHidden/OnClosing позволяют расширять жизненный цикл. При уничтожении снимаются обработчики и завершается ожидание скрытия.
FlowAction, FlowButton, FlowScreen и Dispatch удалены. Каждый экран связывает свои сериализованные Button с коллбеками. MissionMarker получает Action
Слои, ввод и пауза¶
Слои снизу вверх: Hud → Screens → Modal → Overlay. Внутри слоя большее значение priority располагается выше; при равных приоритетах учитывается последнее открытие.
BlocksLowerInput запрещает ввод всем окнам ниже. Это распространяется на HUD и джойстик, а не только на затемнение. PausesGameplay удерживает паузу, пока открыто хотя бы одно требующее её окно.
HoldGameplay() даёт IDisposable-резервирование паузы для асинхронной операции. HoldGameplay(true) также блокирует UI под ней. Оно освобождается при успехе, ошибке и отмене. Очередь удерживает одно резервирование на всю последовательность, включая загрузку и fade между диалогами; ввод во время показа доступен только верхнему диалогу. После последнего сообщения восстанавливается прежний timeScale.
HUD остаётся под инвентарём по прежнему flow. Полноэкранная навигация заменяет текущий маршрут и отменяет связанные с ним диалоги. Смена локации не создаёт ещё один UI или EventSystem.
Асинхронная навигация¶
await flow.OpenScreenAsync<SettingsScreen>(cancellationToken);
flow.Back();
await flow.OpenScreenAsync<DetailsScreen, DetailsData>(
new DetailsData("Награда", "Описание"),
cancellationToken);
DetailsScreen принимает DetailsData, BriefingScreen — BriefingData через IScreenContext
Кнопки используют void-обработчики OpenSettings/OpenInventory/... как границу UI. Внутри они ожидают задачу навигации, обрабатывают отмену и показывают NotificationData при ошибке открытия. Новая асинхронная навигация отменяет предыдущий запрос. Поздний результат не может заменить более новый экран.
Во время ожидания UI и игра заблокированы. Escape отменяет ожидание и оставляет прежний экран. История изменяется только после успешной загрузки и настройки контекста. На повторное открытие используется кешированный экземпляр.
Начальный HUD и экран загрузки остаются синхронно доступными, чтобы ошибка Addressables не блокировала старт и восстановление профиля. Игровые сцены загружаются прежним SceneManager; ID локации записывается только после успешной загрузки.
Диалоги и результаты¶
var answer = await flow.Dialogs.ConfirmAsync(
new ConfirmationData("Запустить миссию?", "Текущая локация будет заменена.", "Запустить"),
priority: 10,
cancellation: cancellationToken);
if (answer == ConfirmationResult.Confirmed)
{
// Игровую операцию выполняет вызывающая система.
}
await flow.Dialogs.NotifyAsync(
new NotificationData("Награда", "Получен предмет"),
priority: 5,
cancellation: cancellationToken);
ConfirmationResult: Confirmed или Cancelled. NotificationResult: Dismissed. Закрытие крестиком/кнопкой/через Escape возвращает результат закрытия. Отмена токена, CancelAll и уничтожение UI отменяют Task; это отличается от ответа игрока.
Больший priority очереди идёт первым; равные значения сохраняют порядок поступления. Уже показанный диалог не прерывается новым запросом. Приоритет очереди не меняет приоритет отрисовки: оба диалога используют Overlay с авторским приоритетом 1000, выше LoadingScreen.
Результат успешного диалога передаётся после завершения скрытия, чтобы следующий диалог и игровое действие не конкурировали с fade. Отмена токена завершает ожидание вызывающей стороны сразу; система завершает скрытие и освобождение ввода самостоятельно.
Запуск миссии подключён к ConfirmAsync. Отмена сохраняет прежнюю локацию; подтверждение начинает прежний LoadLocation. Повторный запуск, смена маршрута и устаревшее подтверждение защищены проверками исходного экрана и выбранной миссии. Ошибка открытия UI использует NotifyAsync.
Addressables и жизненный цикл¶
MenuRoot содержит две прямые ссылки и 11 AssetReferenceGameObject в deferredPrefabs. Ключ типа — полный Type.FullName, адрес — ui/windows/
Группа UI Windows использует локальные build/load paths и PackSeparately: окна могут загружаться отдельно. Remote CDN не настроен — размещение и URL пользователь не задавал.
Создание выполняется через Addressables.InstantiateAsync под заранее созданным неактивным WindowStaging. До привязки обработчиков и проверки типа префаб не показывается. Одновременные GetAsync одного типа используют одну операцию и один экземпляр.
Отмена одного ожидания не отменяет запрос других пользователей того же окна. Когда все ожидания отменены, неиспользуемый результат освобождается. Addressables не предоставляет остановку уже запущенного InstantiateAsync: система принимает его завершение и освобождает handle, не показывая окно.
Для Addressables создаётся TaskCompletionSource поверх Completed, без handle.Task — этот путь не требует Task API операции на WebGL. Поведение конкретной платформы всё равно требует проверки в целевой сборке.
ReleaseInstance освобождает экземпляр и его Addressables handle; при неуспешной операции освобождается handle через Release. Закрытие обычного окна скрывает его и сохраняет экземпляр для повторного открытия. Release допустим только вне текущего маршрута и истории; следующий запрос создаст новый экземпляр.
Загрузки и освобождения должны быть сбалансированы, как описано в документации Addressables 4.1.
В инвентаре и пассах включён RetainInstance. Release этих экранов запрещён: карточки внутри создаются один раз с экраном и переиспользуются до уничтожения постоянного UI. Пул карточек не создан.
var window = await windows.OpenAsync<DetailsScreen>(cancellationToken);
window.ApplyContext(new DetailsData("Предмет", "Описание"));
windows.SetPriority(window, 100);
windows.SetPlacement(window, WindowLayer.Overlay, 200);
windows.Close(window);
// После выхода из навигации при необходимости:
windows.Release(window);
OpenAsync WindowSystem — для дополнительного окна; навигация с возвратом — через FlowSystem; диалоги с результатом — через DialogSystem. Один кешированный экземпляр на тип: для одновременных уведомлений одного типа используется очередь. Синхронные Get/Open доступны только прямым или уже загруженным экранам.
Настройка сборки¶
Shared Bundle Settings переведён на CustomGroup = UI Windows. Это необходимо для нового UI-контента: у прежней Default Local Group отключена BundledAssetGroupSchema, а общий MonoScript bundle при сборке был привязан к ней. Существующие группы, их ассеты и схемы не перестраивались; изменено только назначение shared bundle.
Используется установленный BuildScriptSchemaDriven. Play Mode builder остаётся BuildScriptFastMode; проверка в нём не заменяет проверку packed-контента в сборке.
Подключение нового экрана¶
- Создать отдельный SomethingScreen : GameScreen и BindButtons с коллбеками.
- Для данных реализовать IScreenContext
; для диалога — DialogScreen . - Через Pipeline авторить полный префаб с CanvasGroup, animationRoot, всеми сериализованными ссылками и адаптивными anchors.
- Зарегистрировать префаб в UI Windows и добавить AssetReference/полное имя типа в deferredPrefabs.
- Вызывать OpenScreenAsync
() или типизированную перегрузку; для диалога добавить метод очереди. - Указать слой, приоритет, блокировку, паузу и RetainInstance, если содержимое должно жить весь срок UI.
Область общего кода¶
- WindowSystem.cs: Addressables, объединение ожиданий, отмена, handles и резервирования паузы/ввода; отдельный компонент без связи с общим менеджером не смог бы согласовать порядок окон.
- GameScreen.cs: ожидание скрытия и закрытие запроса нужны очереди и Task-результату; RetainInstance обеспечивает однократное создание карточек.
- FlowSystem.cs: асинхронная навигация, отмена устаревших запросов, подтверждение запуска миссии и подключение диалогов.
- DetailsScreen.cs/BriefingScreen.cs: типизированные контексты при сохранении сериализованных визуальных ссылок.
- MenuRoot, InventoryPopup и PassesScreen: каталог, DialogSystem, inactive staging и удержание карточек.
- AddressableAssetSettings и новая UI Windows: контент и shared bundle, необходимые загрузчику.
Boot, сохранения, движение, игровые сцены и карточки не менялись на этом этапе. Префабы и настройки изменены через подключённый Pipeline. UI Toolkit и пул карточек не добавлялись.
Проверка¶
Компиляция и структурные проверки выполнены через Pipeline: два прямых экрана и 11 Addressable-окон; типы каталога, GUID/entries, ссылки компонентов и inactive staging без ошибок. Сборка Addressables через BuildScriptSchemaDriven завершена успешно после настройки shared bundle; это подтверждает сборку контента, но не его загрузку в Player. Ручное поведение пока не проверено.
Ручной чеклист — все пункты не выполнены:
- Boot сразу открывает HUD в сохранённой/первой локации: один UI/EventSystem, одна игровая сцена, танк и камера.
- Все 73 прежние кнопки, 20 маркеров, вкладки инвентаря и стрелки пасса работают однократно после повторных открытий.
- HUD → магазин → пасс → подробности → Back возвращает по источникам. Брифинг → инвентарь → подробности сохраняет обратный маршрут.
- HUD под инвентарём виден, но кнопки и джойстик заблокированы. Закрытие возвращает прежний timeScale, сцену и позицию танка.
- Первый холодный запрос окна загружает его; повторный не создаёт второй экземпляр. Быстрые внешние запросы одного типа разделяют операцию; отмена одного не отменяет другой.
- Escape/токен при загрузке оставляет прежний экран; поздний результат не показывается. Новый маршрут отменяет предыдущий запрос; неиспользуемый handle освобождается.
- ConfirmAsync: подтверждение даёт Confirmed, отмена/Escape — Cancelled, отмена токена — отменённый Task. Результат ответа приходит после скрытия.
- NotifyAsync закрывается кнопкой/Escape, не меняет исходную сцену/маршрут. Уведомления с разными приоритетами показываются последовательно; равные — FIFO, активное не прерывается.
- Отмена ожидающего/активного диалога, CancelAll, смена маршрута и уничтожение UI завершают задачи без зависшего ввода или паузы. Между диалогами gameplay и нижний UI не получают кадр управления.
- Отмена запуска миссии сохраняет сцену/танк. Подтверждение загружает ровно одну сцену; двойное нажатие и устаревший ответ не запускают вторую загрузку.
- Сохранённая локация меняется только после успешной загрузки. Ошибки сцены, профиля и записи сохраняют Retry/карту/защиту данных. Перезапуск Boot и сворачивание сохраняют прежний flow.
- Ошибка Addressable ключа/префаба не меняет историю, освобождает блокировку и позволяет повторить; уведомление об ошибке закрывается. Ошибка загрузки самого уведомления не оставляет паузу/очередь зависшей.
- Release обычного скрытого окна вне истории → OpenAsync создаёт новый экземпляр; Release источника в истории, InventoryScreen и PassesScreen отклоняется. Карточки инвентаря/пассов сохраняют экземпляры при повторном открытии.
- Packed-сборка загружает все UI Windows и локализацию ru/en после изменения shared bundle; тестировать не только Fast Mode.
- На телефоне с вырезом, 9:16, 9:20 и планшете 3:4 safe area, кнопки подтверждения, тексты, сетки и прокрутка помещаются. Анимации/скролл работают при timeScale = 0.