diff --git a/.gitignore b/.gitignore index f10bee3..67cc1ac 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ !README.md !AI_LOG.md !SELF_NOTES.md +!Task.md .DS_Store .Spotlight-V100 diff --git a/Task.md b/Task.md new file mode 100644 index 0000000..426f423 --- /dev/null +++ b/Task.md @@ -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`. + +Минимальная сигнатура: + +```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`.