13 KiB
Тестовое задание — Unity / C# Middle
Короткое тестовое задание на примерно 1,5 часа.
Нужно выбрать один из двух вариантов и сделать его с нуля.
Содержание
- Введение
- Технические требования
- Архитектура
- Что нужно сдать
- Чего не ждём
- Вариант A — Boot Flow
- Вариант B — Energy--Regen
Введение
Привет! Спасибо, что нашёл время.
Ниже — короткое тестовое задание. Внутри есть два варианта. Нужно выбрать один.
Оба варианта равнозначны по сложности, поэтому можно выбрать тот, который ближе по интересу.
Задание нужно делать с нуля:
- Unity-проект создаётся самостоятельно;
- базовые абстракции пишутся самостоятельно;
- архитектурные решения являются частью оценки.
Результат нужно отправить до завтра 19:00 по МСК в Telegram: @mattnastya.
Формат домашний, поэтому не требуется идеально попадать во время или доказывать, сколько часов было потрачено.
Если ушло больше или меньше 1,5 часов — это нормально. Просто укажи это честно.
Для нас важнее прозрачность и понимание решений, чем попытка сделать всё идеально любой ценой.
Технические требования
Общие требования
- Unity 2022 LTS+
- C#
- Разрешено подключать:
VContainerUniTaskUniRx/R3- свою реактивную реализацию
- Опционально:
Odin InspectorDOTween
Архитектура
Ниже перечислены обязательные архитектурные требования, в которые нужно попасть.
Reactive
Можно использовать UniRx, R3 или собственный ReactiveValue<T>.
Минимальная сигнатура:
IDisposable Subscribe(Action<T> cb, bool invokeImmediately = true);
Важно:
- каждая подписка должна корректно диспозиться;
- не должно быть голых
event Actionбез отписки; - жизненный цикл подписок должен быть понятным и безопасным.
Async
Все асинхронные операции должны быть сделаны через:
UniTaskCancellationToken
async void запрещён везде, кроме Unity-колбэков.
Dependency Injection
Использовать VContainer.
Все сервисы нужно регистрировать через интерфейсы:
builder.Register<Impl>(Lifetime.Singleton).As<IInterface>();
Запрещено использовать:
FindObjectOfTypeSingleton.Instancestatic-хранилища состояния
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. База стейт-машины
Нужно реализовать свои абстракции:
IStateIStatesController<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.