7.9 KiB
SELF_NOTES
Какие варианты я рассматривал
Я смотрел два варианта управления flow.
Первый вариант- сделать состояния более самостоятельными. Например, чтобы SplashState сам запускал переход в LoadState, потом LoadState сам вел в MenuState, а MenuState возвращал flow обратно в LoadState.
От этого варианта я отказался. В таком подходе state machine слишком быстро начинает знать почти весь сценарий. Из-за этого код сложнее проверять, сложнее менять и проще случайно сломать.
Второй вариант- оставить states спокойными и без лишней логики, а сам порядок экранов держать отдельно, во внешнем BootFlowService. Этот вариант я и выбрал.
StatesController<TEnum> просто переключает состояния по понятному порядку ExitAsync -> EnterAsync. А BootFlowService уже решает, куда идти дальше Splash -> Load -> Menu -> Load.
Так state machine не смешивается с boot-сценарием и остается более общей.
Еще я думал запускать flow из монобех.Start(), но в итоге отказался. В проекте используется VContainer, поэтому старт сделан через entry point BootstrapEntryPoint. Так зависимости создаются контейнером, а не руками в сценовом компоненте.
Почему я выбрал текущий вариант
Основная мысль простая, монобех остается View-слоем, а орекстрация живет в обычных C# классах.
GameLifetimeScopeсобирает зависимостиBootstrapEntryPointзапускает boot lifecycleBootFlowServiceведет общий сценарийStatesController<TEnum>отвечает только за переходы между состояниямиSplashState,LoadState,MenuStateотвечают за вход и выход своего состоянияUIViewи наследники держат Unity-ссылки и биндинги- ViewModel-классы не наследуются от
монобех
Так код проще читать. Если нужно понять порядок экранов, я иду в BootFlowService. Если нужно понять UI-логику, смотрю View. Если нужно понять переходы между состояниями, смотрю StatesController<TEnum>.
Что я писал и продумывал сам
Я сам выбрал основные границы ответственности. Вынес flow coordinator отдельно, сделал generic state controller, разделил View и ViewModel, а переходы убрал из View.
Для меня это важные места, потому что они показывают не просто набор классов, а именно архитектурное решение.
Также я отдельно продумывал повторные входы и очистку. Release() должен нормально вызываться повторно и ничего не ломать. Подписки в LoadingUIView должны чиститься через CompositeDisposable. А button listener в MenuUIView должен сниматься тем же method group, которым был добавлен.
Еще я исправил runtime bug с MissingReferenceException. При уничтожении scope Unity уже мог уничтожить View, но cancellation еще приводил к вызову Release(). Поэтому базовый UIView теперь проверяет destroyed Unity object перед обращением к геймобджект.
Что мне понятно
Мне понятна логика StatesController<TEnum>. Он получает код состояния, выходит из текущего state, потом входит в новый. Он не знает ничего про boot flow, поэтому его можно использовать повторно.
Мне понятна логика BootFlowService. Splash выполняется один раз, потом идет loading, потом menu, а после restart запускается новый loading-cycle.
Мне понятна логика LoadingUIView. Она берет progress из ViewModel, подписывается на него через UniRx, двигает Slider и чистит подписки в Release().
Мне понятна логика MenuUIView. Listener добавляется через AddListener(OnRestartClicked) и снимается через RemoveListener(OnRestartClicked). Без lambda и без RemoveAllListeners().
Что пока требует дополнительного изучения
Не до конца прозрачен внутренний механизм VContainer registration вида
builder.Register<Impl>(Lifetime.Singleton).As<IInterface>();
На практическом уровне я понимаю, что контейнер создает Impl как singleton и отдает его по интерфейсу. Но внутренние детали resolution pipeline, disposal order и build callbacks я пока глубоко не разбирал.
Также я понимаю, как использовать сервисный lifecycle
UniTask InitializeAsync(CancellationToken ct)
UniTask ReleaseAsync(CancellationToken ct)
Но внутренности UniTask для меня пока остаются темой, которую нужно разобрать отдельно. На уровне API я это использую осознанно.
Почему здесь сделано именно так
Почему states не вызывают EnterStateAsync сами
Потому что тогда state начинает знать весь сценарий. Сейчас SplashState отвечает за splash, LoadState за loading, MenuState за ожидание restart, а порядок задает BootFlowService. Так связей между частями меньше.
Почему ViewModel не монобех
Потому что ViewModel должна быть обычным C# объектом с логикой и данными для View. Unity-ссылки остаются во View, а runtime-данные передаются явно через Bind(...). Так проще тестировать и меньше скрытой зависимости от сцены.
Почему RegisterInstance используется только для settings
Потому что settings asset уже существует как данные сцены или проекта. А сервисы, контроллеры и states лучше создавать через контейнер, чтобы lifecycle и зависимости были управляемыми через DI.
Почему restart идет через MenuUIView -> MenuUIViewModel -> IMenuRestartSignal
Потому что View не должна знать state machine. Кнопка сообщает ViewModel о действии пользователя, ViewModel дергает restart signal, MenuState.EnterAsync завершается, а внешний BootFlowService уже запускает следующий LoadState.