Files
2026-05-27 11:21:24 +07:00

453 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Тестовое задание — 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`.