[Add] Task
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
!README.md
|
!README.md
|
||||||
!AI_LOG.md
|
!AI_LOG.md
|
||||||
!SELF_NOTES.md
|
!SELF_NOTES.md
|
||||||
|
!Task.md
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.Spotlight-V100
|
.Spotlight-V100
|
||||||
|
|||||||
@@ -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`.
|
||||||
Reference in New Issue
Block a user