# Тестовое задание — 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`. Минимальная сигнатура: ```csharp IDisposable Subscribe(Action cb, bool invokeImmediately = true); ``` Важно: - каждая подписка должна корректно диспозиться; - не должно быть голых `event Action` без отписки; - жизненный цикл подписок должен быть понятным и безопасным. --- ### Async Все асинхронные операции должны быть сделаны через: - `UniTask` - `CancellationToken` `async void` запрещён везде, кроме Unity-колбэков. --- ### Dependency Injection Использовать **VContainer**. Все сервисы нужно регистрировать через интерфейсы: ```csharp builder.Register(Lifetime.Singleton).As(); ``` Запрещено использовать: - `FindObjectOfType` - `Singleton.Instance` - `static`-хранилища состояния --- ### UI Нужно сделать свою базовую пару классов: ```csharp public class UIView : MonoBehaviour { public void Initialize(); public void Release(); } ``` ```csharp public class UIView : 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` - `StatesController` У контроллера должен быть метод: ```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 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 Current { get; } IReadOnlyReactiveValue SecondsToNext { get; } bool TrySpend(int amount); } ``` `EnergyService` должен наследоваться от `Service`. ### Поля ```csharp IReadOnlyReactiveValue Current; IReadOnlyReactiveValue SecondsToNext; ``` `SecondsToNext` — это доля прогресса до следующей единицы энергии: - `0` — восстановление только началось; - `1` — следующая единица почти восстановлена. ### Метод ```csharp bool TrySpend(int amount); ``` Метод должен: - проверять, хватает ли энергии; - если хватает — списывать энергию и возвращать `true`; - если не хватает — ничего не менять и возвращать `false`. ### Регенерация Регенерация должна быть фоновой `UniTask`-петлёй внутри сервиса. Требования: - петля стартует в `InitializeAsync`; - петля корректно останавливается в `ReleaseAsync`; - остановка делается через собственный `CancellationTokenSource`; - если `Current == MaxEnergy`, петля должна спать эффективно; - нельзя крутить `Delay(0)` внутри `while`. --- ## 3. UI Нужно реализовать: ```csharp EnergyBarUIView ``` UI должен показывать: - текст `current / max`; - прогресс-бар: ```csharp Image.fillAmount = SecondsToNext; ``` - кнопку `Потратить 10`. Требования: - биндинги делаются через `ReactiveValue.Subscribe(...)`; - отписки выполняются в `Release()`; - запрещено обновлять UI через `Update()`. --- ## Критерии, на которые будут смотреть - Понятная архитектура. - Корректная работа с жизненным циклом. - Отсутствие скрытых синглтонов и статического состояния. - Корректная отмена `UniTask`-операций. - Чистые подписки и отписки. - Разделение логики между `View`, `ViewModel` и сервисами. - Понимание решений, описанное в `SELF_NOTES.md`.