Между строк: Создание элементов интерфейса через VectorApi Unity UI Toolkit
В недалеком прошлом я рассказывал про то, как можно создавать свои кастомные элементы через генерацию мэша, будет полезно ознакомиться для понимания многих аспектов этой статьи.
А сегодня, мы будем создавать кастомные элементы для интерфейсов, но уже используя VectorApi в UI Toolkit'е движка Unity.
Прочитав эту статью вы узнаете:
Что такое painter2D
Как создавать элементы используя его
Как удивить коллег своими знаниями в области создания интерфейсов
Глава 1: Линии!
Также как и в предыдущей части, мы будем писать свои классы, наследуюсь от класса VisualElement (является базовым классом для создания кастомного элемента интерфейса).
Давайте будем переходить от простого к сложному, на примере кода:
using UnityEngine;
using UnityEngine.UIElements;
namespace CustomElements
{
public class EmojiIconElement : VisualElement
{
public new class UxmlFactory : UxmlFactory<EmojiIconElement> { }
public EmojiIconElement()
{
generateVisualContent += GenerateVisualContent;
}
private void GenerateVisualContent(MeshGenerationContext mgc)
{
var top = 0;
var left = 0f;
var right = contentRect.width;
var bottom = contentRect.height;
var painter2D = mgc.painter2D;
painter2D.lineWidth = 10.0f;
painter2D.strokeColor = Color.white;
painter2D.lineJoin = LineJoin.Bevel;
painter2D.lineCap = LineCap.Round;
painter2D.BeginPath();
painter2D.MoveTo(new Vector2((float)(left + (contentRect.width * 0.2)), (float)(top + contentRect.height * 0.1)));
painter2D.LineTo(new Vector2((float)(left + (contentRect.width * 0.2)), (float)(bottom * 0.7)));
painter2D.MoveTo(new Vector2((float)(right - (contentRect.width * 0.2)), (float)(top + contentRect.height * 0.1)));
painter2D.LineTo(new Vector2((float)(right - (contentRect.width * 0.2)), (float)(bottom * 0.7)));
painter2D.MoveTo(new Vector2((float)(left + (contentRect.width * 0.3)), (float)(bottom * 0.8)));
painter2D.LineTo(new Vector2((float)(right - (contentRect.width * 0.3)), (float)(bottom * 0.8)));
painter2D.Stroke();
}
}
}
В данном случае, мы рассмотрим класс EmojiIconElement.
В рамках этой главы, будет рассмотрен метод GenerateVisualContent(...)
и его внутренности, а про конструктор, базовый класс и UxmlFactory
я уже рассказывал в предыдущей статье, в главе Mesh и треугольник!
Не буду тянуть больше со вступлением, рассмотрим наш код.
private void GenerateVisualContent(MeshGenerationContext mgc)
{
var top = 0;
var left = 0f;
var right = contentRect.width;
var bottom = contentRect.height;
var painter2D = mgc.painter2D;
painter2D.lineWidth = 10.0f;
painter2D.strokeColor = Color.white;
painter2D.lineJoin = LineJoin.Bevel;
painter2D.lineCap = LineCap.Round;
painter2D.BeginPath();
painter2D.MoveTo(new Vector2((float)(left + (contentRect.width * 0.2)), (float)(top + contentRect.height * 0.1)));
painter2D.LineTo(new Vector2((float)(left + (contentRect.width * 0.2)), (float)(bottom * 0.7)));
painter2D.MoveTo(new Vector2((float)(right - (contentRect.width * 0.2)), (float)(top + contentRect.height * 0.1)));
painter2D.LineTo(new Vector2((float)(right - (contentRect.width * 0.2)), (float)(bottom * 0.7)));
painter2D.MoveTo(new Vector2((float)(left + (contentRect.width * 0.3)), (float)(bottom * 0.8)));
painter2D.LineTo(new Vector2((float)(right - (contentRect.width * 0.3)), (float)(bottom * 0.8)));
painter2D.Stroke();
}
Как я и говорил ранее, нас интересует метод GenerateVisualContent.
Если кратко, он активируется, когда нашему VisualElement'у будет необходимо отобразить наш элемент или перегенерировать себя (это, как правило, происходит, если были изменения в UI элементе).
Четыре переменные top, left, right, bottom
нужны для упрощения работы с позициями в рамках нашего UI элемента.
Также важно отметить, что эти значения переменных будет меняться в зависимости от размеров UI элемента (в UI Builder / в билде), и благодаря этому наш UI элемент будет масштабироваться относительно размера экрана и самого элемента.
Дальше у нас идет описание нашего painter2D и здесь мы остановимся подробнее.
Если говорить официальным языком, то это класс, который позволяет рисовать векторную графику.
– А как именно рисовать векторую графику?
– Хороший вопрос!
Он предоставляет различные вызовы API используя которые можно рисовать линии, дуги, кривые.
Также, у него есть различные свойства, которые влияют на результат зарисовки:
lineWidth – отвечает за ширину линии
strokeColor – цвет обводки
fillColor – цвет заливки
lineJoin – как будет выглядеть линии при соединение
lineCap – как будут выглядеть концы линий
C подготовкой нашего painter2D определились, дальше разберем, как устроена работа с путями в графическом программировании.
В контексте графического программирования "путь" (path) представляет собой последовательность геометрических фигур, таких как линии, кривые, прямоугольники и окружности, которые определяют форму или контур объекта.
Путь может быть открытым или закрытым.
Открытый путь: Начинается и заканчивается без соединения конечных точек.
Закрытый путь: Конечные точки пути соединены, образуя замкнутую форму.
Начало нового пути: Этот шаг определяет начало нового векторного пути. При вызове
BeginPath()
вы начинаете записывать команды рисования для нового пути.Добавление команд рисования: После начала нового пути вы добавляете команды рисования, такие как
ArcTo()
,LineTo()
, и другие, для создания форм и геометрических объектов в вашем пути.Завершение пути: После того как вы нарисовали все необходимые фигуры и геометрические объекты для текущего пути, вы вызываете
ClosePath()
для завершения этого пути.
Это указывает графическому движку на то, что вы закончили рисование этого пути, и что он должен его отрисовать.
Давайте теперь вернемся к нашему примеру и разберем, что мы делаем:
painter2D.BeginPath();
painter2D.MoveTo(new Vector2((float)(left + (contentRect.width * 0.2)), (float)(top + contentRect.height * 0.1)));
painter2D.LineTo(new Vector2((float)(left + (contentRect.width * 0.2)), (float)(bottom * 0.7)));
painter2D.MoveTo(new Vector2((float)(right - (contentRect.width * 0.2)), (float)(top + contentRect.height * 0.1)));
painter2D.LineTo(new Vector2((float)(right - (contentRect.width * 0.2)), (float)(bottom * 0.7)));
painter2D.MoveTo(new Vector2((float)(left + (contentRect.width * 0.3)), (float)(bottom * 0.8)));
painter2D.LineTo(new Vector2((float)(right - (contentRect.width * 0.3)), (float)(bottom * 0.8)));
painter2D.Stroke();
Мы начинаем с команды BeginPath()
, после чего вызываем метод MoveTo(Vector2 pos)
– который перемещает точку рисования на новую позицию, от которой будут выполняться следующие команды.
Следом за ней идет метод LineTo(Vector2 pos)
, как можно понять из названия, оно проводит прямую линию из текущей позицию painter2D
до позиции заданный в аргументе метода.
Далее идет две пачки команды, которые перемещают курсор рисования и чертят линию.
В конце, мы можем заметить метод Stroke()
– который непосредственно отрисовывает контур текущего пути, который мы определили ранее.
После вызова метода текущий контур будет отрисован на холсте с помощью текущего стиля обводки, такого как цвет и толщина линии.
Поздравляю, теперь у вас есть кастомный UI элемент!
Глава 2: Кривые!
У нас есть два разных варианта возможности нарисовать кривые линии:
Метод
BezierCurveTo()
генерирует кубическую кривую Безье по двум контрольным точкам и конечному положению кубической кривой Безье.Метод
QuadraticCurveTo()
генерирует квадратичную кривую Безье по контрольной точке и конечному положению квадратичной кривой Безье.
Рассмотрим их использование:
painter2D.BeginPath();
painter2D.MoveTo(new Vector2(100, 100));
painter2D.BezierCurveTo(new Vector2(150, 150), new Vector2(200, 50), new Vector2(250, 100));
painter2D.Stroke();
И также приведем код для второго примера:
painter2D.BeginPath();
painter2D.MoveTo(new Vector2(100, 100));
painter2D.QuadraticCurveTo(new Vector2(150, 150), new Vector2(250, 100));
painter2D.Stroke();
Для более глубокого понимания кривых Безье, читаем Wikipedia.
Глава 3: Дуги!
Для рисования дуг можно использовать следующие методы:
Метод
Arc()
создает дугу на основе предоставленного центра дуги, радиуса, а также начального и конечного углов.Метод
ArcTo()
создает дугу между двумя прямыми сегментами.
В рамках рисования дуг, так же стоит рассказать про заливку пути.
Когда в конце пути мы получаем замкнутую фигуру, тогда мы можем ее покрасить в какой-то цвет, вызвав метод painter2D.Fill()
.
Рассмотрим пример построения дуги используя метод Arc()
.
painter2D.lineWidth = 2.0f;
painter2D.strokeColor = Color.red;
painter2D.fillColor = Color.blue;
painter2D.BeginPath();
painter2D.MoveTo(new Vector2(100, 100));
painter2D.Arc(new Vector2(100, 100), 50.0f, 10.0f, 95.0f);
painter2D.ClosePath();
painter2D.Fill();
painter2D.Stroke();
И как раз, параметр painter2D.FillColor
отвечает какой цвет будет у залитой области.
А используя метод painter2D.ArcTo()
, можно нарисовать кривую:
painter2D.BeginPath();
painter2D.MoveTo(new Vector2(100, 100));
painter2D.ArcTo(new Vector2(150, 150), new Vector2(200, 100), 20.0f);
painter2D.LineTo(new Vector2(200, 100));
painter2D.Stroke();
Вы наверное могли задуматься, а можно ли используя метод Arc()
, построить окружность или круговую диаграмму.
– Ну, конечно, можно.
С ней вы можете ознакомиться из официальной документации от Unity.
Глава 4: Остальное?
Здесь хочу рассмотреть, что не вошло в другие главы, и другие комментарии по отрисовке.
Первое, о чем хочется поговорить, это про дыры в заливке.
Когда вы вызываете Fill()
для закраски области, содержащейся внутри пути, вы также можете создать "дыры" в этой закрашенной области, используя дополнительные подпути.
Чтобы создать дыру, вы должны создать дополнительный подпуть с помощью MoveTo()
, а затем использовать правило заливки (fill rule), чтобы определить, какие области будут закрашены, а какие нет.
Вот два основных правила заливки:
OddEven (Нечетное/Четное): Отрисовывается луч из данной точки в бесконечность в любом направлении и подсчитываются количество пересечений сегментов пути. Если количество пересечений нечетное, то точка считается внутри пути, если четное - снаружи.
NonZero (Не нуль): Отрисовывается луч из данной точки в бесконечность в любом направлении, и подсчитываются пересечения сегментов пути. При этом, когда сегменты пересекают луч справа налево, счетчик уменьшается, а когда слева направо - увеличивается. Если счетчик равен нулю, то точка считается снаружи пути, иначе - внутри.
Таким образом, вы можете использовать эти правила, чтобы создать дыру в заполненной области, описав подпуть, который определяет контур этой дыры, и используя правило заливки для указания, как области должны быть закрашены.
В приведенном коде создается прямоугольник с дополнительным подпутем, который определяет форму ромба (бриллианта) внутри прямоугольника. Этот ромб будет являться "дырой" в заполненной области прямоугольника.
painter2D.BeginPath();
painter2D.MoveTo(new Vector2(10, 10));
painter2D.LineTo(new Vector2(300, 10));
painter2D.LineTo(new Vector2(300, 150));
painter2D.LineTo(new Vector2(10, 150));
painter2D.ClosePath();
painter2D.MoveTo(new Vector2(150, 50));
painter2D.LineTo(new Vector2(175, 75));
painter2D.LineTo(new Vector2(150, 100));
painter2D.LineTo(new Vector2(125, 75));
painter2D.ClosePath();
painter2D.Fill(FillRule.OddEven);
Второе, это возможность настраивать стили в каждом подпути.
Для этого нужно использовать методы BeginPath()
и ClosePath()
и между ними менять значения у painter2D
.
private void GenerateVisualContent(MeshGenerationContext mgc)
{
var painter2D = mgc.painter2D;
painter2D.lineWidth = 10.0f;
// Начало первого подпути
painter2D.BeginPath();
painter2D.strokeColor = Color.red;
painter2D.MoveTo(new Vector2(50, 50));
painter2D.LineTo(new Vector2(100, 100));
painter2D.Stroke();
painter2D.ClosePath();
// Конец первого подпутя
// Начало второго подпути
painter2D.BeginPath();
painter2D.strokeColor = Color.blue;
painter2D.MoveTo(new Vector2(20, 20));
painter2D.LineTo(new Vector2(60, 60));
painter2D.Stroke();
painter2D.ClosePath();
// Конец второго подпутя
// Начало третьего подпути
painter2D.BeginPath();
painter2D.strokeGradient = new Gradient()
{
colorKeys = new GradientColorKey[]
{
new() { color = Color.red, time = 0.0f },
new() { color = Color.blue, time = 1.0f }
}
};
painter2D.fillColor = Color.green;
painter2D.MoveTo(new Vector2(50, 150));
painter2D.LineTo(new Vector2(100, 200));
painter2D.LineTo(new Vector2(150, 150));
painter2D.Fill();
painter2D.Stroke();
painter2D.ClosePath();
// Конец третьего подпутя
}
Внимательный зритель уже заметил следующую фишку – поддержка градиента для обводки.
painter2D.strokeGradient = new Gradient()
{
colorKeys = new GradientColorKey[]
{
new() { color = Color.red, time = 0.0f },
new() { color = Color.blue, time = 1.0f }
}
};
Используя свойство strokeGradient
можно рисовать обводку через градиент.
Глава 5: Финал!
Поздравляю с тем, что вы дочитали эту статья до конца.
Сегодня мы разобрались как можно рисовать кастомные элементы в интерфейсах, какой инструментарий для этого имеется в движке Unity.
Для более глубокого погружения и изучения создания элементов интерфейса, рекомендую официальную документацию.
Если у вас остались вопросы или не поняли какую-то часть, напишите в комментариях, постараюсь объяснить, что да как :)
Спасибо за внимание и до скорых встреч!