Пользовательский интерфейс в Unity.
Привет! Сегодня хочу поделиться опытом, как я реализовывал пользовательский интерфейс в своём первом проекте на Unity. Про сам проект можете почитать в профиле)
Тема пользовательского интерфейса никогда не была мне близка. А применительно к игре она меня просто пугает. Сейчас моя цель — сделать вертикальный срез, поэтому долго возиться с меню и кнопками я не планирую. Но в то же время я постарался заложить возможность масштабировать решение на готовую игру в будущем.
У меня уже есть пост про общую архитектуру игры. Некоторые моменты будут понятнее, если предварительно с ним ознакомиться.
Итак, я начал с того, что сделал набросок UX в первом приближении:
Для прототипа мне не нужна вариативность в выборе мира и возможность сохранять игру, но мне важно понимать, что в будущем у меня такая потребность возникнет, и я должен вести разработку с поправкой на это.
Все базовые элементы UI я решил наследовать от единого абстрактного класса UIElement, который, в свою очередь, является MonoBehaviour. Ниже приведу полный код этого класса.
В основном этот класс отвечает за взаимодействие с сервисом локализации. Почти у каждого элемента интерфейса есть заголовок. И наследование от UIElement позволяет не думать об отдельной реализации для кнопок, слайдеров и т. д.
В поле textComponent как раз и хранится элемент заголовка, который нужно локализовать, а метод InitializeTextComponent() отвечает за то, чтобы каждый из потомков по-своему его определял. Логика инициализации этого поля может отличаться в зависимости от вида элемента.
Поле id хранит идентификатор компонента, который нужен как для локализации, так и для внутреннего взаимодействия с ServiceOfUserInterface.
Вот пример того, как InitializeTextComponent() и GetElementType() работают в UIButton, в дальнейшем приводить их реализацию в коде не буду.
Ниже я приведу диаграмму с UIElement и всеми его наследниками, а потом поговорим про каждого в отдельности.
Почти все элементы UI так или иначе взаимодействует с ServiceOfUserInterface. Чтобы уменьшить количество зависимостей, я использовал Messenger для их связи.
Объекты UIElement отправляют сообщения в сервис о различных событиях. Сервис внутри уже решает, как обрабатывать эти события. Классы, которые наследуют UIElement, ничего не знают о том, как устроен ServiceOfUserInterface. В свою очередь, сам сервис не завязан на внутреннюю логику UIElement.
Если я решу в будущем реализовать селектор вместо дропдауна через список или вообще через 3D объекты сцены, мне нужно будет переделать только логику селектора — на класс ServiceOfUserInterface это не повлияет.
UIButton — простой пример такого взаимодействия. Поле button содержит в себе соответствующий элемент Unity. В методе Start() мы назначаем для button наш метод OnClick, в котором рассылаем сообщение события BUTTON_ON_CLICK с id кнопки, которую нажали. ServiceOfUserInterface в момент нажатия кнопки уже подписан на событие BUTTON_ON_CLICK и, получив его, вызывает соответствующий обработчик.
Как пример, приведена обработка нажатия кнопки применения настроек. В ApplySettings() мы вызываем соответствующий публичный метод сервиса настроек.
UISelector — здесь ситуация немного сложнее. Нам нужно знать, какой есть список элементов для выбора. Поэтому для начала селектор запрашивает инициализацию через рассылку события SELECTOR_ON_INITIATE. А при выборе какого-то из элементов рассылает событие SELECTOR_ON_CHANGED со своим id и выбранным вариантом.
В данном случае реализованы селекторы для языка интерфейса и текущего самолёта игрока. Список языков мы получаем из сервиса настроек, варианты доступных самолётов берем у сервиса игрового прогресса. Им же сообщаем при событии выбора варианта.
UIView представляет собой конкретное окно интерфейса. К примеру, главное меню или окно с настройками. На сцене это просто картинка, которая содержит подчинённые кнопки, селекторы и слайдеры. Данный элемент я использую, чтобы управлять видимостью окон в сцене.
Сам по себе UIView никак с сервисом интерфейса не взаимодействует. Для управления окнами я добавил ещё один класс — ViewManager. Объект ViewManager находится на каждой загружаемой сцене. Он хранит в себе список окон для текущей сцены и управляет стеком активности окон.
Когда сцена стартует, ViewManager инициализирует внутри себя viewMap и рассылает событие VIEW_ON_INITIATE.
ServiceOfUserInterface по этому событию пушит в стек менеджеров текущий ViewManager (который отправил событие). После этого открывается стартовое окно ViewManager, если оно имеется. Теперь ServiceOfUserInterface может через публичные методы ViewManager показывать и скрывать окна.
В данном случае при вызове StartGame() текущая сцена закрывается(при этом извлекается из стека), и запускается новая сцена "Prototype". При открытии этой сцены уже её ViewManager заявит о себе и будет добавлен в стек.
Вот так примитивно у меня сейчас реализован UI. По-идее для простенькой игры такого должно хватить. Посмотрим, как он будет себя вести с ростом проекта.
Спасибо за внимание!
Если интересно, подписывайтесь на мой телеграм-канал. Там я регулярно делюсь прогрессом по своему проекту.