Release
This commit is contained in:
@@ -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`.
|
||||
Reference in New Issue
Block a user