Между строк: Создание элементов интерфейса через VectorApi Unity UI Toolkit

от автора

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

А сегодня, мы будем создавать кастомные элементы для интерфейсов, но уже используя VectorApi в UI Toolkit‘е движка Unity.

Прочитав эту статью вы узнаете:

  1. Что такое painter2D

  2. Как создавать элементы используя его

  3. Как удивить коллег своими знаниями в области создания интерфейсов

Глава 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 элемент будет масштабироваться относительно размера экрана и самого элемента.

Именно на этом самом contentRect'e и будет происходить наша генерация.

Именно на этом самом contentRect’e и будет происходить наша генерация.

Дальше у нас идет описание нашего painter2D и здесь мы остановимся подробнее.

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

– А как именно рисовать векторую графику?

– Хороший вопрос!

Он предоставляет различные вызовы API используя которые можно рисовать линии, дуги, кривые.

Также, у него есть различные свойства, которые влияют на результат зарисовки:

  • lineWidth – отвечает за ширину линии

  • strokeColor – цвет обводки

  • fillColor – цвет заливки

  • lineJoin – как будет выглядеть линии при соединение

  • lineCap – как будут выглядеть концы линий

Более наглядно как это выглядит

Более наглядно как это выглядит

C подготовкой нашего painter2D определились, дальше разберем, как устроена работа с путями в графическом программировании.

В контексте графического программирования «путь» (path) представляет собой последовательность геометрических фигур, таких как линии, кривые, прямоугольники и окружности, которые определяют форму или контур объекта.

Путь может быть открытым или закрытым.

  • Открытый путь: Начинается и заканчивается без соединения конечных точек.

  • Закрытый путь: Конечные точки пути соединены, образуя замкнутую форму.

  1. Начало нового пути: Этот шаг определяет начало нового векторного пути. При вызове BeginPath() вы начинаете записывать команды рисования для нового пути.

  2. Добавление команд рисования: После начала нового пути вы добавляете команды рисования, такие как ArcTo(), LineTo(), и другие, для создания форм и геометрических объектов в вашем пути.

  3. Завершение пути: После того как вы нарисовали все необходимые фигуры и геометрические объекты для текущего пути, вы вызываете 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: Кривые!

У нас есть два разных варианта возможности нарисовать кривые линии:

  1. Метод BezierCurveTo() генерирует кубическую кривую Безье по двум контрольным точкам и конечному положению кубической кривой Безье.

  2. Метод 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: Дуги!

Для рисования дуг можно использовать следующие методы:

  1. Метод Arc() создает дугу на основе предоставленного центра дуги, радиуса, а также начального и конечного углов.

  2. Метод 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), чтобы определить, какие области будут закрашены, а какие нет.

Вот два основных правила заливки:

  1. OddEven (Нечетное/Четное): Отрисовывается луч из данной точки в бесконечность в любом направлении и подсчитываются количество пересечений сегментов пути. Если количество пересечений нечетное, то точка считается внутри пути, если четное — снаружи.

  2. 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.

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

Если у вас остались вопросы или не поняли какую-то часть, напишите в комментариях, постараюсь объяснить, что да как 🙂

Спасибо за внимание и до скорых встреч!


ссылка на оригинал статьи https://habr.com/ru/articles/803651/