С тех пор как в Unity появилась новая система UI (User interface), создавать пользовательский интерфейс стало намного проще и удобней. Теперь все элементы этого интерфейса стали отдельными объектами, и с тех пор к каждому элементу можно найти особый подход в работе.
В этой статье с помощью системы UI мы создадим простое, разворачивающееся меню, примерно как на анимации ниже.
По принципу работы, оно чем то напоминает панель настроек в верхнем углу экрана у смартфонов.
Для этого не нужно будет разбирать как разработчики операционных систем Android и iOS, добились подобного эффекта. Здесь мы будем использовать только возможности Unity и системы UI.
Начнем с канваса – нашего холста. На сцене у нас уже есть камера, добавим теперь еще и объект Canvas, который можно найти в разделе GameObject -> UI. Так как канвас у нас будет привязан к одной камере, в настройках компонента Canvas укажем режим Screen Space – Camera в поле Render Mode, и добавим камеру в появившееся поле Render Camera.
Готово, теперь канвас у нас будет обрабатывать только события, поступающие с указанной камеры. Далее внутри канваса создадим простую панель – Panel, которая будет содержать меню игры. Также добавим еще одну панель, в которую поместим изображение “игры”.
Настройки
Самое главное в работе маскирующегося меню, это правильная настройка его элементов. У каждого UI элемента есть Anchors Presets в компоненте RectTransform, которые помогают привязывать элементы относительно друг друга.
У элемента Panel по умолчанию установлена привязка как “полное” растягивание, то есть она заполняет всю область родительского элемента независимо от размера последнего. Так, продолжим с панелью, куда добавим несколько UI кнопок Button. У каждой кнопки в настройках Anchors Presets установим смещение сверху, так как панель меню у нас будет разворачиваться сверху – вниз.
Самой панели, добавим компонент Mask, чтобы сделать из нее маску. Этот компонент можно найти в разделе Component -> UI -> Mask, либо найти по названию через Add Component меню. Можно протестировать работу маски меню, перемещая ее нижний край и изменяя ее размер.
Теперь, там же, в канвасе, добавим новый элемент Image, который будет служить тумблером, для примера – выберем изображение стрелочки. С помощью этого элемента мы будем разворачивать панель. Расположим его у верхнего края панели меню.
Действия элементов
Работа тумблера заключается в перемещении между нижним и верхним краем панели меню на канвасе. Эти края будут определяться высотой самой панели. И так, завершив настройку элементов канваса можно переходить к программной части.
Создадим новый скрипт CustomSlider унаследованный от класса UIBehaviour, этот класс можно найти подключив библиотеку EventSystem.
- using UnityEngine.EventSystem;
- public sealed class CustomSlider : UIBehaviour {}
Далее объявим новую переменную fillRect, которая будет ссылаться на RectTransform панели меню.
- using UnityEngine.EventSystem;
- public sealed class CustomSlider : UIBehaviour {
- public RectTransform fillRect;
- }
Теперь добавим скрипт CustomSlider тумблеру и в поле Fill Rect поместим панель меню.
Так как сам тумблер мы будем перемещать по канвасу перетаскивая его по экрану, нужно будет использовать дополнительные инструменты обработки событий касания. Для этого в системе UI есть целая дюжина интерфейсов, которые помогут обработать каждое действие пользователя, в том числе касания, перемещения, нажатия и тд. В этом примере нам понадобятся только три из них, это IBeginDragHandler – для обработки события начала перетаскивания, IDragHandler – для обработки самого перетаскивания и IEndDragHandler – для обработки окончания перетаскивания элемента.
Наследуем скрипт CustomSlider от каждого из трех выбранных интерфейсов после чего реализуем все их методы.
- using UnityEngine.EventSystem;
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- public void OnBeginDrag(PointerEventData eventData) {}
- public void OnDrag(PointerEventData eventData) {}
- public void OnEndDrag(PointerEventData eventData) {}
- }
Метод OnBeginDrag сработает перед началом перетаскивания тумблера, метод OnDrag будет обрабатывать сам процесс перетаскивания, а метод OnEndDrag сработает после того как мы закончим перетаскивать элемент.
Каждый метод принимает параметр PointerEventData, этот класс содержит много полезных данных от каждого события, но нам понадобится только два из них: это камера, от которой поступило событие касания и сама позиция касания.
Для проверки работоспособности скрипта попробуем перемещать тумблер в методе OnDrag. Для этого нужно будет всего лишь перевести позицию касания на экране в мировую точку на сцене, а потом в локальную точку относительно канваса, выглядит это так:
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- public void OnBeginDrag(PointerEventData eventData) {}
- public void OnDrag(PointerEventData eventData) {
- Camera eventCam = eventData.pressEventCamera;
- Vector2 worldPoint = eventCam.ScreenToWorldPoint(eventData.position);
- Vector2 localPoint = this.canvas.transform.InverseTransformPoint(worldPoint);
- this.transform.localPosition = localPoint;
- }
- public void OnEndDrag(PointerEventData eventData) {}
- }
Получив локальную позицию localPoint при помощи метода InverseTransformPoint Transform’а канваса, применяем эти координаты Transform’у тумблера. Теперь можно запустить и попробовать “повозить” тумблер по экрану.
Отлично, все работает – события обрабатываются исправно можно продолжать работу со скриптом.
Для начала сотрем все что написали в методе OnDrag и добавим несколько новых переменных: canvasRect и rectTransform, которые будут указывать на Transform’ы канваса и тумблера.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- public void OnBeginDrag(PointerEventData eventData) {}
- public void OnDrag(PointerEventData eventData) {}
- public void OnEndDrag(PointerEventData eventData) {}
- }
Дальше нам понадобится хранить высоту панели меню в переменной fillHeight, высоту изображения тумблера imageHeight, нижний предел в переменной minPosY, верхний предел в maxPosY и текущую высоту тумблера в переменной targetPosY.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- private float fillHeight, imageHeight, minPosY, maxPosY, targetPosY;
- public void OnBeginDrag(PointerEventData eventData) {}
- public void OnDrag(PointerEventData eventData) {}
- public void OnEndDrag(PointerEventData eventData) {}
- }
Этих переменных нам будет достаточно для начала. Далее в методе Start займемся сбором данных.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- private float fillHeight, minPosY, maxPosY, imageHeight, targetPosY;
- protected override void Start() {}
- /*…остальной код…*/
- }
Начнем с определения переменных Transform’а канваса и тумблера. Для того, чтобы найти канвас, будем использовать метод GetComponentInParent.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- private float fillHeight, minPosY, maxPosY, imageHeight, targetPosY;
- protected override void Start() {
- Canvas canvas = GetComponentInParent<Canvas>();
- this.canvasRect = canvas.transform as RectTransform;
- this.rectTransform = this.transform as RectTransform;
- }
- /*…остальной код…*/
-
}
Дальше, в методе Start определим размеры панели меню и тумблера, но делать мы это будем двумя разными способами.
Дело в том, что у тумблера в настройках компонента RectTransform указаны его длина и высота (Width и Height), а у панели меню указаны только отступы лево, верх, право и низ (Left, top, right и bottom), то есть панель всегда, независимо от размеров экрана, будет заполнять максимум родительского пространства (в данном случае весь канвас). Поэтому узнать размеры панели через длину и высоту, как у тумблера – не получится, для этого придется использовать другой подход, а именно – найти расположения краев панели на канвасе.
И так, начнем с высоты тумблера, здесь все просто – используем свойство sizeDelta у его Transform’а.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- private float fillHeight, minPosY, maxPosY, imageHeight, targetPosY;
- protected override void Start() {
- Canvas canvas = GetComponentInParent<Canvas>();
- this.canvasRect = canvas.transform as RectTransform;
- this.rectTransform = this.transform as RectTransform;
- this.imageHeight = this.rectTransform.sizeDelta.y;
- }
- /*…остальной код…*/
- }
Чтобы найти края панели меню нам понадобится небольшой массив векторов Vector3 и метод GetLocalCorners, который вернет все четыре(4) угла Transform’а панели.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- private float fillHeight, minPosY, maxPosY, imageHeight, targetPosY;
- protected override void Start() {
- Canvas canvas = GetComponentInParent<Canvas>();
- this.canvasRect = canvas.transform as RectTransform;
- this.rectTransform = this.transform as RectTransform;
- this.imageHeight = this.rectTransform.sizeDelta.y;
- Vector3[] fillCorners = new Vector3[4];
- this.fillRect.GetLocalCorners(fillCorners);
- }
- /*…остальной код…*/
- }
Вообще все углы любого элемента UI располагаются по часовой стрелке начиная с нижнего левого края элемента.
Теперь, используя края панели, определим ее размеры, а также нижний и верхний пределы.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- private float fillHeight, minPosY, maxPosY, imageHeight, targetPosY;
- protected override void Start() {
- Canvas canvas = GetComponentInParent<Canvas>();
- this.canvasRect = canvas.transform as RectTransform;
- this.rectTransform = this.transform as RectTransform;
- this.imageHeight = this.rectTransform.sizeDelta.y;
- Vector3[] fillCorners = new Vector3[4];
- this.fillRect.GetLocalCorners(fillCorners);
- this.fillHeight = Mathf.Abs(fillCorners[0].y * 2f);
- this.maxPosY = fillCorners[0].y + this.imageHeight / 2f;
- this.minPosY = fillCorners[1].y – this.imageHeight / 2f;
- }
- /*…остальной код…*/
- }
Для высоты fillHeight используем нижний левый угол панели под индексом 0 массива fillCorners умноженный на 2. Нижний предел minPosY будет располагаться в левом нижнем углу панели, верхний предел maxPosY – в левом верхнем углу соответственно.
Готово, после выполнения метода Start мы получим все необходимые данные об элементах панели и тумблера.
Само же перемещение тумблера теперь будем обрабатывать в методе Update, так как в отличие от метода OnDrag, метод Update работает всегда, а не только когда мы совершаем действие “перетаскивания”. Поэтому добавим новый метод Update, и два дополнительных метода для обработки перемещения тумблера UpdatePosition и заполнения панели UpdateFill.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- private float fillHeight, minPosY, maxPosY, imageHeight, targetPosY;
- /*…остальной код…*/
- private void Update() {
- UpdateFill();
- UpdatePosition();
- }
- private void UpdateFill() {}
- private void UpdatePosition() {}
- /*…остальной код…*/
- }
Для перемещения тумблера будем использовать переменную targetPosY которую будем заполнять в методе OnDrag.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- private float fillHeight, minPosY, maxPosY, imageHeight, targetPosY;
- /*…остальной код…*/
- private void Update() {
- UpdateFill();
- UpdatePosition();
- }
- private void UpdateFill() {}
- private void UpdatePosition() {}
- public void OnDrag(PointerEventData eventData) {
- Camera eventCam = eventData.pressEventCamera;
- Vector2 worldPoint = eventCam.ScreenToWorldPoint(eventData.position);
- Vector2 localPoint = this.canvasRect.InverseTransformPoint(worldPoint);
- this.targetPosY = localPoint.y;
- }
- /*…остальной код…*/
- }
Как и раньше, в методе OnDrag мы сначала находим мировые координаты касания worldPoint, после чего конвертируем их в локальные localPoint относительно канваса, и задаем переменной targetPosY новую позицию тумблера по высоте. Дальше переходим в метод UpdatePosition, где будем перемещать сам тумблер.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- private float fillHeight, minPosY, maxPosY, imageHeight, targetPosY;
- /*…остальной код…*/
- private void Update() {
- UpdateFill();
- UpdatePosition();
- }
- private void UpdateFill() {}
- private void UpdatePosition() {
- Vector2 currentPos = this.rectTransform.localPosition;
- float yPos = Mathf.Clamp(this.targetPosY, this.maxPosY, this.minPosY);
- this.rectTransform.localPosition = new Vector2(currentPos.x, yPos);
- }
- /*…остальной код…*/
- }
Здесь тоже все просто, сначала помещаем текущую позицию тумблера в переменную currentPos, после чего в переменную yPos записываем новую позицию, с учетом пределов maxPosY и minPosY, и в конце применяем полученные координаты локальной позиции Transform’а тумблера. И так, теперь тумблер перемещается по канвасу учитывая минимальную и максимальную высоту панели, осталось только определить – на сколько нам разворачивать панель меню в зависимости от расположения тумблера на канвасе.
Для этой задачи воспользуемся простой арифметикой: подсчитаем кол-во пройденного пути тумблером от верхнего предела к нижнему, переведем полученные значение в проценты и умножим на высоту панели, которая заранее нам известна.
Запишем в скрипте CustomSlider новый метод GetFillValue, с помощью которого получим кол-во пройденного пути в виде значения от 0 до 1, где ноль будет показывать, что тумблер находится в самом начале пути, а 1 – что в самом конце.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- private float fillHeight, minPosY, maxPosY, imageHeight, targetPosY;
- /*…остальной код…*/
- private void Update() {
- UpdateFill();
- UpdatePosition();
- }
- private void UpdateFill() {}
- private float GetFillValue() {
- float currentYPos = this.rectTransform.localPosition.y;
- float diff = currentYPos – this.minPosY;
- float result = -(diff / (this.fillHeight – this.imageHeight));
- return result;
- }
- /*…остальной код…*/
- }
Для нахождения соотношения между пройденным путем и полным расстоянием необходимо знать текущую позицию тумблера currentYPos и разницу diff между нижним и верхним пределом с учетом высоты тумблера.
Переходим в метод UpdateFill, где будем заполнять нашу панель меню с учетом пройденного пути.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- private float fillHeight, minPosY, maxPosY, imageHeight, targetPosY;
- /*…остальной код…*/
- private void Update() {
- UpdateFill();
- UpdatePosition();
- }
- private void UpdateFill() {
- float value = GetFillValue();
- float newSizeY = this.fillHeight * value;
- }
- /*…остальной код…*/
- }
Для начала в переменную value запишем кол-во пройденного пути тумблером, далее в переменной newSizeY установим высоту на которую необходимо развернуть панель меню. Осталось только задать новую высоту панели меню, для чего воспользуемся специальным методом SetInsetAndSizeFromParentEdge, который позволяет изменять размеры UI элемента от определенного края RectTransform.Edge.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- public RectTransform fillRect;
- private RectTransform canvasRect, rectTransform;
- private float fillHeight, minPosY, maxPosY, imageHeight, targetPosY;
- /*…остальной код…*/
- private void Update() {
- UpdateFill();
- UpdatePosition();
- }
- private void UpdateFill() {
- float value = GetFillValue();
- float newSizeY = this.fillHeight * value;
- RectTransform.Edge edge = RectTransform.Edge.Top;
- this.fillRect.SetInsetAndSizeFromParentEdge(edge, 0f, newSizeY);
- }
- /*…остальной код…*/
- }
В нашем случае, так как панель будет разворачиваться сверху-вниз, необходимо использовать верхний край RectTransform.Edge.Top. Теперь попробуем протестировать то, что у нас получилось. Если вы проделали все также как и в примере, то результат у вас должен быть примерно такой.
Тумблер отлично перемещается между границами, а панель, в зависимости от расположения тумблера на экране, изменяет свой размер.
Открытие и закрытие панели
Чтобы совершать автоматические действия открытия и закрытия панели, без перетаскивания тумблера вручную, необходимо добавить два новых метода Open и Close, и пару boolean переменных isDragging и isOpen.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- /*…остальной код…*/
- private bool isDragging, isOpen;
- public void Open() {}
- public void Close() {}
- /*…остальной код…*/
- }
-
Вызвав любой из этих методов, переменная isOpen будет меняться, и в зависимости от того какое значение она принимает, true или false, панель будет открываться или закрываться.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- /*…остальной код…*/
- private bool isDragging, isOpen;
- public void Open() {
- if (this.isOpen == false && this.isDragging == false) this.isOpen = true;
- }
- public void Close() {
- if (this.isOpen && this.isDragging == false) this.isOpen = false;
- }
- /*…остальной код…*/
- }
-
Переменная isDragging будет указывать, перетаскиваем мы сейчас тумблер вручную или нет, а изменять мы ее будем в методах OnBeginDrag и OnEndDrag перед началом и после завершения перетаскивания тумблера.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- /*…остальной код…*/
- private bool isDragging, isOpen;
- public void Open() {
- if (this.isOpen == false && this.isDragging == false) this.isOpen = true;
- }
- public void Close() {
- if (this.isOpen && this.isDragging == false) this.isOpen = false;
- }
- public void OnBeginDrag(PointerEventData eventData) {
- this.isDragging = true;
- }
- public void OnEndDrag(PointerEventData eventData) {
- this.isDragging = false;
- }
- /*…остальной код…*/
- }
-
Действия по автоматическому открытию или закрытию панели будем выполнять в методе UpdatePosition.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- /*…остальной код…*/
- private void UpdatePosition() {
- Vector2 currentPos = this.rectTransform.localPosition;
- if (this.isDragging == false) {
- }
- float yPos = Mathf.Clamp(this.targetPosY, this.maxPosY, this.minPosY);
- this.rectTransform.localPosition = new Vector2(currentPos.x, yPos);
- }
- /*…остальной код…*/
- }
-
После определения текущей позиции тумблера в переменную currentPos, добавляем новое условие, что когда мы НЕ перетаскиваем тумблер вручную, то есть переменная isDragging равна false, необходимо закрывать или открывать панель автоматически.
- public sealed class CustomSlider : UIBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
- /*…остальной код…*/
- private void UpdatePosition() {
- Vector2 currentPos = this.rectTransform.localPosition;
- if (this.isDragging == false) {
- float newYPos = (this.isOpen) ? this.maxPosY : this.minPosY;
- float speed = Time.deltaTime * 10f;
- this.targetPosY = Mathf.Lerp(currentPos.y, newYPos, speed);
- }
- float yPos = Mathf.Clamp(this.targetPosY, this.maxPosY, this.minPosY);
- this.rectTransform.localPosition = new Vector2(currentPos.x, yPos);
- }
- /*…остальной код…*/
- }
-
Для этого в переменную newYPos запишем новую позицию тумблера в зависимости от того совершаем мы действие открытия или закрытия. Если открываем, значит нам нужно переместить тумблер в самый нижний предел maxPosY, если же закрываем то нужно переместить в верхний предел minPosY. После чего, с помощью простого Lerp и дельты, плавно перемещаем тумблер к установленной границе.
Теперь панель можно открывать и закрывать просто вызвав метод Open или Close соответственно, без необходимости перетаскивать тумблер вручную.
В примере именно кнопка “назад” вызывает метод Close скрипта CustomSlider, после чего панель самостоятельно сворачивается.
Вот так с помощью простого скрипта и несложных настроек элементов интерфейса можно получить симпатичное и интуитивно понятное меню.
С эффектом размытия панели и другими деталями работы скрипта CustomSlider можно ознакомится скачав исходник проекта по ссылке.