Files
QuizPlease/Task.md
T
2026-05-27 11:21:24 +07:00

13 KiB
Raw Blame History

Тестовое задание — Unity / C# Middle

Короткое тестовое задание на примерно 1,5 часа.
Нужно выбрать один из двух вариантов и сделать его с нуля.


Содержание


Введение

Привет! Спасибо, что нашёл время.

Ниже — короткое тестовое задание. Внутри есть два варианта. Нужно выбрать один.

Оба варианта равнозначны по сложности, поэтому можно выбрать тот, который ближе по интересу.
Задание нужно делать с нуля:

  • Unity-проект создаётся самостоятельно;
  • базовые абстракции пишутся самостоятельно;
  • архитектурные решения являются частью оценки.

Результат нужно отправить до завтра 19:00 по МСК в Telegram: @mattnastya.

Формат домашний, поэтому не требуется идеально попадать во время или доказывать, сколько часов было потрачено.
Если ушло больше или меньше 1,5 часов — это нормально. Просто укажи это честно.

Для нас важнее прозрачность и понимание решений, чем попытка сделать всё идеально любой ценой.


Технические требования

Общие требования

  • Unity 2022 LTS+
  • C#
  • Разрешено подключать:
    • VContainer
    • UniTask
    • UniRx / R3
    • свою реактивную реализацию
  • Опционально:
    • Odin Inspector
    • DOTween

Архитектура

Ниже перечислены обязательные архитектурные требования, в которые нужно попасть.

Reactive

Можно использовать UniRx, R3 или собственный ReactiveValue<T>.

Минимальная сигнатура:

IDisposable Subscribe(Action<T> cb, bool invokeImmediately = true);

Важно:

  • каждая подписка должна корректно диспозиться;
  • не должно быть голых event Action без отписки;
  • жизненный цикл подписок должен быть понятным и безопасным.

Async

Все асинхронные операции должны быть сделаны через:

  • UniTask
  • CancellationToken

async void запрещён везде, кроме Unity-колбэков.


Dependency Injection

Использовать VContainer.

Все сервисы нужно регистрировать через интерфейсы:

builder.Register<Impl>(Lifetime.Singleton).As<IInterface>();

Запрещено использовать:

  • FindObjectOfType
  • Singleton.Instance
  • static-хранилища состояния

UI

Нужно сделать свою базовую пару классов:

public class UIView : MonoBehaviour
{
    public void Initialize();
    public void Release();
}
public class UIView<TVm> : UIView
    where TVm : IUIViewModel
{
}

Требования к UI:

  • ViewModel — обычный C#-класс;
  • ViewModel не должен наследоваться от MonoBehaviour;
  • логика должна быть во ViewModel;
  • View хранит только Unity-ссылки и биндинги.

Service

Сервисы должны быть оформлены через Service : IService с асинхронным жизненным циклом:

UniTask InitializeAsync(CancellationToken ct);
UniTask ReleaseAsync(CancellationToken ct);

Требования:

  • фоновые циклы стартуют в InitializeAsync;
  • фоновые циклы останавливаются через CancellationTokenSource.Cancel() в ReleaseAsync;
  • отмена должна быть реальной, а не формальной.

Config

Конфиги должны быть сделаны через ScriptableObject с суффиксом *Settings.

Пример:

[field: SerializeField]
public EnergySettings EnergySettings { get; private set; }

Регистрация в LifetimeScope:

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>

У контроллера должен быть метод:

UniTask EnterStateAsync(TEnum code, CancellationToken ct);

Контракт перехода между стейтами:

await currentState.ExitAsync(ct);
await newState.EnterAsync(ct);

Требования:

  • сначала вызывается ExitAsync текущего стейта;
  • потом вызывается EnterAsync нового стейта;
  • CancellationToken пробрасывается насквозь;
  • внутренние ожидания должны реально отменяться через CancellationToken.

2. Три стейта

SplashState

Должен:

  • показать лого;
  • подождать 1 секунду через UniTask.Delay(..., ct);
  • перейти дальше.

LoadState

Должен имитировать загрузку:

  • 5 шагов;
  • каждый шаг длится 200 мс;
  • на стейте лежит реактивное значение прогресса:
ReactiveValue<float> Progress;

Прогресс:

  • диапазон от 0 до 1;
  • обновляется после каждого шага загрузки.

MenuState

Должен:

  • показать MenuUIView;
  • содержать одну кнопку Restart;
  • по клику возвращать приложение в LoadState.

3. UI

LoadingUIView должен быть подписан на:

LoadState.Progress

Он должен двигать прогресс-бар:

  • через DOTween;
  • или через ручной Lerp;
  • способ не принципиален.

Требования к жизненному циклу:

  • при ExitAsync все подписки должны корректно диспозиться;
  • при повторном входе не должно быть NullReferenceException;
  • повторный вход в стейт должен работать стабильно.

Вариант B — Energy & Regen

Задача: сделать систему энергии для мобильной игры.

1. EnergySettings

Нужно создать ScriptableObject:

public class EnergySettings : ScriptableObject
{
    public int MaxEnergy;
    public float RegenSeconds;
}

Поля:

  • MaxEnergy — максимальное количество энергии;
  • RegenSeconds — количество секунд на восстановление одной единицы энергии.

Требования:

  • создать asset с настройками;
  • заинжектить его в LifetimeScope.

2. IEnergyService / EnergyService

Нужно реализовать:

public interface IEnergyService
{
    IReadOnlyReactiveValue<int> Current { get; }
    IReadOnlyReactiveValue<float> SecondsToNext { get; }

    bool TrySpend(int amount);
}

EnergyService должен наследоваться от Service.

Поля

IReadOnlyReactiveValue<int> Current;
IReadOnlyReactiveValue<float> SecondsToNext;

SecondsToNext — это доля прогресса до следующей единицы энергии:

  • 0 — восстановление только началось;
  • 1 — следующая единица почти восстановлена.

Метод

bool TrySpend(int amount);

Метод должен:

  • проверять, хватает ли энергии;
  • если хватает — списывать энергию и возвращать true;
  • если не хватает — ничего не менять и возвращать false.

Регенерация

Регенерация должна быть фоновой UniTask-петлёй внутри сервиса.

Требования:

  • петля стартует в InitializeAsync;
  • петля корректно останавливается в ReleaseAsync;
  • остановка делается через собственный CancellationTokenSource;
  • если Current == MaxEnergy, петля должна спать эффективно;
  • нельзя крутить Delay(0) внутри while.

3. UI

Нужно реализовать:

EnergyBarUIView<EnergyBarUIViewModel>

UI должен показывать:

  • текст current / max;
  • прогресс-бар:
Image.fillAmount = SecondsToNext;
  • кнопку Потратить 10.

Требования:

  • биндинги делаются через ReactiveValue.Subscribe(...);
  • отписки выполняются в Release();
  • запрещено обновлять UI через Update().

Критерии, на которые будут смотреть

  • Понятная архитектура.
  • Корректная работа с жизненным циклом.
  • Отсутствие скрытых синглтонов и статического состояния.
  • Корректная отмена UniTask-операций.
  • Чистые подписки и отписки.
  • Разделение логики между View, ViewModel и сервисами.
  • Понимание решений, описанное в SELF_NOTES.md.