В прошлой статье — Разновидности координат используемые в 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));
Вот так выглядит координата для смещения вправо с компенсацией ширины ректа и смещением за экран на ширину ректа, тут нет возможности задавать офсет, планирую дописать чуть позже, но думаю кому нибудь будет интересно самому попробовать написать такое.
Выводы
- Для UI элементов лучше использовать локальные или якорные координаты, и надо постараться их понять. Глобальные координаты можно использовать для особых случаев, но они не дают возможности удобно работать например с размерами ректов и в многих других микро эпизодах.
- Нужно присмотреться к 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