RectTransformUtility, или как сделать компонент, который анимированно сдвигает элементы UI за экран

в 5:50, , рубрики: game development, UI, unity, unity3d, разработка игр

В прошлой статье — Разновидности координат используемые в GUI Unity3d я попытался кратко рассказать о разновидностях координат в Unity UI/RectTransform. Теперь хочется немножко осветить такую полезную для UI штуку как RectTransformUtility. Которая является одним из основных инструментов вычисления чего либо в UI по отношению к чему либо ещё.

Простая непростая задача

Есть задача — нужен компонент который анимированно убирает UI элемент за выбранный край экрана. Компоненту должно быть фиолетово где он находится иерархически, в каких местах стоят якоря, какой размер экрана, и в каком месте экрана он находится. Компонент должен уметь убирать объект в 4е стороны (вверх, вниз, влево, вправо) за заданное время.

Размышления
В принципе как такое можно сделать? Узнать размеры экрана в координатах объекта, подвинуть объект в координату за краем экрана, и вроде бы дело в шляпе. Но есть пару но:

Как узнать координаты экрана относительно UI?
Если гуглить в лоб, то гуглится какая то ахинея или не полезные штуки, или даже вопросы без ответа. Самое близкое что подходит — это случаи когда какой то UI элемент следует за курсором, который как раз существует в координатах экрана.

RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas, new Vector2(Input.mousePosition), null, out topRightLocalCoord);

Это непосредственно RectTransformUtility и ScreenPointToLocalPointInRectangle. Мы тут получаем локальные координаты внутри ректа(RectTransform), исходя из позиции точки на экране.
В текущем примере мы находим локальные координаты курсора мыши, нам их нужно заменить на край экрана:

RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas, new Vector2(Screen.width, Screen.height), null, out topRightLocalCoord);

И вот мы получили координату верхней правой точки экрана, чтобы объект уехал за экран вправо, наш объект должен быть дальше чем эта точка + допустим ширина ректа или заданный отступ.

Итак, первый нюанс
Мы получили локальные координаты которые подходят для объектов непосредственно внутри канваса, если рект который надо сместить лежит в другом ректе, то его локальные координаты будут считаться относительно родителя, а не канваса. То есть непосредственно эти координаты нам не подходят.

Тут есть два пути, первое — воспользоваться глобальными координатами, на то они и глобальные. Или высчитывать координаты экрана в локальных координатах каждого ректа в отдельности.

Рассмотрим первый случай — как конвертировать локальные координаты в глобальные.

В большинстве нагугленных способов используют — TransformPoint.

transform.position = myCanvas.transform.TransformPoint(pos);

Таким образом мы конвертируем локальные координаты в глобальные.

На мой взгляд это вообще лишний шаг, так как у RectTransformUtility, есть метод ScreenPointToWorldPointInRectangle, который сразу возвращает глобальную позицию.
Нам нужно сместить рект за правый край экрана, для этого мы возьмем X координату с найденной позиции, а Y оставим того ректа который двигаем, чтобы он просто двигался вправо.

new Vector3(topRightCoord.x+offset, rectTransform.position.y, 0);

Полученную координату скормим DoTween.

rectTransform.DOMove(new Vector3(correctedTargetRight.x, rectTransform.position.y, 0), timeForHiding);

И ура, объект уезжает направо. Но…

Второй нюанс
Тут мы выясним, что на самом деле позиционирование ректа, зависит от rect pivot.
RectTransformUtility, или как сделать компонент, который анимированно сдвигает элементы UI за экран - 1

Поэтому объект может плясать с позиционированием в зависимости от pivot, плюс объект может быть очень большой, и offset не задвинет его полностью за экран, всегда будет шанс что кусочек будет торчать.

То есть нам нужно к offset прикрутить компенсацию которая будет учитывать размер ректа + pivot.

Второй нюанс заключается в том, чтобы сдвинуть объект на размер ректа, надо знать локальные или якорные координаты, а мы получаем глобальные координаты. Сразу скажу что глобальные координаты нельзя взять и конвертировать в локальные координаты UI, или же в якорные.
Я придумал следующий костыль, мы запоминаем стартовую позицию ректа, перемещаем его в конечную глобальную позицию, сдвигаем якорную позицию на размер ректа вправо, запоминаем глобальную позицию которая учитывает уже смещение с учётом размера объекта, её и скармливаем дутвину, не забывая перед этим вернуть на начальную позицию.

Пример кода

 var targetRight = new Vector3(topRightLocalCoord.x, rectTransform.position.y, 0);
                rectTransform.position = targetRight;
                rectTransform.anchoredPosition += rectTransform.sizeDelta;
                var correctedTargetRight = rectTransform.position;
                rectTransform.localPosition = startPoint;
                rectTransform.DOMove(new Vector3(correctedTargetRight.x, rectTransform.position.y, 0), timeForHiding);

Выглядит как гигантский костыль, но этот костыль позволяет синхронизировать глобальные и прочие координаты. Это выручает когда в интерфейсе есть объекты которые движутся относительно друг друга, и они находятся в разных иерархиях. Ну и плюс пока это единственный способ который я нашел для получения рект координат из глобальных.
На этом месте мы скажем нет костылям, и вернемся к мысли получения размера экрана в локальных координатах.

