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

    В прошлой статье — Разновидности координат используемые в 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.



    Поэтому объект может плясать с позиционированием в зависимости от 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 enum CanvasType {OVERLAY, CAMERATYPE}
    
    public class HideBeyondScreenComponent : MonoBehaviour
    {
        [SerializeField] private Direction direction;
        [SerializeField] private CanvasType canvasType;
        [SerializeField] private float timeForHiding = 1;
        [SerializeField] private float offset = 50;
        private Vector3 startPoint;
        private RectTransform rectTransform;
        private Vector2 topRightCoord;
        private Vector2 bottomLeftCoord;
    
        private void Start()
        {
            rectTransform = transform as RectTransform;
            startPoint = rectTransform.localPosition;
            Camera camera = null;
    
            if (canvasType == CanvasType.CAMERATYPE)
                camera = Camera.main;
    
            RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, new Vector2(Screen.width, Screen.height), camera, out topRightCoord);
            RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, new Vector2(0, 0), camera, 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 - offset) + rectTransform.sizeDelta.x * rectTransform.pivot.x,
                            (-rectTransform.sizeDelta.y - offset) + rectTransform.sizeDelta.y * rectTransform.pivot.y);
        }
    
        private Vector3 PositiveCompensation()
        {
            return new Vector2((rectTransform.sizeDelta.x * rectTransform.pivot.x) + offset,
                                    (rectTransform.sizeDelta.y * rectTransform.pivot.y) + offset);
        }
    
        private Vector2 EndPosition(Direction direction)
        {
            switch (direction)
            {
                case Direction.LEFT:
                    return ((Vector3)bottomLeftCoord + rectTransform.localPosition) + NegativeCompensation();
                case Direction.RIGHT:
                    return (Vector3)topRightCoord + rectTransform.localPosition + PositiveCompensation();
                case Direction.TOP:
                    return ((Vector3)topRightCoord + rectTransform.localPosition) + PositiveCompensation();
                case Direction.BOTTOM:
                    return ((Vector3)bottomLeftCoord + rectTransform.localPosition) + NegativeCompensation();
            }
    
            return startPoint;
        }
    }

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 7

      0
      Компоненту должно быть фиолетово где он находится иерархически

      Что если компонент находится внутри Layout группы, например, HorizontalLayoutGroup?
        0
        Тогда не сработает, ибо layout компоненты переписывают все манипуляции с рект трансформами, навскидку можно предложить вешать layout element на скрываемый объект, включать игнорирование layout и потом активировать скрытие за экраном, тогда сработает. Но скорее всего будет глючить ибо освободится место в layout group и все элементы сместятся.

        Вообще все анимирование layout group сложная история, там в итоге придется писать кучу костылей, поэтому большинство анимированных списков в играх — это самописная история без использование layout.
        0
        Код компонента не отрабатывает должным образом. Замените плз на финальный вариант
        Прим. С удовольствием буду юзать
          0
          Перезалил, там да, надо выбрать какого типа канвас, там для канвасов с использованием камеры нужно передать камеру.

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

          Есть игровое поле, которое «прибито к центру» и есть интерфейс, панель сверху, который «липнет» к верхнему краю экрана.

          Задача простая, чтобы фишки с поля летели наверх в панельку с очками (вариантов может быть много).

          То есть, надо конвертнуть локальную позицию «поля с очками», в локальную позицию фишек, чтобы указать фишке, как целевую позицию. И вот на этом мозг ломается (
            0
            Если движение от точки А до точки Б, можно использовать глобальные координаты. Так как у нас нет привязки к конкретному элементу UI и смещению относительно него. Может возникать вопрос в взаимной отрисовке сцены и UI. Фишки — элементы сцены, они должны изначально рисоваться под UI, потом поверх. Для этого канвас должен быть Camera Type c своим слоем сортировки, потом меняем фишкам, сортировку на UI и вуаля они уже кучкуются на панельку.
              0
              Так и сделал, написал комментарий, пришла такая же идея.
              Спасибо за статью, толковой информации по 2d Unity мало.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое