Независимый UI слой — ускоряем разработку UI
Это первая статья из цикла “Ускоряем разработку UI”. В этом цикле я хочу поднять проблему, которая у нас остро стояла на нескольких проектах. Одной из главных головных болей в повседневной разработке у нас частенько были UI задачи:
- UI, как правило, пишется не изолировано, поэтому протестировать его, например, на отдельной сцене, может быть проблематично
- Дизайнеры креативят то, что сложно впихнуть в игру: постоянная борьба за размер текстур, сложность реализации тех или иных компонентов и т.д.
- Нет дизайн гайдбука, где описаны все цвета, элементы и т.д. Многие вещи делаются на глаз, что порождает большое количество итераций по правкам дизайнера в стиле “тут на два пикселя больше, а тут цвет не тот”.
- Из предыдущего пункта выливается отсутствие стандартных элементов, переиспользуемых в UI. Одинаковые вещи делаются разными людьми, привнося разные баги.
Это только то что, что сразу пришло на ум. Конечно же, со всем этим можно, и нужно разбираться.
Изолированное тестирование UI
Один из сложнейших случаев тестирования: есть какой-либо игровой ивент, завязанный на сервер, ограниченный временными рамками, и работающий только при определенном количестве игроков.
Чтобы проверить поведение UI в таких сложно-воспроизводимых условиях, зачастую пишутся хаки/тулы и т.д., чтобы их обеспечить. Это может и работает, но требует все равно приличных усилий на поддержку, и много телодвижений при использовании.
Очень ломает со всем этим заморачиваться, когда нужно проверить что-нибудь тривиально, например, действительно ли появляется спиннер, если мы кликнем на кнопку в UI ивента.
Мы, разработчики, очень ленивые люди. Нередки ситуации, когда кто-то шлепнет фикс не глядя, авось прокатит, а тестер потом нам сообщит если что не так. Если таких итераций несколько, то это огромная трата времени, как разработчика, так и отдела тестирования.
Чтобы убрать издержки на тестирование в таких ситуациях, нужно обеспечить легкий способ проверять правки. Как правило, большинство UI элементов имеют всего-лишь несколько состояний, которые довольно просто проверять изолировано.
К сожалению, я редко вижу практику, когда UI элементы изолируют от остального кода. UI кишит привязками к синглтонам, моделям, которые не имеют никакого отношения к UI, системным сервисам, аналитике, и так далее. А это ведет нас к истокам проблемы.
Различные шаблоны UI не просто так придумали. Но почему то считается, что к разработке игр, в частности на Unity, они не особенно применимы. Лично я убедился, что это не так.
Это типичная архитектура UI. Я тут не привязывался к конкретному паттерну, это может быть MVP/MVC/MVVM, не суть. Во всех шаблонах есть четкое разделение на слои. Если это разделение отсутствует, то тестировать UI изолированно не выйдет.
Ключевые моменты, на которые стоит обратить внимание:
- У бизнес логики и UI разные модели. Это позволяет использовать одни и те же компоненты UI в привязке к разным частям бизнес логики. Плюс вы всегда можете, например, перетащить такой UI в другой проект, или даже организовать репозиторий с общими компонентами.
- View Script максимально тупой. Он либо передает пользовательский ввод в слой бизнес логики, либо реагирует на обновление view model со стороны бизнес логики. _ Изменение моделей однонаправленное, вьюха напрямую не меняет модели. Это упрощает понимание и отладку логики.
- Добавьте RX и получится неплохой микс
Не смотря на то, что такое разделение может показаться избыточным, на практике – это очень удобно.
- UI складывается из различных компонентов, которые можно переиспользовать, комбинировать совершенно независимо.
- Тестировать конкретный компонент UI можно вытащив на отдельную сцену, ведь нет завязки на какие-либо компоненты и модели бизнес-логики
- Если хорошенько поработать на тулами, встроить их в редактор, то интерфейсы строить сможет даже дизайнер
- Если UI скрипты скрыта за интерфейсами, то бизнес логику можно тестировать независимо от UI, юнит тестами.
- Работу над UI и бизнес-логикой можно вести параллельно, обговорив контракт, в виде интерфейсов
Немного кода
Я решил в качестве примера накидать простенькое окно, которое может быть в трех состояниях:
- Загрузка
- Ошибка
- Отображение контента
Для простоты, контент – строки.
Префаб окна выглядит примерно так:
Обычно дизайн UI я начинаю с модели для конкретного компонента. Для начала обозначим три взаимоисключающих состояния окна.
Далее определимся какие данные нужны для отображения в окне, и зафиксируем их в контракте-интерфейсе:
Сформировав уже на этом этапе контракт, разработка UI и бизнес логики могла бы происходить параллельно.
В некоторых случаях интерфейс для модели может быть избыточным. Но я нахожу интерфейсы более гибкими, так как они позволяют отвязаться от наследования.
Тогда моделью может выступать любой класс, например, сам презентер/контроллер. Было бы невозможно его одновременно унаследовать и от базового класса, и от модели.
Дефолтная имплементация модели пригодится для теста, при желании ее можно использоват и в бизнес логике.
Реализация самого окна сводится просто к байндингам:
Класс, теперь есть готовое окно. Но в стандартном подходе, мы бы не смогли потестить его, пока не готова бизнес логика. Она в свое время может быть завязана, например на сервер, или на другие части системы, которые еще не готовы.
На самом деле, у нас уже есть все, чтобы полноценно проверить все состояния UI.
Я создал отдельную сцену для теста окна, и отдельный скрипт, позволяющий проверить каждое из его состояний.
Скрипт посылает в модель соответствующие каждому состоянию данные. Отдельно я вытащил кнопки на каждое состояние и привязал их к скрипту.
Окно вставлено в сцену как префаб, тестовый скрипт – отдельная сущность. Таким образом мы можем играться с тестом не затрагивая реализацию окна.
Вот так это выглядит в живую:
Подводим итоги
В данной статье я продемонстрировал всего лишь наброски мыслей на тему. Подход может быть доработан и адаптирован под проект. Кто-то найдет некоторые части излишними и обойдется без них. Но в долгосрочной перспективе иметь UI как отдельный слой – однозначно выгодно.