Второй путь

Второй путь заключается в том, чтобы получить размеры экрана для каждого ректа в отдельности, таким образом мы будем знать локальные координаты краёв экрана вне зависимости от канваса и иерархии.

Третий нюанс

RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, new Vector2(Screen.width, Screen.height), null, out topRightCoord);
        RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, new Vector2(0, 0), null, out bottomScreenCoord);

Объекты могут находится где угодно на экране, в отличии от канваса который перекрывает весь экран. Поэтому расстояния до левого и правого краёв экрана могут существенно отличаться. В случае с канвасом нам хватило бы только правого верхнего края, а минус правый верхний это был бы левый верхний. В данном случае надо получить нижнюю левую и верхнюю правую точки отдельно, что и показано в примере кода.

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

((Vector3)bottomLeftCoord + rectTransform.localPosition) 

складываем вектора и получаем нужную нам координату. Получается более запутано чем с глобальным координатами, но теперь мы можем проводить любые вычисления связанные с размерами ректа. И спокойно наконец дописать компенсацию без костылей.

 (Vector3)topRightCoord + rectTransform.localPosition + (new Vector3((rectTransform.sizeDelta.x * rectTransform.pivot.x) + rectTransform.sizeDelta.x, 0, 0));

Вот так выглядит координата для смещения вправо с компенсацией ширины ректа и смещением за экран на ширину ректа, тут нет возможности задавать офсет, планирую дописать чуть позже, но думаю кому нибудь будет интересно самому попробовать написать такое.

Выводы

  1. Для UI элементов лучше использовать локальные или якорные координаты, и надо постараться их понять. Глобальные координаты можно использовать для особых случаев, но они не дают возможности удобно работать например с размерами ректов и в многих других микро эпизодах.
  2. Нужно присмотреться к RectTransformUtility, у неё много полезного функционала для UI, все вычисления связанные с положением чего то внутри и около ректа, делаются через неё.

Ну и сам компонент, если кому хочется поиграться с ним, для этого будет нужен DoTween:

Компонент

using DG.Tweening;
using UnityEngine;

public enum Direction {DEFAULT, RIGHT, LEFT, TOP, BOTTOM }

public class HideBeyondScreenComponent : MonoBehaviour
{
    [SerializeField] private Direction direction;
    [SerializeField] private float timeForHiding;
    private Vector3 startPoint;
    private RectTransform rectTransform;
    private Vector2 topRightCoord;
    private Vector2 bottomLeftCoord;

    private void Start()
    {
        rectTransform = transform as RectTransform;
        startPoint = rectTransform.localPosition;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, new Vector2(Screen.width, Screen.height), null, out topRightCoord);
        RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, new Vector2(0, 0), null, out bottomLeftCoord);
        Hide();
    }

    public void Show()
    {
        rectTransform.DOLocalMove(startPoint, timeForHiding);
    }

    public void Hide()
    {
        switch (direction)
        {
            case Direction.LEFT:
                rectTransform.DOLocalMove(new Vector3(EndPosition(Direction.LEFT).x, rectTransform.localPosition.y, 0), timeForHiding);
                break;
            case Direction.RIGHT:
                rectTransform.DOLocalMove(new Vector3(EndPosition( Direction.RIGHT).x, rectTransform.localPosition.y, 0), timeForHiding);
                break;
            case Direction.TOP:
                rectTransform.DOLocalMove(new Vector3(rectTransform.localPosition.x, EndPosition(Direction.TOP).y, 0), timeForHiding);
                break;
            case Direction.BOTTOM:
                rectTransform.DOLocalMove(new Vector3(rectTransform.localPosition.x, EndPosition(Direction.BOTTOM).y, 0), timeForHiding);
                break;
        }
    }
    private Vector3 NegativeCompensation()
    {
        return new Vector2(-rectTransform.sizeDelta.x * 2 + rectTransform.sizeDelta.x * rectTransform.pivot.x,
                        -rectTransform.sizeDelta.y * 2 + rectTransform.sizeDelta.y * rectTransform.pivot.y);
    }

    private Vector2 EndPosition (Direction direction)
    {
        switch (direction)
        {
            case Direction.LEFT:
                return ((Vector3)bottomLeftCoord + rectTransform.localPosition) + NegativeCompensation();
            case Direction.RIGHT:
                return (Vector3)topRightCoord + rectTransform.localPosition + (new Vector3((rectTransform.sizeDelta.x * rectTransform.pivot.x) + rectTransform.sizeDelta.x, 0, 0));
            case Direction.TOP:
                return ((Vector3)topRightCoord + rectTransform.localPosition) + new Vector3(0, (rectTransform.sizeDelta.y * rectTransform.pivot.y) + rectTransform.sizeDelta.y, 0);
            case Direction.BOTTOM:
                return ((Vector3)bottomLeftCoord + rectTransform.localPosition) + NegativeCompensation();
        }

        return startPoint;
    }
}

Автор: Brightori

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js