Compare commits
4 Commits
3dd611423e
..
1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| e3e370bee2 | |||
| f3e479a45b | |||
| 0b088d112c | |||
| b91fe5adae |
+3
-1
@@ -5,11 +5,13 @@
|
|||||||
!/ProjectSettings/
|
!/ProjectSettings/
|
||||||
!/Packages/
|
!/Packages/
|
||||||
!/docs/
|
!/docs/
|
||||||
!/Agent/
|
|
||||||
!.gitignore
|
!.gitignore
|
||||||
!.gitattributes
|
!.gitattributes
|
||||||
!LICENSE
|
!LICENSE
|
||||||
!README.md
|
!README.md
|
||||||
|
!AI_LOG.md
|
||||||
|
!SELF_NOTES.md
|
||||||
|
!Task.md
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.Spotlight-V100
|
.Spotlight-V100
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# AI_LOG
|
||||||
|
|
||||||
|
## Использование AI
|
||||||
|
|
||||||
|
AI использовался как помощник при планировании, реализации и проверке проекта. Работа велась итеративно по task-файлам из `Agent/Task/`.
|
||||||
|
|
||||||
|
## Какие промпты использовались
|
||||||
|
|
||||||
|
- Изучить `TASK-0001` и файл `Agent/Agent.md`, заранее прочитать остальные задачи и подготовить базу под них.
|
||||||
|
- Спланировать и реализовать TASK-0001 - TASK-0006 (с персональными правками под планирование).
|
||||||
|
|
||||||
|
## Где AI ошибся или требовал корректировки
|
||||||
|
|
||||||
|
- После реализации lifecycle возник runtime bug `MissingReferenceException`: при уничтожении `LifetimeScope` cancellation вызывал `Release()` уже уничтоженного `MenuUIView`. Это потребовало отдельной диагностики и фикса в базовом `UIView`.
|
||||||
|
- AI приходилось явно удерживать ограничения проекта. Например не использовать `FindObjectOfType`, не использовать `Singleton.Instance`, не использовать static-state, делать serialized поля через auto property с `[field: SerializeField]` и другие моменты.
|
||||||
|
- Пытался игнорировать SOLID.
|
||||||
|
|
||||||
|
## Что было переписано или уточнено руками
|
||||||
|
|
||||||
|
- Правки по stack trace для `MissingReferenceException` на основе этого был исправлен lifecycle в `UIView`.
|
||||||
|
- Документы написаны с учетом фактической реализации, а не только первоначального плана.
|
||||||
|
- Исправлены моменты TASK-ок которые нарушали задуманную архитекутру.
|
||||||
|
- Scene references были настроены вручную.
|
||||||
|
- Сборка UI с настройкой якорей для адаптивной работы на разных экранах.
|
||||||
|
- Пронумировал enum, чтобы не ломалась сериализация при добавление стейтов.
|
||||||
|
|
||||||
|
## Что проверялось
|
||||||
|
|
||||||
|
- Кодстайл проекта, соблюдение namespace.
|
||||||
|
- Поиск запрещенных паттернов `FindObjectOfType`, `Singleton.Instance`, `static`, `async void`, `Task.Delay`, `Update`, `RemoveAllListeners`.
|
||||||
|
- Проверка listener `AddListener(OnRestartClicked)` и `RemoveListener(OnRestartClicked)`.
|
||||||
|
- Проверка, что states не вызывают `EnterStateAsync` сами.
|
||||||
|
- Проверка, что ViewModel не наследуются от `MonoBehaviour`.
|
||||||
|
|
||||||
|
## Что важно проверить вручную
|
||||||
|
|
||||||
|
- Открыть `Assets/Scenes/SampleScene.unity`.
|
||||||
|
- Запустить Play Mode.
|
||||||
|
- Проверить `Splash -> Loading -> Menu`.
|
||||||
|
- Проверить, что `Text (Data_Status)` показывает актуальный этап.
|
||||||
|
- Проверить, что `Slider` идет от `0` до `1`.
|
||||||
|
- Нажать `Restart` и проверить повторный `Loading -> Menu`.
|
||||||
|
- Остановить Play Mode на `Menu` и убедиться, что нету Warning-ов и Error-ов.
|
||||||
-128
@@ -1,128 +0,0 @@
|
|||||||
# Правила для агента
|
|
||||||
|
|
||||||
## Главный источник задачи
|
|
||||||
|
|
||||||
Перед началом любой работы по проекту агент обязан прочитать файл:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Agent/TASK.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Именно этот файл считается основным описанием задачи, требований и ограничений.
|
|
||||||
|
|
||||||
## Папка с задачами
|
|
||||||
|
|
||||||
Детальный план работ хранится в папке:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Agent/Task/
|
|
||||||
```
|
|
||||||
|
|
||||||
Задачи должны называться строго по формату:
|
|
||||||
|
|
||||||
```text
|
|
||||||
TASK-0001.md
|
|
||||||
TASK-0002.md
|
|
||||||
TASK-0003.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Новые задачи создаются только со следующим свободным номером. Нельзя пропускать номера без причины и нельзя переиспользовать номер удаленной или завершенной задачи.
|
|
||||||
|
|
||||||
Правила шаблона задач описаны в файле:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Agent/Task/TASK_TEMPLATE.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Каждая новая задача обязана соблюдать этот шаблон.
|
|
||||||
|
|
||||||
## Порядок работы
|
|
||||||
|
|
||||||
1. Сначала прочитать `Agent/TASK.md` полностью.
|
|
||||||
2. Прочитать `Agent/Task/TASK_TEMPLATE.md`.
|
|
||||||
3. Прочитать актуальные задачи `Agent/Task/TASK-*.md`.
|
|
||||||
4. После чтения сверять все решения с требованиями из задачи и конкретных task-файлов.
|
|
||||||
5. Не реализовывать вариант B или любую функциональность, которой нет в `Agent/TASK.md` или `Agent/Task/TASK-*.md`.
|
|
||||||
6. Не добавлять лишние архитектурные слои, если они не нужны для выполнения задачи.
|
|
||||||
7. Приоритет: минимальная корректная реализация, чистый жизненный цикл, понятный код.
|
|
||||||
|
|
||||||
## Работа со статусами задач
|
|
||||||
|
|
||||||
- Перед реализацией брать только задачи со статусом `Ready`.
|
|
||||||
- При начале реализации переводить задачу в статус `In Progress`.
|
|
||||||
- После выполнения, проверки и фиксации результата переводить задачу в статус `Done`.
|
|
||||||
- Если задача заблокирована, переводить ее в статус `Blocked` и описывать причину в разделе `Заметки`.
|
|
||||||
- Не начинать следующую задачу, если текущая задача в статусе `In Progress` не завершена и не заблокирована.
|
|
||||||
- Если для выполнения текущей задачи появилась новая работа, создать новую задачу по шаблону, а не смешивать разные цели в одном файле.
|
|
||||||
|
|
||||||
## Правила заведения задач
|
|
||||||
|
|
||||||
- Новые задачи создавать только в `Agent/Task/`.
|
|
||||||
- Имя файла должно быть `TASK-XXXX.md`, где `XXXX` — номер из четырех цифр.
|
|
||||||
- Заголовок задачи должен начинаться с того же номера, что и имя файла.
|
|
||||||
- Структура задачи должна соответствовать `Agent/Task/TASK_TEMPLATE.md`.
|
|
||||||
- В задаче должны быть разделы `Статус`, `Цель`, `Что сделать`, `Технические требования`, `Критерии готовности`, `Заметки`.
|
|
||||||
- Статус задачи должен быть одним из значений: `Planned`, `Ready`, `In Progress`, `Done`, `Blocked`.
|
|
||||||
- Задача должна быть достаточно крупной, чтобы не дробить работу на слишком много файлов.
|
|
||||||
- Задача должна быть достаточно конкретной, чтобы по критериям готовности можно было проверить результат.
|
|
||||||
- Нельзя заводить задачи по варианту B.
|
|
||||||
|
|
||||||
## Ограничения
|
|
||||||
|
|
||||||
- Использовать VContainer для DI.
|
|
||||||
- Использовать UniTask для async-операций.
|
|
||||||
- Использовать UniRx для реактивных значений и подписок.
|
|
||||||
- Не использовать `FindObjectOfType`.
|
|
||||||
- Не использовать `Singleton.Instance`.
|
|
||||||
- Не хранить состояние в `static`.
|
|
||||||
- Не использовать `async void`, кроме Unity-колбэков.
|
|
||||||
- Все async-операции выполнять через UniTask и `CancellationToken`.
|
|
||||||
- Все подписки должны корректно освобождаться.
|
|
||||||
|
|
||||||
## Архитектурные правила
|
|
||||||
|
|
||||||
- Orchestration-логика должна жить в plain C# классах.
|
|
||||||
- `MonoBehaviour` использовать только как View-компоненты и Unity binding layer.
|
|
||||||
- State не должен сам переключать state machine изнутри своего `EnterAsync(...)`.
|
|
||||||
- Переходами между state управляет внешний flow coordinator.
|
|
||||||
- Generic state machine должна отвечать только за порядок `ExitAsync` текущего state и `EnterAsync` нового state.
|
|
||||||
- ViewModel должна быть обычным C#-классом.
|
|
||||||
- ViewModel передавать во View явно через bind/setup-метод, а не через DI-поля MonoBehaviour.
|
|
||||||
- View не должна содержать бизнес-логику boot flow и не должна напрямую управлять state machine.
|
|
||||||
|
|
||||||
## Правила DI
|
|
||||||
|
|
||||||
- Scene View регистрировать через `RegisterComponent(...)`.
|
|
||||||
- Settings/config assets регистрировать через `[SerializeField]` и `RegisterInstance(...)`.
|
|
||||||
- Сервисы, controller-объекты и state-объекты не регистрировать через `RegisterInstance(...)`.
|
|
||||||
- Сервисы регистрировать как интерфейсы через `builder.Register<Impl>(Lifetime.Singleton).As<IInterface>()`.
|
|
||||||
- Runtime-данные не прокидывать в MonoBehaviour через DI-поля.
|
|
||||||
- Не создавать зависимости вручную внутри MonoBehaviour, если они должны управляться контейнером.
|
|
||||||
|
|
||||||
## Правила async и отмены
|
|
||||||
|
|
||||||
- Каждый `await` должен получать `CancellationToken`, пришедший сверху, если операция поддерживает отмену.
|
|
||||||
- `UniTask.Delay`, `UniTask.Yield` и похожие операции должны использовать `CancellationToken`.
|
|
||||||
- `CancellationToken` не должен быть декоративным: отмена должна реально останавливать ожидания и фоновые операции.
|
|
||||||
- `OperationCanceledException` при штатной остановке flow, сцены или scope не считать ошибкой.
|
|
||||||
- Если операция должна завершаться по нескольким причинам, использовать `CancellationTokenSource.CreateLinkedTokenSource(...)`.
|
|
||||||
- Linked `CancellationTokenSource` обязательно освобождать через `Dispose()`.
|
|
||||||
- Фоновые циклы запускать в `InitializeAsync` и останавливать через `CancellationTokenSource.Cancel()` в `ReleaseAsync`.
|
|
||||||
|
|
||||||
## Правила UI и подписок
|
|
||||||
|
|
||||||
- Для UniRx-подписок использовать `CompositeDisposable`.
|
|
||||||
- Подписки создавать при `Initialize()` или после явного bind/setup ViewModel.
|
|
||||||
- Все подписки очищать в `Release()`.
|
|
||||||
- `Release()` должен быть идемпотентным и безопасным при повторном вызове.
|
|
||||||
- Сначала останавливать входящие сигналы и подписки, затем очищать ссылки на ViewModel и runtime-данные.
|
|
||||||
- Для `Button.onClick` использовать симметричные `AddListener(...)` и `RemoveListener(...)`.
|
|
||||||
- Снимать нужно тот же listener, который был добавлен.
|
|
||||||
- Не использовать `RemoveAllListeners()` как основной способ очистки.
|
|
||||||
- Не использовать анонимные listeners, если их потом нельзя симметрично снять.
|
|
||||||
- Не обновлять реактивный UI через `Update()`.
|
|
||||||
- Повторный вход во View не должен создавать дублирующиеся подписки или callbacks.
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
|
|
||||||
Сделать только задачу `Boot Flow` из `Agent/TASK.md` и подготовить проект так, чтобы его можно было проверить по описанным требованиям.
|
|
||||||
-216
@@ -1,216 +0,0 @@
|
|||||||
# Задача: Unity / C# Middle
|
|
||||||
|
|
||||||
## Кратко
|
|
||||||
|
|
||||||
Нужно реализовать загрузочный поток приложения в Unity через собственную state machine.
|
|
||||||
|
|
||||||
Проект создается с нуля. Базовые абстракции, сервисы, View/ViewModel и state machine нужно написать самостоятельно, потому что это часть оценки.
|
|
||||||
|
|
||||||
Ориентировочное время выполнения: около 1,5 часов. Если ушло больше или меньше времени, это нормально, но нужно честно указать это в документации.
|
|
||||||
|
|
||||||
## Технические требования
|
|
||||||
|
|
||||||
- Unity 2022 LTS+.
|
|
||||||
- C#.
|
|
||||||
- Использовать VContainer, UniTask и UniRx.
|
|
||||||
- Опционально можно использовать Odin Inspector и DOTween.
|
|
||||||
|
|
||||||
## Архитектурные требования
|
|
||||||
|
|
||||||
### Reactive
|
|
||||||
|
|
||||||
Использовать UniRx для реактивных значений и подписок.
|
|
||||||
|
|
||||||
Если дополнительно понадобится собственная обертка `ReactiveValue<T>`, она должна поддерживать сигнатуру подписки:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
IDisposable Subscribe(Action<T> cb, bool invokeImmediately = true)
|
|
||||||
```
|
|
||||||
|
|
||||||
Требования:
|
|
||||||
|
|
||||||
- каждая подписка должна корректно диспозиться;
|
|
||||||
- нельзя использовать голые `event Action` без отписки.
|
|
||||||
|
|
||||||
### Async
|
|
||||||
|
|
||||||
Все async-операции должны быть реализованы только через UniTask и `CancellationToken`.
|
|
||||||
|
|
||||||
Запрещено использовать `async void`, кроме Unity-колбэков.
|
|
||||||
|
|
||||||
### Dependency Injection
|
|
||||||
|
|
||||||
Использовать VContainer.
|
|
||||||
|
|
||||||
Все сервисы регистрируются как интерфейсы:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
builder.Register<Impl>(Lifetime.Singleton).As<IInterface>();
|
|
||||||
```
|
|
||||||
|
|
||||||
Запрещено использовать:
|
|
||||||
|
|
||||||
- `FindObjectOfType`;
|
|
||||||
- `Singleton.Instance`;
|
|
||||||
- `static`-хранилища состояния.
|
|
||||||
|
|
||||||
### UI
|
|
||||||
|
|
||||||
Нужно сделать базовую пару View/ViewModel:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class UIView : MonoBehaviour
|
|
||||||
{
|
|
||||||
public virtual void Initialize() { }
|
|
||||||
public virtual void Release() { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UIView<TVm> : UIView where TVm : IUIViewModel
|
|
||||||
{
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Требования:
|
|
||||||
|
|
||||||
- ViewModel должна быть обычным C#-классом, не `MonoBehaviour`;
|
|
||||||
- логика находится во ViewModel;
|
|
||||||
- View хранит только Unity-ссылки и биндинги.
|
|
||||||
|
|
||||||
### Services
|
|
||||||
|
|
||||||
Сервисы должны иметь асинхронный жизненный цикл:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
UniTask InitializeAsync(CancellationToken ct)
|
|
||||||
UniTask ReleaseAsync(CancellationToken ct)
|
|
||||||
```
|
|
||||||
|
|
||||||
Требования:
|
|
||||||
|
|
||||||
- сервисы оформляются как `Service : IService`;
|
|
||||||
- фоновые циклы стартуют в `InitializeAsync`;
|
|
||||||
- фоновые циклы останавливаются через `CancellationTokenSource.Cancel()` в `ReleaseAsync`.
|
|
||||||
|
|
||||||
### Config
|
|
||||||
|
|
||||||
Конфиги должны быть `ScriptableObject` с суффиксом `*Settings`.
|
|
||||||
|
|
||||||
Регистрация в `LifetimeScope`:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[SerializeField] private SomeSettings _settings;
|
|
||||||
|
|
||||||
protected override void Configure(IContainerBuilder builder)
|
|
||||||
{
|
|
||||||
builder.RegisterInstance(_settings);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Задача: Boot Flow
|
|
||||||
|
|
||||||
Реализовать загрузочный поток приложения через собственную state machine из трех состояний.
|
|
||||||
|
|
||||||
### 1. База state machine
|
|
||||||
|
|
||||||
Нужно реализовать собственные абстракции:
|
|
||||||
|
|
||||||
- `IState`;
|
|
||||||
- `IStatesController<TEnum>`;
|
|
||||||
- `StatesController<TEnum>`.
|
|
||||||
|
|
||||||
В `StatesController<TEnum>` должен быть метод:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
UniTask EnterStateAsync(TEnum code, CancellationToken ct)
|
|
||||||
```
|
|
||||||
|
|
||||||
Контракт перехода между состояниями:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
await currentState.ExitAsync(ct);
|
|
||||||
await newState.EnterAsync(ct);
|
|
||||||
```
|
|
||||||
|
|
||||||
Требования:
|
|
||||||
|
|
||||||
- `CancellationToken` пробрасывается через всю цепочку вызовов;
|
|
||||||
- `CancellationToken` должен реально отменять внутренние ожидания;
|
|
||||||
- повторные входы в состояния не должны ломать UI и подписки.
|
|
||||||
|
|
||||||
### 2. Состояния
|
|
||||||
|
|
||||||
Нужно реализовать три состояния.
|
|
||||||
|
|
||||||
#### SplashState
|
|
||||||
|
|
||||||
Поведение:
|
|
||||||
|
|
||||||
- показывает лого;
|
|
||||||
- ждет 1 секунду через `UniTask.Delay(..., ct)`;
|
|
||||||
- переходит в следующее состояние.
|
|
||||||
|
|
||||||
#### LoadState
|
|
||||||
|
|
||||||
Поведение:
|
|
||||||
|
|
||||||
- имитирует загрузку;
|
|
||||||
- выполняет 5 шагов по 200 мс;
|
|
||||||
- хранит `ReactiveValue<float> Progress` со значением от `0` до `1`;
|
|
||||||
- обновляет `Progress` после каждого шага.
|
|
||||||
|
|
||||||
#### MenuState
|
|
||||||
|
|
||||||
Поведение:
|
|
||||||
|
|
||||||
- показывает `MenuUIView`;
|
|
||||||
- содержит одну кнопку `Restart`;
|
|
||||||
- по клику на `Restart` возвращает приложение в `LoadState`.
|
|
||||||
|
|
||||||
### 3. UI
|
|
||||||
|
|
||||||
Нужно реализовать `LoadingUIView`.
|
|
||||||
|
|
||||||
Требования:
|
|
||||||
|
|
||||||
- `LoadingUIView` подписывается на `LoadState.Progress`;
|
|
||||||
- прогресс-бар обновляется через DOTween или ручной Lerp;
|
|
||||||
- при `ExitAsync` все подписки должны корректно очищаться;
|
|
||||||
- при повторном входе в `LoadState` не должно быть `NullReferenceException` и дублирующихся подписок.
|
|
||||||
|
|
||||||
## Что сдать
|
|
||||||
|
|
||||||
- GitHub-репозиторий или zip-архив без `Library/`, `Temp/`, `obj/`.
|
|
||||||
- `README.md` с инструкцией запуска.
|
|
||||||
- В `README.md` добавить 5-10 строк о том, что было бы доделано при наличии еще 2 часов.
|
|
||||||
- `SELF_NOTES.md` с описанием собственных решений.
|
|
||||||
- `AI_LOG.md` с описанием использования AI. Если AI не использовался, нужно написать почему.
|
|
||||||
|
|
||||||
## SELF_NOTES.md
|
|
||||||
|
|
||||||
В `SELF_NOTES.md` нужно своими словами описать:
|
|
||||||
|
|
||||||
- какие идеи были рассмотрены и почему выбрана текущая реализация;
|
|
||||||
- какие места в коде были придуманы и написаны самостоятельно, без AI;
|
|
||||||
- что в коде понятно до последней строки;
|
|
||||||
- что осталось непонятным или воспринимается как магия;
|
|
||||||
- ответы на 2-3 ключевых вопроса вида: почему здесь сделано именно так.
|
|
||||||
|
|
||||||
## AI_LOG.md
|
|
||||||
|
|
||||||
В `AI_LOG.md` нужно описать:
|
|
||||||
|
|
||||||
- какие промпты использовались;
|
|
||||||
- где AI ошибся;
|
|
||||||
- что было переписано руками;
|
|
||||||
- если AI не использовался, почему было принято такое решение.
|
|
||||||
|
|
||||||
## Что не требуется
|
|
||||||
|
|
||||||
Не нужно тратить время на:
|
|
||||||
|
|
||||||
- красивый арт;
|
|
||||||
- сложные анимации;
|
|
||||||
- звук;
|
|
||||||
- мобильную сборку.
|
|
||||||
|
|
||||||
Главное: архитектурные слои, стиль кода, корректная работа жизненного цикла и понимание собственных решений.
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# TASK-0001: Composition Root и зависимости
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
Ready
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
|
|
||||||
Настроить корневую точку композиции проекта через VContainer так, чтобы orchestration-логика жила в plain C# классах, а MonoBehaviour использовались только как View-компоненты сцены.
|
|
||||||
|
|
||||||
## Что сделать
|
|
||||||
|
|
||||||
- Создать `GameLifetimeScope`.
|
|
||||||
- Зарегистрировать сервисы и контроллеры через VContainer как managed dependencies.
|
|
||||||
- Зарегистрировать `BootstrapEntryPoint` как entry point приложения.
|
|
||||||
- Зарегистрировать `BootStatesController` как `IStatesController<BootStateCode>` и при необходимости как self.
|
|
||||||
- Зарегистрировать `SplashState`, `LoadState`, `MenuState`.
|
|
||||||
- Зарегистрировать scene View через `RegisterComponent(...)`.
|
|
||||||
- Зарегистрировать settings asset через `[SerializeField]` и `RegisterInstance(...)`.
|
|
||||||
|
|
||||||
## Технические требования
|
|
||||||
|
|
||||||
- Использовать VContainer.
|
|
||||||
- Сервисы регистрировать через `builder.Register<Impl>(Lifetime.Singleton).As<IInterface>()`.
|
|
||||||
- Не использовать `RegisterInstance(...)` для сервисов и controller-объектов.
|
|
||||||
- `RegisterInstance(...)` использовать только для settings/config assets.
|
|
||||||
- Не использовать `FindObjectOfType`, `Singleton.Instance` и `static`-хранилища состояния.
|
|
||||||
- MonoBehaviour не должны содержать orchestration-логику boot flow.
|
|
||||||
|
|
||||||
## Критерии готовности
|
|
||||||
|
|
||||||
- В сцене есть один `GameLifetimeScope`.
|
|
||||||
- Все boot flow зависимости собираются через VContainer.
|
|
||||||
- View-компоненты подключаются через ссылки из сцены, а не через поиск объектов.
|
|
||||||
- Entry point запускается контейнером.
|
|
||||||
- Сервисы и state machine не создаются вручную через `new` внутри MonoBehaviour.
|
|
||||||
|
|
||||||
## Заметки
|
|
||||||
|
|
||||||
Scene View допустимо регистрировать как компоненты, потому что они физически живут в сцене. Settings asset допустимо регистрировать через `RegisterInstance(...)`, потому что это данные, а не сервис с жизненным циклом.
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# TASK-0002: Базовая архитектура Boot Flow
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
Ready
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
|
|
||||||
Создать минимальную архитектурную основу для boot flow: сервисный lifecycle, state-контракты и generic state controller.
|
|
||||||
|
|
||||||
## Что сделать
|
|
||||||
|
|
||||||
- Создать `IService` с методами `InitializeAsync(CancellationToken ct)` и `ReleaseAsync(CancellationToken ct)`.
|
|
||||||
- Создать базовый `Service : IService`, если это упростит общую структуру.
|
|
||||||
- Создать `IState` с методами `EnterAsync(CancellationToken ct)` и `ExitAsync(CancellationToken ct)`.
|
|
||||||
- Создать `IStatesController<TEnum>` с методом `EnterStateAsync(TEnum code, CancellationToken ct)`.
|
|
||||||
- Реализовать `StatesController<TEnum>`.
|
|
||||||
- Создать `BootStateCode` для состояний `Splash`, `Load`, `Menu`.
|
|
||||||
- Создать `BootStatesController`, который собирает конкретные boot states и передает их в базовый controller.
|
|
||||||
|
|
||||||
## Технические требования
|
|
||||||
|
|
||||||
- Использовать UniTask для всех async-методов.
|
|
||||||
- Во все async-операции передавать `CancellationToken`.
|
|
||||||
- Контракт перехода должен быть строгим: сначала `ExitAsync` текущего state, затем `EnterAsync` нового state.
|
|
||||||
- `StatesController<TEnum>` не должен знать бизнес-логику boot flow.
|
|
||||||
- State не должен сам решать, какой state будет следующим.
|
|
||||||
|
|
||||||
## Критерии готовности
|
|
||||||
|
|
||||||
- `EnterStateAsync` вызывает `ExitAsync` текущего state перед `EnterAsync` нового state.
|
|
||||||
- Первый вход в state работает без попытки выйти из отсутствующего текущего state.
|
|
||||||
- Повторный вход в другой state не оставляет предыдущий state активным.
|
|
||||||
- `CancellationToken` проброшен через `EnterStateAsync`, `ExitAsync` и `EnterAsync`.
|
|
||||||
|
|
||||||
## Заметки
|
|
||||||
|
|
||||||
Generic state machine должна уметь только переключать состояния. Сценарий `Splash -> Load -> Menu -> Load` должен жить отдельно, чтобы не смешивать control flow и инфраструктуру state machine.
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# TASK-0003: BootFlowService и запуск сценария
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
Ready
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
|
|
||||||
Реализовать внешний flow coordinator, который запускает и управляет сценарием загрузки приложения.
|
|
||||||
|
|
||||||
## Что сделать
|
|
||||||
|
|
||||||
- Создать `BootFlowService : Service` или сервис с аналогичным lifecycle.
|
|
||||||
- Реализовать сценарий `Splash -> Load -> Menu -> Load -> Menu ...`.
|
|
||||||
- Сделать так, чтобы `SplashState` выполнялся один раз при старте.
|
|
||||||
- После `MenuState` возвращаться в `LoadState` по restart-сигналу.
|
|
||||||
- Создать `BootstrapEntryPoint`, который запускается через VContainer entry point.
|
|
||||||
- Запустить `BootFlowService` из entry point через UniTask.
|
|
||||||
- Корректно обработать штатную отмену, не логируя ее как ошибку.
|
|
||||||
|
|
||||||
## Технические требования
|
|
||||||
|
|
||||||
- Использовать `IAsyncStartable` или подходящий VContainer entry point для запуска.
|
|
||||||
- Не запускать boot flow из `MonoBehaviour.Start()` вручную.
|
|
||||||
- Не делать самопереключающиеся states, которые вызывают `EnterStateAsync(...)` изнутри своего `EnterAsync(...)`.
|
|
||||||
- `OperationCanceledException` считать нормальным завершением при уничтожении scope или остановке flow.
|
|
||||||
- Все ожидания должны использовать токен, полученный сверху.
|
|
||||||
|
|
||||||
## Критерии готовности
|
|
||||||
|
|
||||||
- При старте выполняется последовательность `Splash -> Load -> Menu`.
|
|
||||||
- Нажатие `Restart` запускает новый цикл `Load -> Menu`.
|
|
||||||
- При отмене токена boot flow завершается без зависших UniTask.
|
|
||||||
- Orchestration-код находится в plain C# классе, не во View.
|
|
||||||
|
|
||||||
## Заметки
|
|
||||||
|
|
||||||
Рекомендуемый сценарий: внешний сервис вызывает `_states.EnterStateAsync(...)` последовательно. Это проще защищать на ревью и не создает проблем с reentrancy внутри states.
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# TASK-0004: UI база и ViewModel слой
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
Ready
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
|
|
||||||
Создать базовый UI слой, в котором View отвечает только за Unity-ссылки и биндинги, а логика находится во ViewModel или state/flow сервисах.
|
|
||||||
|
|
||||||
## Что сделать
|
|
||||||
|
|
||||||
- Создать `IUIViewModel`.
|
|
||||||
- Создать `UIView : MonoBehaviour` с методами `Initialize()` и `Release()`.
|
|
||||||
- Создать `UIView<TVm> : UIView where TVm : IUIViewModel`.
|
|
||||||
- Добавить явную привязку ViewModel во View через метод вроде `Bind(TVm vm)` или `Setup(TVm vm)`.
|
|
||||||
- Сделать `Release()` идемпотентным для всех View.
|
|
||||||
- Подготовить View для splash, loading и menu экранов.
|
|
||||||
|
|
||||||
## Технические требования
|
|
||||||
|
|
||||||
- ViewModel должна быть обычным C#-классом, не `MonoBehaviour`.
|
|
||||||
- Runtime-данные передавать во View явно, а не через DI-поля MonoBehaviour.
|
|
||||||
- View не должна содержать orchestration-логику boot flow.
|
|
||||||
- Все подписки и listeners должны сниматься в `Release()`.
|
|
||||||
- Повторный вызов `Release()` не должен приводить к ошибкам.
|
|
||||||
|
|
||||||
## Критерии готовности
|
|
||||||
|
|
||||||
- Есть базовые классы `UIView` и `UIView<TVm>`.
|
|
||||||
- ViewModel можно передать во View явно перед `Initialize()`.
|
|
||||||
- `Release()` безопасен при повторном вызове.
|
|
||||||
- View не вызывает `FindObjectOfType`, не хранит глобальное состояние и не управляет переходами state machine.
|
|
||||||
|
|
||||||
## Заметки
|
|
||||||
|
|
||||||
MonoBehaviour должны оставаться presentation layer. Это соответствует задаче: логика находится в VM или сервисах, View только показывает состояние и прокидывает UI-события.
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# TASK-0005: SplashState, LoadState и прогресс
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
Ready
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
|
|
||||||
Реализовать splash и loading состояния с настоящей отменой async-операций и реактивным прогрессом через UniRx.
|
|
||||||
|
|
||||||
## Что сделать
|
|
||||||
|
|
||||||
- Реализовать `SplashState`.
|
|
||||||
- В `SplashState.EnterAsync` показать splash View и подождать 1 секунду через `UniTask.Delay(..., cancellationToken: ct)`.
|
|
||||||
- В `SplashState.ExitAsync` скрыть или release-нуть splash View.
|
|
||||||
- Реализовать `LoadState`.
|
|
||||||
- В `LoadState` создать реактивный прогресс от `0` до `1` через UniRx.
|
|
||||||
- Наружу отдавать progress как read-only значение или read-only интерфейс.
|
|
||||||
- В `LoadState.EnterAsync` сбрасывать progress в `0f`.
|
|
||||||
- Выполнить 5 шагов загрузки по 200 мс с обновлениями `0.2f`, `0.4f`, `0.6f`, `0.8f`, `1f`.
|
|
||||||
- Реализовать `LoadingUIView`, подписанный на progress.
|
|
||||||
|
|
||||||
## Технические требования
|
|
||||||
|
|
||||||
- Использовать UniTask и `CancellationToken` во всех delay.
|
|
||||||
- Использовать UniRx для progress и подписок.
|
|
||||||
- Подписки хранить в `CompositeDisposable` и очищать в `Release()`.
|
|
||||||
- `LoadingUIView` не должна менять progress, только читать его.
|
|
||||||
- Не обновлять progress UI через `Update()`.
|
|
||||||
- При использовании tween/lerp останавливать предыдущую визуализацию перед запуском новой.
|
|
||||||
|
|
||||||
## Критерии готовности
|
|
||||||
|
|
||||||
- `SplashState` отменяется через внешний `CancellationToken`.
|
|
||||||
- `LoadState` выставляет ожидаемую последовательность progress значений.
|
|
||||||
- `LoadingUIView` корректно обновляет progress bar.
|
|
||||||
- При выходе из `LoadState` подписки очищаются.
|
|
||||||
- Повторный вход в `LoadState` не создает дублирующихся подписок и не вызывает `NullReferenceException`.
|
|
||||||
|
|
||||||
## Заметки
|
|
||||||
|
|
||||||
В этом проекте важнее чистый lifecycle и гигиена подписок, чем сложная анимация progress bar. Достаточно прямого обновления или простого lerp.
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# TASK-0006: MenuState и Restart
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
Ready
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
|
|
||||||
Реализовать меню с кнопкой `Restart`, которое завершает `MenuState` и возвращает flow к загрузке.
|
|
||||||
|
|
||||||
## Что сделать
|
|
||||||
|
|
||||||
- Реализовать `MenuState`.
|
|
||||||
- Реализовать `MenuUIView`.
|
|
||||||
- Реализовать `MenuUIViewModel` или аналогичный plain C# объект для restart-сигнала.
|
|
||||||
- В `MenuState.EnterAsync` показать menu View и ожидать restart.
|
|
||||||
- Ожидание restart реализовать через `UniTaskCompletionSource` или аналогичный UniTask-friendly механизм.
|
|
||||||
- По нажатию кнопки `Restart` завершать ожидание `MenuState.EnterAsync`.
|
|
||||||
- В `MenuState.ExitAsync` release-нуть menu View и очистить подписки/listeners.
|
|
||||||
|
|
||||||
## Технические требования
|
|
||||||
|
|
||||||
- Кнопку подключать через `Button.onClick.AddListener(...)`.
|
|
||||||
- В `Release()` снимать ровно тот же listener через `RemoveListener(...)`.
|
|
||||||
- Не использовать `RemoveAllListeners()` как основной способ очистки.
|
|
||||||
- Не использовать анонимную лямбду, которую невозможно симметрично снять.
|
|
||||||
- Не переводить state machine напрямую из `MenuUIView`.
|
|
||||||
- Restart-событие должно попадать во flow через ViewModel/state, а не через глобальное состояние.
|
|
||||||
|
|
||||||
## Критерии готовности
|
|
||||||
|
|
||||||
- `MenuState.EnterAsync` завершается после нажатия `Restart`.
|
|
||||||
- После завершения `MenuState` внешний `BootFlowService` запускает `LoadState`.
|
|
||||||
- Несколько циклов `Menu -> Restart -> Load -> Menu` не дублируют callbacks.
|
|
||||||
- Повторный `Release()` у `MenuUIView` не падает.
|
|
||||||
|
|
||||||
## Заметки
|
|
||||||
|
|
||||||
Для кнопки лучше использовать method group или заранее сохраненный delegate. Это упрощает симметричный `AddListener`/`RemoveListener` и снижает риск дублирующихся callbacks.
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# TASK-0007: Документация и проверка
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
|
|
||||||
Подготовить проект к сдаче: описать запуск, собственные решения, использование AI и проверить основной сценарий.
|
|
||||||
|
|
||||||
## Что сделать
|
|
||||||
|
|
||||||
- Создать или обновить `README.md`.
|
|
||||||
- В `README.md` указать версию Unity, используемые пакеты и стартовую сцену.
|
|
||||||
- В `README.md` описать, как проверить цикл `Splash -> Load -> Menu -> Restart -> Load`.
|
|
||||||
- В `README.md` добавить 5-10 строк о том, что было бы доделано при наличии еще 2 часов.
|
|
||||||
- Создать `SELF_NOTES.md`.
|
|
||||||
- В `SELF_NOTES.md` объяснить ключевые архитектурные решения.
|
|
||||||
- Создать `AI_LOG.md`.
|
|
||||||
- В `AI_LOG.md` описать использование AI или честно указать, что AI не использовался.
|
|
||||||
- По возможности добавить несколько небольших Edit Mode или Play Mode тестов на pure C# слой.
|
|
||||||
|
|
||||||
## Технические требования
|
|
||||||
|
|
||||||
- В `SELF_NOTES.md` объяснить выбор внешнего flow coordinator вместо самопереключающихся states.
|
|
||||||
- В `SELF_NOTES.md` объяснить разделение plain C# orchestration и MonoBehaviour View.
|
|
||||||
- В `SELF_NOTES.md` описать гигиену подписок и повторных входов.
|
|
||||||
- Если тесты добавляются, не усложнять проект сверх задачи.
|
|
||||||
|
|
||||||
## Критерии готовности
|
|
||||||
|
|
||||||
- `README.md` содержит инструкцию запуска и сценарий проверки.
|
|
||||||
- `SELF_NOTES.md` содержит объяснение 2-3 ключевых решений.
|
|
||||||
- `AI_LOG.md` присутствует и честно описывает использование AI.
|
|
||||||
- Основной цикл вручную проверен: `Splash -> Load -> Menu -> Restart -> Load`.
|
|
||||||
- В документации указано фактически потраченное время или место для его заполнения.
|
|
||||||
|
|
||||||
## Заметки
|
|
||||||
|
|
||||||
Документы являются частью оценки. В них нужно показать понимание решений, а не просто перечислить файлы проекта.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# TASK-000X: Название задачи
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
Planned
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
|
|
||||||
Кратко описать, какой результат должен появиться после выполнения задачи.
|
|
||||||
|
|
||||||
## Что сделать
|
|
||||||
|
|
||||||
- Описать конкретные действия, которые нужно выполнить.
|
|
||||||
- Не добавлять лишние пункты, не относящиеся к текущей задаче.
|
|
||||||
- Формулировать действия так, чтобы их можно было проверить после выполнения.
|
|
||||||
|
|
||||||
## Технические требования
|
|
||||||
|
|
||||||
- Указать обязательные технологии, ограничения и архитектурные правила для задачи.
|
|
||||||
- Если специальных требований нет, написать: `Нет дополнительных требований кроме Agent/TASK.md`.
|
|
||||||
|
|
||||||
## Критерии готовности
|
|
||||||
|
|
||||||
- Описать проверяемые признаки завершения задачи.
|
|
||||||
- Каждый критерий должен быть конкретным и однозначным.
|
|
||||||
|
|
||||||
## Заметки
|
|
||||||
|
|
||||||
Дополнительный контекст, пояснения и важные решения. Если заметок нет, написать: `Нет`.
|
|
||||||
+811
-162
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,8 @@ namespace QuizPleaseTest.Boot.States
|
|||||||
{
|
{
|
||||||
public enum BootStateCode
|
public enum BootStateCode
|
||||||
{
|
{
|
||||||
Splash,
|
Splash = 0,
|
||||||
Load,
|
Load = 1,
|
||||||
Menu
|
Menu = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: b4444444444444444444444444444444, type: 3}
|
m_Script: {fileID: 11500000, guid: b4444444444444444444444444444444, type: 3}
|
||||||
m_Name: BootSettings
|
m_Name: BootSettings
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
<SplashDurationSeconds>k__BackingField: 1
|
<SplashDurationSeconds>k__BackingField: 5
|
||||||
<LoadSteps>k__BackingField: 5
|
<LoadSteps>k__BackingField: 5
|
||||||
<LoadStepDurationMs>k__BackingField: 200
|
<LoadStepDurationMs>k__BackingField: 200
|
||||||
|
|||||||
@@ -591,7 +591,7 @@ PlayerSettings:
|
|||||||
webGLTemplate: APPLICATION:Default
|
webGLTemplate: APPLICATION:Default
|
||||||
webGLAnalyzeBuildSize: 0
|
webGLAnalyzeBuildSize: 0
|
||||||
webGLUseEmbeddedResources: 0
|
webGLUseEmbeddedResources: 0
|
||||||
webGLCompressionFormat: 0
|
webGLCompressionFormat: 2
|
||||||
webGLWasmArithmeticExceptions: 0
|
webGLWasmArithmeticExceptions: 0
|
||||||
webGLLinkerTarget: 1
|
webGLLinkerTarget: 1
|
||||||
webGLThreadsSupport: 0
|
webGLThreadsSupport: 0
|
||||||
@@ -608,7 +608,8 @@ PlayerSettings:
|
|||||||
platformArchitecture: {}
|
platformArchitecture: {}
|
||||||
scriptingBackend: {}
|
scriptingBackend: {}
|
||||||
il2cppCompilerConfiguration: {}
|
il2cppCompilerConfiguration: {}
|
||||||
il2cppCodeGeneration: {}
|
il2cppCodeGeneration:
|
||||||
|
WebGL: 1
|
||||||
managedStrippingLevel:
|
managedStrippingLevel:
|
||||||
EmbeddedLinux: 1
|
EmbeddedLinux: 1
|
||||||
GameCoreScarlett: 1
|
GameCoreScarlett: 1
|
||||||
@@ -711,11 +712,11 @@ PlayerSettings:
|
|||||||
apiCompatibilityLevel: 6
|
apiCompatibilityLevel: 6
|
||||||
activeInputHandler: 0
|
activeInputHandler: 0
|
||||||
windowsGamepadBackendHint: 0
|
windowsGamepadBackendHint: 0
|
||||||
cloudProjectId:
|
cloudProjectId: e10a1ee9-c8af-426e-83b9-c733374fcc6a
|
||||||
framebufferDepthMemorylessMode: 0
|
framebufferDepthMemorylessMode: 0
|
||||||
qualitySettingsNames: []
|
qualitySettingsNames: []
|
||||||
projectName:
|
projectName: QuizPleaseTest
|
||||||
organizationId:
|
organizationId: horooko
|
||||||
cloudEnabled: 0
|
cloudEnabled: 0
|
||||||
legacyClampBlendShapeWeights: 0
|
legacyClampBlendShapeWeights: 0
|
||||||
hmiLoadingImage: {fileID: 0}
|
hmiLoadingImage: {fileID: 0}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# QuizPleaseTest Boot Flow
|
||||||
|
|
||||||
|
Unity-проект с загрузочным flow через собственную state machine, VContainer, UniTask и UniRx.
|
||||||
|
|
||||||
|
## Версия и пакеты
|
||||||
|
|
||||||
|
- Unity: `2022.3.62f3`
|
||||||
|
- Стартовая сцена: `Assets/Scenes/SampleScene.unity`
|
||||||
|
- DI: `VContainer`
|
||||||
|
- Async: `UniTask`
|
||||||
|
- Reactive: `UniRx`
|
||||||
|
- UI: `uGUI`, `TextMeshPro`
|
||||||
|
|
||||||
|
## Как запустить
|
||||||
|
|
||||||
|
1. Открыть проект в Unity `2022.3.62f3` или совместимой Unity 2022 LTS.
|
||||||
|
2. Открыть сцену `Assets/Scenes/SampleScene.unity`.
|
||||||
|
3. Убедиться, что на сцене есть `GameLifetimeScope`, `Canvas`, `EventSystem`, `Text (Data_Status)` и `Slider`.
|
||||||
|
4. Нажать `Play`.
|
||||||
|
|
||||||
|
## Как проверить сценарий
|
||||||
|
|
||||||
|
1. После старта статус должен стать `Splash`.
|
||||||
|
2. Затем должен начаться этап `Loading 0%`.
|
||||||
|
3. Во время загрузки статус должен обновляться до `Loading 100%`.
|
||||||
|
4. `Slider` должен двигаться от `0` к `1`.
|
||||||
|
5. После загрузки статус должен стать `Menu`.
|
||||||
|
6. Нажать кнопку `Restart`.
|
||||||
|
7. После нажатия должен снова выполниться цикл `Loading -> Menu`.
|
||||||
|
8. При остановке Play Mode на `Menu` не должно быть `MissingReferenceException`.
|
||||||
|
|
||||||
|
Фактически потраченное время: 1 час 40 минут + 20 минут на написание текста
|
||||||
|
|
||||||
|
Дополнительно можно изменить BootSettings для проверки.
|
||||||
|
|
||||||
|
## Что бы я доделал, будь еще 2 часа
|
||||||
|
|
||||||
|
- Добавил бы Unit/Edit Mode тесты для быстрой проверки state machine, restart-сигнала и порядка `Exit -> Enter`, с комментариями к сценариям.
|
||||||
|
- Настроил бы workflow для автобилдов, чтобы на каждый push проверялись компиляция и базовый build.
|
||||||
|
- Добавил бы отдельный build-скрипт, который можно запускать локально и в CI одной командой.
|
||||||
|
- Добавил бы tween-анимации для появления экранов и progress bar. В идеале сделал бы свои простые tweeners под задачу, а не тянул лишнюю абстракцию (и плагины).
|
||||||
|
- Вынес бы тексты статусов в settings/localization-ready слой, чтобы не держать строки внутри states.
|
||||||
|
- Добавил бы нормальный визуальный layout для Splash, Loading и Menu вместо минимального UI. Вероятно на основе готового ассета, для экономии времени.
|
||||||
|
- Добавил бы Play Mode тест полного цикла `Splash -> Load -> Menu -> Restart -> Load`.
|
||||||
|
- Добавил бы обработку ошибок загрузки и отдельное error-state поведение.
|
||||||
|
- Нормально собрал в Prefab экраны и вынес бы текст как отдельный префаб, чтобы можно было по всему проекту быстро менять шрифт.
|
||||||
@@ -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`.
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
# Тестовое задание — Unity / C# Middle
|
||||||
|
|
||||||
|
> Короткое тестовое задание на **примерно 1,5 часа**.
|
||||||
|
> Нужно выбрать **один из двух вариантов** и сделать его с нуля.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Содержание
|
||||||
|
|
||||||
|
- [Введение](#введение)
|
||||||
|
- [Технические требования](#технические-требования)
|
||||||
|
- [Архитектура](#архитектура)
|
||||||
|
- [Что нужно сдать](#что-нужно-сдать)
|
||||||
|
- [Чего не ждём](#чего-не-ждём)
|
||||||
|
- [Вариант A — Boot Flow](#вариант-a--boot-flow)
|
||||||
|
- [Вариант B — Energy--Regen](#вариант-b--energy--regen)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Введение
|
||||||
|
|
||||||
|
Привет! Спасибо, что нашёл время.
|
||||||
|
|
||||||
|
Ниже — короткое тестовое задание. Внутри есть два варианта. Нужно выбрать **один**.
|
||||||
|
|
||||||
|
Оба варианта равнозначны по сложности, поэтому можно выбрать тот, который ближе по интересу.
|
||||||
|
Задание нужно делать **с нуля**:
|
||||||
|
|
||||||
|
- Unity-проект создаётся самостоятельно;
|
||||||
|
- базовые абстракции пишутся самостоятельно;
|
||||||
|
- архитектурные решения являются частью оценки.
|
||||||
|
|
||||||
|
Результат нужно отправить **до завтра 19:00 по МСК** в Telegram: [@mattnastya](https://t.me/mattnastya).
|
||||||
|
|
||||||
|
Формат домашний, поэтому не требуется идеально попадать во время или доказывать, сколько часов было потрачено.
|
||||||
|
Если ушло больше или меньше 1,5 часов — это нормально. Просто укажи это честно.
|
||||||
|
|
||||||
|
Для нас важнее прозрачность и понимание решений, чем попытка сделать всё идеально любой ценой.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Технические требования
|
||||||
|
|
||||||
|
### Общие требования
|
||||||
|
|
||||||
|
- **Unity 2022 LTS+**
|
||||||
|
- **C#**
|
||||||
|
- Разрешено подключать:
|
||||||
|
- `VContainer`
|
||||||
|
- `UniTask`
|
||||||
|
- `UniRx` / `R3`
|
||||||
|
- свою реактивную реализацию
|
||||||
|
- Опционально:
|
||||||
|
- `Odin Inspector`
|
||||||
|
- `DOTween`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
Ниже перечислены обязательные архитектурные требования, в которые нужно попасть.
|
||||||
|
|
||||||
|
### Reactive
|
||||||
|
|
||||||
|
Можно использовать `UniRx`, `R3` или собственный `ReactiveValue<T>`.
|
||||||
|
|
||||||
|
Минимальная сигнатура:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
IDisposable Subscribe(Action<T> cb, bool invokeImmediately = true);
|
||||||
|
```
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
|
||||||
|
- каждая подписка должна корректно диспозиться;
|
||||||
|
- не должно быть голых `event Action` без отписки;
|
||||||
|
- жизненный цикл подписок должен быть понятным и безопасным.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Async
|
||||||
|
|
||||||
|
Все асинхронные операции должны быть сделаны через:
|
||||||
|
|
||||||
|
- `UniTask`
|
||||||
|
- `CancellationToken`
|
||||||
|
|
||||||
|
`async void` запрещён везде, кроме Unity-колбэков.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Dependency Injection
|
||||||
|
|
||||||
|
Использовать **VContainer**.
|
||||||
|
|
||||||
|
Все сервисы нужно регистрировать через интерфейсы:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Register<Impl>(Lifetime.Singleton).As<IInterface>();
|
||||||
|
```
|
||||||
|
|
||||||
|
Запрещено использовать:
|
||||||
|
|
||||||
|
- `FindObjectOfType`
|
||||||
|
- `Singleton.Instance`
|
||||||
|
- `static`-хранилища состояния
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### UI
|
||||||
|
|
||||||
|
Нужно сделать свою базовую пару классов:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class UIView : MonoBehaviour
|
||||||
|
{
|
||||||
|
public void Initialize();
|
||||||
|
public void Release();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class UIView<TVm> : UIView
|
||||||
|
where TVm : IUIViewModel
|
||||||
|
{
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Требования к UI:
|
||||||
|
|
||||||
|
- `ViewModel` — обычный C#-класс;
|
||||||
|
- `ViewModel` не должен наследоваться от `MonoBehaviour`;
|
||||||
|
- логика должна быть во `ViewModel`;
|
||||||
|
- `View` хранит только Unity-ссылки и биндинги.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Service
|
||||||
|
|
||||||
|
Сервисы должны быть оформлены через `Service : IService` с асинхронным жизненным циклом:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
UniTask InitializeAsync(CancellationToken ct);
|
||||||
|
UniTask ReleaseAsync(CancellationToken ct);
|
||||||
|
```
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
|
||||||
|
- фоновые циклы стартуют в `InitializeAsync`;
|
||||||
|
- фоновые циклы останавливаются через `CancellationTokenSource.Cancel()` в `ReleaseAsync`;
|
||||||
|
- отмена должна быть реальной, а не формальной.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
Конфиги должны быть сделаны через `ScriptableObject` с суффиксом `*Settings`.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[field: SerializeField]
|
||||||
|
public EnergySettings EnergySettings { get; private set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
Регистрация в `LifetimeScope`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.RegisterInstance(_settings);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что нужно сдать
|
||||||
|
|
||||||
|
- GitHub-репозиторий
|
||||||
|
или `.zip`-архив без папок:
|
||||||
|
- `Library/`
|
||||||
|
- `Temp/`
|
||||||
|
- `obj/`
|
||||||
|
|
||||||
|
- `README.md`:
|
||||||
|
- как запустить проект;
|
||||||
|
- 5–10 строк о том, что бы ты доделал, если бы было ещё 2 часа.
|
||||||
|
|
||||||
|
- `SELF_NOTES.md` — обязательный документ про собственные решения.
|
||||||
|
|
||||||
|
В `SELF_NOTES.md` нужно написать своими словами:
|
||||||
|
|
||||||
|
- какие идеи ты рассмотрел;
|
||||||
|
- почему выбрал именно эту реализацию, а не альтернативы;
|
||||||
|
- какие места в коде ты придумал и написал сам, без AI;
|
||||||
|
- почему именно эти места писал сам;
|
||||||
|
- что в коде ты понимаешь до последней строки;
|
||||||
|
- что осталось «магией», если такое есть;
|
||||||
|
- как бы ты объяснил 2–3 ключевых решения, если бы завтра тебя спросили:
|
||||||
|
«Почему здесь сделано именно так?»
|
||||||
|
|
||||||
|
- `AI_LOG.md`:
|
||||||
|
- какие промпты использовал;
|
||||||
|
- где AI ошибся;
|
||||||
|
- что переписал руками;
|
||||||
|
- если AI не использовал — напиши, почему.
|
||||||
|
|
||||||
|
> Важно: нужно показать, что ты понимаешь собственные решения, а не просто копируешь готовый ответ из AI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Чего не ждём
|
||||||
|
|
||||||
|
Не нужно тратить время на:
|
||||||
|
|
||||||
|
- красивый арт;
|
||||||
|
- анимации;
|
||||||
|
- звук;
|
||||||
|
- мобильную сборку.
|
||||||
|
|
||||||
|
Главное:
|
||||||
|
|
||||||
|
- слои;
|
||||||
|
- стиль кода;
|
||||||
|
- архитектура;
|
||||||
|
- понимание собственного решения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Варианты задания
|
||||||
|
|
||||||
|
Нужно выбрать **один** вариант.
|
||||||
|
|
||||||
|
| Вариант | Название | Суть |
|
||||||
|
|---|---|---|
|
||||||
|
| A | Boot Flow | State machine из трёх стейтов |
|
||||||
|
| B | Energy & Regen | Реактивный сервис энергии и UI |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Вариант A — Boot Flow
|
||||||
|
|
||||||
|
> Задача: сделать загрузочный поток приложения через свою стейт-машину.
|
||||||
|
|
||||||
|
## 1. База стейт-машины
|
||||||
|
|
||||||
|
Нужно реализовать свои абстракции:
|
||||||
|
|
||||||
|
- `IState`
|
||||||
|
- `IStatesController<TEnum>`
|
||||||
|
- `StatesController<TEnum>`
|
||||||
|
|
||||||
|
У контроллера должен быть метод:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
UniTask EnterStateAsync(TEnum code, CancellationToken ct);
|
||||||
|
```
|
||||||
|
|
||||||
|
Контракт перехода между стейтами:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await currentState.ExitAsync(ct);
|
||||||
|
await newState.EnterAsync(ct);
|
||||||
|
```
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
|
||||||
|
- сначала вызывается `ExitAsync` текущего стейта;
|
||||||
|
- потом вызывается `EnterAsync` нового стейта;
|
||||||
|
- `CancellationToken` пробрасывается насквозь;
|
||||||
|
- внутренние ожидания должны реально отменяться через `CancellationToken`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Три стейта
|
||||||
|
|
||||||
|
### SplashState
|
||||||
|
|
||||||
|
Должен:
|
||||||
|
|
||||||
|
- показать лого;
|
||||||
|
- подождать 1 секунду через `UniTask.Delay(..., ct)`;
|
||||||
|
- перейти дальше.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LoadState
|
||||||
|
|
||||||
|
Должен имитировать загрузку:
|
||||||
|
|
||||||
|
- 5 шагов;
|
||||||
|
- каждый шаг длится 200 мс;
|
||||||
|
- на стейте лежит реактивное значение прогресса:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
ReactiveValue<float> Progress;
|
||||||
|
```
|
||||||
|
|
||||||
|
Прогресс:
|
||||||
|
|
||||||
|
- диапазон от `0` до `1`;
|
||||||
|
- обновляется после каждого шага загрузки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MenuState
|
||||||
|
|
||||||
|
Должен:
|
||||||
|
|
||||||
|
- показать `MenuUIView`;
|
||||||
|
- содержать одну кнопку `Restart`;
|
||||||
|
- по клику возвращать приложение в `LoadState`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. UI
|
||||||
|
|
||||||
|
`LoadingUIView` должен быть подписан на:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
LoadState.Progress
|
||||||
|
```
|
||||||
|
|
||||||
|
Он должен двигать прогресс-бар:
|
||||||
|
|
||||||
|
- через `DOTween`;
|
||||||
|
- или через ручной `Lerp`;
|
||||||
|
- способ не принципиален.
|
||||||
|
|
||||||
|
Требования к жизненному циклу:
|
||||||
|
|
||||||
|
- при `ExitAsync` все подписки должны корректно диспозиться;
|
||||||
|
- при повторном входе не должно быть `NullReferenceException`;
|
||||||
|
- повторный вход в стейт должен работать стабильно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Вариант B — Energy & Regen
|
||||||
|
|
||||||
|
> Задача: сделать систему энергии для мобильной игры.
|
||||||
|
|
||||||
|
## 1. EnergySettings
|
||||||
|
|
||||||
|
Нужно создать `ScriptableObject`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class EnergySettings : ScriptableObject
|
||||||
|
{
|
||||||
|
public int MaxEnergy;
|
||||||
|
public float RegenSeconds;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Поля:
|
||||||
|
|
||||||
|
- `MaxEnergy` — максимальное количество энергии;
|
||||||
|
- `RegenSeconds` — количество секунд на восстановление одной единицы энергии.
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
|
||||||
|
- создать asset с настройками;
|
||||||
|
- заинжектить его в `LifetimeScope`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. IEnergyService / EnergyService
|
||||||
|
|
||||||
|
Нужно реализовать:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IEnergyService
|
||||||
|
{
|
||||||
|
IReadOnlyReactiveValue<int> Current { get; }
|
||||||
|
IReadOnlyReactiveValue<float> SecondsToNext { get; }
|
||||||
|
|
||||||
|
bool TrySpend(int amount);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`EnergyService` должен наследоваться от `Service`.
|
||||||
|
|
||||||
|
### Поля
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
IReadOnlyReactiveValue<int> Current;
|
||||||
|
IReadOnlyReactiveValue<float> SecondsToNext;
|
||||||
|
```
|
||||||
|
|
||||||
|
`SecondsToNext` — это доля прогресса до следующей единицы энергии:
|
||||||
|
|
||||||
|
- `0` — восстановление только началось;
|
||||||
|
- `1` — следующая единица почти восстановлена.
|
||||||
|
|
||||||
|
### Метод
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
bool TrySpend(int amount);
|
||||||
|
```
|
||||||
|
|
||||||
|
Метод должен:
|
||||||
|
|
||||||
|
- проверять, хватает ли энергии;
|
||||||
|
- если хватает — списывать энергию и возвращать `true`;
|
||||||
|
- если не хватает — ничего не менять и возвращать `false`.
|
||||||
|
|
||||||
|
### Регенерация
|
||||||
|
|
||||||
|
Регенерация должна быть фоновой `UniTask`-петлёй внутри сервиса.
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
|
||||||
|
- петля стартует в `InitializeAsync`;
|
||||||
|
- петля корректно останавливается в `ReleaseAsync`;
|
||||||
|
- остановка делается через собственный `CancellationTokenSource`;
|
||||||
|
- если `Current == MaxEnergy`, петля должна спать эффективно;
|
||||||
|
- нельзя крутить `Delay(0)` внутри `while`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. UI
|
||||||
|
|
||||||
|
Нужно реализовать:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
EnergyBarUIView<EnergyBarUIViewModel>
|
||||||
|
```
|
||||||
|
|
||||||
|
UI должен показывать:
|
||||||
|
|
||||||
|
- текст `current / max`;
|
||||||
|
- прогресс-бар:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Image.fillAmount = SecondsToNext;
|
||||||
|
```
|
||||||
|
|
||||||
|
- кнопку `Потратить 10`.
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
|
||||||
|
- биндинги делаются через `ReactiveValue.Subscribe(...)`;
|
||||||
|
- отписки выполняются в `Release()`;
|
||||||
|
- запрещено обновлять UI через `Update()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Критерии, на которые будут смотреть
|
||||||
|
|
||||||
|
- Понятная архитектура.
|
||||||
|
- Корректная работа с жизненным циклом.
|
||||||
|
- Отсутствие скрытых синглтонов и статического состояния.
|
||||||
|
- Корректная отмена `UniTask`-операций.
|
||||||
|
- Чистые подписки и отписки.
|
||||||
|
- Разделение логики между `View`, `ViewModel` и сервисами.
|
||||||
|
- Понимание решений, описанное в `SELF_NOTES.md`.
|
||||||
Reference in New Issue
Block a user