[Add] Task

This commit is contained in:
2026-05-27 11:21:24 +07:00
parent 0b088d112c
commit f3e479a45b
2 changed files with 453 additions and 0 deletions
+452
View File
@@ -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`.