Координаты 2D: локальные, мир, viewport и HUD
Преобразование точек, масштаб, камера, ввод и координаты HUD без двойных смещений.
Преобразование координат нужно, когда точка приходит из другого пространства: щелчок во viewport, смещение дула внутри оружия или метка игрового объекта на HUD. Простого сложения и вычитания позиций недостаточно при повороте, масштабе или работе камеры.
Определите исходное и конечное пространство
| Пространство | Что означает | Для чего нужно |
|---|---|---|
| Локальное | Относительно начала координат и осей самого элемента | Точки крепления спрайта, локальное рисование и геометрия. |
| Родителя | Локальные координаты родителя | Entity2D.Position размещает здесь начало координат дочернего объекта. |
| Canvas / мир | Координаты в canvas элемента с учётом предков CanvasItem | Entity2D.GlobalPosition, ToGlobal и ToLocal. У разных CanvasLayer могут быть разные canvas. |
| Логический viewport, этап canvas | После преобразования основного canvas или соответствующего CanvasLayer | GetGlobalTransformWithCanvas и MakeCanvasPositionLocal. Этот этап используется в примере ниже. |
| Окно / экран | После дополнительных преобразований показа, растяжения и встраивания | Не смешивайте позиции курсора или окна ОС с логическими координатами viewport из примера. |
В обычных координатах 2D ось +X направлена вправо, +Y — вниз. Положительный поворот на четверть оборота переводит Right в Down. После масштабирования логическая единица не обязательно равна физическому пикселю дисплея.
Преобразуйте точку в иерархии
item.ToGlobal(localPoint) применяет глобальное преобразование элемента. item.ToLocal(canvasPoint) применяет обратное аффинное преобразование. «Глобальное» здесь относится к canvas элемента: это не координаты экрана ОС и не результат преобразования canvas камерой Camera2D.
Например, у родителя столбцы базиса (0, 2) и (-3, 0), начало координат (100, 50). Position дочернего объекта — (10, 4). Его GlobalPosition равна (88, 70), а не (110, 54): на смещение действуют поворот и масштаб родителя. Локальная точка дочернего объекта (2, 1) переходит в точку canvas (85, 74). ToLocal возвращает её обратно.
Чтобы разместить начало координат дочернего объекта в известной точке canvas, присвойте GlobalPosition. ToGlobal принимает точку, уже заданную локально относительно самого элемента; передача его Position обычно повторно применяет дочернее смещение.
В пределах одного canvas точка из A переводится в B выражением b.ToLocal(a.ToGlobal(pointInA)). Для разных CanvasLayer одного viewport используйте помощник ниже, учитывающий преобразование canvas.
Переведите ввод viewport или точку мира в координаты HUD
Сохраните помощник в Coordinates2D.cs:
using System;
using Reckfell;
public static class Coordinates2D
{
public static Vector2 LocalToViewport(CanvasItem item, Vector2 point)
{
RequireTree(item);
return item.GetGlobalTransformWithCanvas() * point;
}
public static Vector2 ViewportToLocal(CanvasItem item, Vector2 point)
{
RequireTree(item);
return item.GetGlobalTransformWithCanvas().AffineInverse() * point;
}
public static Vector2 BetweenItems(CanvasItem source, CanvasItem target, Vector2 point)
{
RequireTree(source);
RequireTree(target);
if (source.GetViewport() != target.GetViewport())
throw new ArgumentException("Items must belong to the same Viewport.");
return ViewportToLocal(target, LocalToViewport(source, point));
}
private static void RequireTree(CanvasItem item)
{
if (!item.IsInsideTree())
throw new InvalidOperationException("Add the item to the tree first.");
}
}Вызовите Coordinates2D.ViewportToLocal(item, viewportPoint) для точки в логическом viewport элемента. Встроенный метод item.MakeCanvasPositionLocal(viewportPoint) выполняет соответствующее преобразование. MakeInputLocal возвращает преобразованное событие ввода, оставляя исходное без изменений. Не преобразуйте позицию повторно, если обработчик уже получает локальные координаты элемента, как при GUI-вводе Control.
Для метки HUD вызовите Coordinates2D.BetweenItems(worldItem, hudParent, pointInWorldItem). Результат можно присвоить Position метки, если она задаётся относительно hudParent. Помощник учитывает преобразования предков обоих элементов и их canvas/слоёв. Оба элемента должны находиться в дереве и одном viewport. У Control внутри Container позицию может перезаписать раскладка; для меток выберите подходящего родителя со свободным позиционированием.
Если CanvasTransform viewport масштабирует в 2 раза и смещает на (7, 11), точка canvas (85, 74) из примера становится (177, 159). Слой HUD с CanvasLayer.Offset (20, 30) и родителем в (5, 7) переводит её в локальную точку родителя HUD (152, 122). Вычитание только позиции камеры пропустило бы масштаб и преобразование HUD.
Помощник работает на этапе canvas. Он не переводит точку экрана ОС или другого SubViewport через преобразования окна, растяжения и встраивания. Учитывайте конечные преобразования показа отдельно: см. масштабирование окна.
Различайте точки, направления и обратные преобразования
Transform2D хранит столбцы базиса X и Y и смещение Origin. transform * point учитывает Origin. transform.BasisXform(direction) исключает перенос, но сохраняет действие масштаба и скоса; нормализуйте результат, только если нужно единичное направление.
parentTransform * childTransform сначала применяет преобразование дочернего объекта, затем родителя. Обратный порядок даёт другой результат.
Для базиса с масштабом или скосом используйте AffineInverse. Inverse и обратный порядок операндов point * transform предполагают ортонормированный базис; это не универсальная замена обращения масштабированной матрицы. У BasisXformInv то же ограничение. Для направления с масштабом используйте transform.AffineInverse().BasisXform(direction).
Для единичной матрицы в C# используйте Transform2D.Identity. Вызов new Transform2D() без аргументов и default(Transform2D) дают нулевые компоненты, а не единичный базис.
Базис должен быть обратимым. Нулевой масштаб по оси схлопывает измерение; C#-метод AffineInverse выбрасывает InvalidOperationException при определителе, точно равном нулю. Почти нулевой масштаб способен усилить погрешность даже при ненулевом определителе. Избегайте вырожденных и неконечных преобразований вместо подстановки выдуманной точки при ошибке.
Найдите причину смещения
| Симптом | Вероятная причина |
|---|---|
| До изменения зума камеры всё правильно | Смешаны координаты canvas/мира и логический ввод viewport. |
| Ошибка только у повёрнутого или масштабированного родителя | Вместо преобразования использовано вычитание позиций. |
| Метка HUD неправильно следует за камерой | Мир и HUD находятся в разных canvas; переводите точку через общий viewport. |
| Смещение применено дважды | Position передана в ToGlobal самого элемента либо уже локальный ввод преобразован повторно. |
| TopLevel-объект продолжает реагировать на камеру | TopLevel отключает наследование преобразования родительского CanvasItem, но не преобразование canvas. |
| Помощник отклоняет элементы | Добавьте их в дерево и проверьте, что viewport совпадает. |
Пример проверен в движке численно: обращение масштаба/скоса, иерархия, преобразования canvas/слоёв, позиции в копии события ввода и поведение TopLevel. Координаты ОС/DPI, вложенные окна, доставка настоящего указателя и сочетания конечных преобразований показа не входят в проверенный охват этого примера.