Zenject Demo
Демо показывает, как интегрировать WorldGraphEditor с DI-контейнером Zenject: собственная реализация ITransitionManager, разделение ответственности на маленькие сервисы вокруг перехода между сценами.
Обзор сервисов
| Сервис | Отвечает за |
|---|---|
TransitionManagerService | Управление переходом: валидация, fade, загрузка сцены, spawn-позиция, graph-состояние |
PlayerLifecycleService | Спавн/pull игрока по данным менеджера |
GraphDataService | Visited-порты и shortcut-прогресс |
GameCompletionViewService | HUD: имя сцены, прогресс shortcut'ов |
ScreenFadeService | Fade-анимация с поддержкой отмены |
TransitionManagerService
Реализует ITransitionManager, IDisposable. Graph инжектится как IWorldGraph (не конкретный класс).
GoFromAsync(currentPortGuid) — обычный переход. Спрашивает IGraphDataService.IsPortVisited, затем Graph.CanPassTransition — граф сам решает, разрешён ли проход (visited-состояние, направление связи). Отказ — не ошибка, а LogWarning. В интерфейсе нет ignoreShortcuts — эта логика инкапсулирована в сервисе через IGraphDataService; кто хочет явный контроль, передаёт флаг через ITransitionContext.
GoToAsync(currentPortGuid, targetPortGuid) — прямой телепорт без CanPassTransition, для нестандартных переходов (fast travel и т.п.).
GoInternalAsync — общий пайплайн: TransitionStarted → fade in → SetPortVisited(source) → SceneManager.LoadSceneAsync → резолв OutputTransitionComponent и NextSpawnPosition → SetPortVisited(target) → SceneLoaded → fade out → TransitionEnded.
Обёрнут в try/catch с двумя ветками: OperationCanceledException — штатная отмена (например, Dispose() во время перехода), логируется как Info; остальные Exception — баги, LogException с полным стектрейсом. Try/catch на границе fire-and-forget, потому что порты могут вызывать GoFromAsync без await.
PlayerLifecycleService
Единственная ответственность — создавать экземпляр игрока в правильном месте после завершения перехода. В конструкторе подписывается на TransitionEnded и сразу вызывает SpawnPlayer() — это спавн для стартовой сцены (когда перехода ещё не было).
SpawnPlayer инстанциирует префаб в NextSpawnPosition. Если OutputTransitionComponent реализует IPusher — применяет push-force через SetPushForce. Затем включает управление через EnableMovement().
PullPlayer срабатывает на TransitionStarted — деактивирует игрока до загрузки следующей сцены.
GraphDataService
Хранит игровое состояние графа в памяти (между сессиями не сохраняется — для реальной игры тут нужен был бы файл сохранения). Два HashSet<string>: _visitedPorts (все посещённые guid'ы) и _shortcuts (подмножество visited, где порт — shortcut-destination). Разделение на два множества даёт OpenedShortcuts за O(1) вместо пересчёта по всем visited.
SetPortVisited вызывается менеджером дважды за переход — для input и output порта. Публичный контракт (IGraphDataService) — три члена: OpenedShortcuts, SetPortVisited, IsPortVisited.
GameCompletionViewService
View-сервис для HUD: слушает TransitionManager.SceneLoaded, обновляет текст текущей сцены и прогресса shortcut'ов. Т.к. это MonoBehaviour, инжект идёт через [Inject]-метод (не конструктор) — Zenject вызывает его сразу после Awake.
_shortcutsCount (общее число shortcut-рёбер в графе) считается один раз в конструкторе и кэшируется. В OnSceneLoaded guid текущего порта берётся из OutputTransitionComponent, а на первом запуске (до первого перехода, когда он ещё null) — fallback на TransitionComponentUtility.FindAny().
ScreenFadeService
Fade-анимация экрана: ShowAsync(CancellationToken) / HideAsync(CancellationToken), оба принимают токен отмены — чтобы TransitionManagerService.Dispose() мог прервать анимацию, если переход отменяется по ходу.