Перейти к содержанию

Система экранов, окон и диалогов

Текущий этап: 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. 73 прежние кнопки сохраняются; подтверждение добавляет две кнопки, уведомление — одну.

Слои, ввод и пауза

Слои снизу вверх: 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. Новый экран с данными реализует этот интерфейс; игровые системы создают контекст, экран отображает его. Нет словарей object или приведения payload по имени действия.

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

Подключение нового экрана

  1. Создать отдельный SomethingScreen : GameScreen и BindButtons с коллбеками.
  2. Для данных реализовать IScreenContext; для диалога — DialogScreen.
  3. Через Pipeline авторить полный префаб с CanvasGroup, animationRoot, всеми сериализованными ссылками и адаптивными anchors.
  4. Зарегистрировать префаб в UI Windows и добавить AssetReference/полное имя типа в deferredPrefabs.
  5. Вызывать OpenScreenAsync() или типизированную перегрузку; для диалога добавить метод очереди.
  6. Указать слой, приоритет, блокировку, паузу и 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. Ручное поведение пока не проверено.

Ручной чеклист — все пункты не выполнены:

  1. Boot сразу открывает HUD в сохранённой/первой локации: один UI/EventSystem, одна игровая сцена, танк и камера.
  2. Все 73 прежние кнопки, 20 маркеров, вкладки инвентаря и стрелки пасса работают однократно после повторных открытий.
  3. HUD → магазин → пасс → подробности → Back возвращает по источникам. Брифинг → инвентарь → подробности сохраняет обратный маршрут.
  4. HUD под инвентарём виден, но кнопки и джойстик заблокированы. Закрытие возвращает прежний timeScale, сцену и позицию танка.
  5. Первый холодный запрос окна загружает его; повторный не создаёт второй экземпляр. Быстрые внешние запросы одного типа разделяют операцию; отмена одного не отменяет другой.
  6. Escape/токен при загрузке оставляет прежний экран; поздний результат не показывается. Новый маршрут отменяет предыдущий запрос; неиспользуемый handle освобождается.
  7. ConfirmAsync: подтверждение даёт Confirmed, отмена/Escape — Cancelled, отмена токена — отменённый Task. Результат ответа приходит после скрытия.
  8. NotifyAsync закрывается кнопкой/Escape, не меняет исходную сцену/маршрут. Уведомления с разными приоритетами показываются последовательно; равные — FIFO, активное не прерывается.
  9. Отмена ожидающего/активного диалога, CancelAll, смена маршрута и уничтожение UI завершают задачи без зависшего ввода или паузы. Между диалогами gameplay и нижний UI не получают кадр управления.
  10. Отмена запуска миссии сохраняет сцену/танк. Подтверждение загружает ровно одну сцену; двойное нажатие и устаревший ответ не запускают вторую загрузку.
  11. Сохранённая локация меняется только после успешной загрузки. Ошибки сцены, профиля и записи сохраняют Retry/карту/защиту данных. Перезапуск Boot и сворачивание сохраняют прежний flow.
  12. Ошибка Addressable ключа/префаба не меняет историю, освобождает блокировку и позволяет повторить; уведомление об ошибке закрывается. Ошибка загрузки самого уведомления не оставляет паузу/очередь зависшей.
  13. Release обычного скрытого окна вне истории → OpenAsync создаёт новый экземпляр; Release источника в истории, InventoryScreen и PassesScreen отклоняется. Карточки инвентаря/пассов сохраняют экземпляры при повторном открытии.
  14. Packed-сборка загружает все UI Windows и локализацию ru/en после изменения shared bundle; тестировать не только Fast Mode.
  15. На телефоне с вырезом, 9:16, 9:20 и планшете 3:4 safe area, кнопки подтверждения, тексты, сетки и прокрутка помещаются. Анимации/скролл работают при timeScale = 0.