diff --git a/.gitignore b/.gitignore index d436863..f10bee3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,12 @@ !/ProjectSettings/ !/Packages/ !/docs/ -!/Agent/ !.gitignore !.gitattributes !LICENSE !README.md +!AI_LOG.md +!SELF_NOTES.md .DS_Store .Spotlight-V100 diff --git a/AI_LOG.md b/AI_LOG.md new file mode 100644 index 0000000..ea25d11 --- /dev/null +++ b/AI_LOG.md @@ -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-ов. diff --git a/Agent/Agent.md b/Agent/Agent.md deleted file mode 100644 index ca5fe56..0000000 --- a/Agent/Agent.md +++ /dev/null @@ -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(Lifetime.Singleton).As()`. -- 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` и подготовить проект так, чтобы его можно было проверить по описанным требованиям. diff --git a/Agent/TASK.md b/Agent/TASK.md deleted file mode 100644 index 65eb135..0000000 --- a/Agent/TASK.md +++ /dev/null @@ -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`, она должна поддерживать сигнатуру подписки: - -```csharp -IDisposable Subscribe(Action cb, bool invokeImmediately = true) -``` - -Требования: - -- каждая подписка должна корректно диспозиться; -- нельзя использовать голые `event Action` без отписки. - -### Async - -Все async-операции должны быть реализованы только через UniTask и `CancellationToken`. - -Запрещено использовать `async void`, кроме Unity-колбэков. - -### Dependency Injection - -Использовать VContainer. - -Все сервисы регистрируются как интерфейсы: - -```csharp -builder.Register(Lifetime.Singleton).As(); -``` - -Запрещено использовать: - -- `FindObjectOfType`; -- `Singleton.Instance`; -- `static`-хранилища состояния. - -### UI - -Нужно сделать базовую пару View/ViewModel: - -```csharp -public class UIView : MonoBehaviour -{ - public virtual void Initialize() { } - public virtual void Release() { } -} - -public class UIView : 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`; -- `StatesController`. - -В `StatesController` должен быть метод: - -```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 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 не использовался, почему было принято такое решение. - -## Что не требуется - -Не нужно тратить время на: - -- красивый арт; -- сложные анимации; -- звук; -- мобильную сборку. - -Главное: архитектурные слои, стиль кода, корректная работа жизненного цикла и понимание собственных решений. diff --git a/Agent/Task/TASK-0001.md b/Agent/Task/TASK-0001.md deleted file mode 100644 index 811b00e..0000000 --- a/Agent/Task/TASK-0001.md +++ /dev/null @@ -1,40 +0,0 @@ -# TASK-0001: Composition Root и зависимости - -## Статус - -Ready - -## Цель - -Настроить корневую точку композиции проекта через VContainer так, чтобы orchestration-логика жила в plain C# классах, а MonoBehaviour использовались только как View-компоненты сцены. - -## Что сделать - -- Создать `GameLifetimeScope`. -- Зарегистрировать сервисы и контроллеры через VContainer как managed dependencies. -- Зарегистрировать `BootstrapEntryPoint` как entry point приложения. -- Зарегистрировать `BootStatesController` как `IStatesController` и при необходимости как self. -- Зарегистрировать `SplashState`, `LoadState`, `MenuState`. -- Зарегистрировать scene View через `RegisterComponent(...)`. -- Зарегистрировать settings asset через `[SerializeField]` и `RegisterInstance(...)`. - -## Технические требования - -- Использовать VContainer. -- Сервисы регистрировать через `builder.Register(Lifetime.Singleton).As()`. -- Не использовать `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(...)`, потому что это данные, а не сервис с жизненным циклом. diff --git a/Agent/Task/TASK-0002.md b/Agent/Task/TASK-0002.md deleted file mode 100644 index 6323788..0000000 --- a/Agent/Task/TASK-0002.md +++ /dev/null @@ -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` с методом `EnterStateAsync(TEnum code, CancellationToken ct)`. -- Реализовать `StatesController`. -- Создать `BootStateCode` для состояний `Splash`, `Load`, `Menu`. -- Создать `BootStatesController`, который собирает конкретные boot states и передает их в базовый controller. - -## Технические требования - -- Использовать UniTask для всех async-методов. -- Во все async-операции передавать `CancellationToken`. -- Контракт перехода должен быть строгим: сначала `ExitAsync` текущего state, затем `EnterAsync` нового state. -- `StatesController` не должен знать бизнес-логику 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. diff --git a/Agent/Task/TASK-0003.md b/Agent/Task/TASK-0003.md deleted file mode 100644 index 95ce4a1..0000000 --- a/Agent/Task/TASK-0003.md +++ /dev/null @@ -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. diff --git a/Agent/Task/TASK-0004.md b/Agent/Task/TASK-0004.md deleted file mode 100644 index 897f4f6..0000000 --- a/Agent/Task/TASK-0004.md +++ /dev/null @@ -1,37 +0,0 @@ -# TASK-0004: UI база и ViewModel слой - -## Статус - -Ready - -## Цель - -Создать базовый UI слой, в котором View отвечает только за Unity-ссылки и биндинги, а логика находится во ViewModel или state/flow сервисах. - -## Что сделать - -- Создать `IUIViewModel`. -- Создать `UIView : MonoBehaviour` с методами `Initialize()` и `Release()`. -- Создать `UIView : 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`. -- ViewModel можно передать во View явно перед `Initialize()`. -- `Release()` безопасен при повторном вызове. -- View не вызывает `FindObjectOfType`, не хранит глобальное состояние и не управляет переходами state machine. - -## Заметки - -MonoBehaviour должны оставаться presentation layer. Это соответствует задаче: логика находится в VM или сервисах, View только показывает состояние и прокидывает UI-события. diff --git a/Agent/Task/TASK-0005.md b/Agent/Task/TASK-0005.md deleted file mode 100644 index 21c8859..0000000 --- a/Agent/Task/TASK-0005.md +++ /dev/null @@ -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. diff --git a/Agent/Task/TASK-0006.md b/Agent/Task/TASK-0006.md deleted file mode 100644 index 36b5eae..0000000 --- a/Agent/Task/TASK-0006.md +++ /dev/null @@ -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. diff --git a/Agent/Task/TASK-0007.md b/Agent/Task/TASK-0007.md deleted file mode 100644 index 3921b9b..0000000 --- a/Agent/Task/TASK-0007.md +++ /dev/null @@ -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`. -- В документации указано фактически потраченное время или место для его заполнения. - -## Заметки - -Документы являются частью оценки. В них нужно показать понимание решений, а не просто перечислить файлы проекта. diff --git a/Agent/Task/TASK_TEMPLATE.md b/Agent/Task/TASK_TEMPLATE.md deleted file mode 100644 index 1f6f107..0000000 --- a/Agent/Task/TASK_TEMPLATE.md +++ /dev/null @@ -1,29 +0,0 @@ -# TASK-000X: Название задачи - -## Статус - -Planned - -## Цель - -Кратко описать, какой результат должен появиться после выполнения задачи. - -## Что сделать - -- Описать конкретные действия, которые нужно выполнить. -- Не добавлять лишние пункты, не относящиеся к текущей задаче. -- Формулировать действия так, чтобы их можно было проверить после выполнения. - -## Технические требования - -- Указать обязательные технологии, ограничения и архитектурные правила для задачи. -- Если специальных требований нет, написать: `Нет дополнительных требований кроме Agent/TASK.md`. - -## Критерии готовности - -- Описать проверяемые признаки завершения задачи. -- Каждый критерий должен быть конкретным и однозначным. - -## Заметки - -Дополнительный контекст, пояснения и важные решения. Если заметок нет, написать: `Нет`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8af74e6 --- /dev/null +++ b/README.md @@ -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 экраны и вынес бы текст как отдельный префаб, чтобы можно было по всему проекту быстро менять шрифт. diff --git a/SELF_NOTES.md b/SELF_NOTES.md new file mode 100644 index 0000000..7307bbb --- /dev/null +++ b/SELF_NOTES.md @@ -0,0 +1,88 @@ +# SELF_NOTES + +## Какие варианты я рассматривал + +Я смотрел два варианта управления flow. + +Первый вариант- сделать состояния более самостоятельными. Например, чтобы `SplashState` сам запускал переход в `LoadState`, потом `LoadState` сам вел в `MenuState`, а `MenuState` возвращал flow обратно в `LoadState`. + +От этого варианта я отказался. В таком подходе state machine слишком быстро начинает знать почти весь сценарий. Из-за этого код сложнее проверять, сложнее менять и проще случайно сломать. + +Второй вариант- оставить states спокойными и без лишней логики, а сам порядок экранов держать отдельно, во внешнем `BootFlowService`. Этот вариант я и выбрал. + +`StatesController` просто переключает состояния по понятному порядку `ExitAsync -> EnterAsync`. А `BootFlowService` уже решает, куда идти дальше `Splash -> Load -> Menu -> Load`. + +Так state machine не смешивается с boot-сценарием и остается более общей. + +Еще я думал запускать flow из `монобех.Start()`, но в итоге отказался. В проекте используется VContainer, поэтому старт сделан через entry point `BootstrapEntryPoint`. Так зависимости создаются контейнером, а не руками в сценовом компоненте. + +## Почему я выбрал текущий вариант + +Основная мысль простая, монобех остается View-слоем, а орекстрация живет в обычных C# классах. + +- `GameLifetimeScope` собирает зависимости +- `BootstrapEntryPoint` запускает boot lifecycle +- `BootFlowService` ведет общий сценарий +- `StatesController` отвечает только за переходы между состояниями +- `SplashState`, `LoadState`, `MenuState` отвечают за вход и выход своего состояния +- `UIView` и наследники держат Unity-ссылки и биндинги +- ViewModel-классы не наследуются от `монобех` + +Так код проще читать. Если нужно понять порядок экранов, я иду в `BootFlowService`. Если нужно понять UI-логику, смотрю View. Если нужно понять переходы между состояниями, смотрю `StatesController`. + +## Что я писал и продумывал сам + +Я сам выбрал основные границы ответственности. Вынес 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`. Он получает код состояния, выходит из текущего 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(Lifetime.Singleton).As(); +``` + +На практическом уровне я понимаю, что контейнер создает `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`.