This commit is contained in:
2026-05-27 08:29:41 +07:00
parent b91fe5adae
commit 0b088d112c
14 changed files with 179 additions and 644 deletions
+88
View File
@@ -0,0 +1,88 @@
# 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 lifecycle
- `BootFlowService` ведет общий сценарий
- `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 вида
```csharp
builder.Register<Impl>(Lifetime.Singleton).As<IInterface>();
```
На практическом уровне я понимаю, что контейнер создает `Impl` как singleton и отдает его по интерфейсу. Но внутренние детали resolution pipeline, disposal order и build callbacks я пока глубоко не разбирал.
Также я понимаю, как использовать сервисный lifecycle
```csharp
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`.