Пользовательская отрисовка в 2D
Введение
В Godot есть узлы для рисования спрайтов, полигонов, частиц, текста и многих других распространенных задач разработки игр. Однако, если вам нужно что-то конкретное и не представленное стандартными узлами, вы можете создать любой 2D-узел (например, на основе Control или Node2D), отображаемый на экране с помощью пользовательских команд.
Пользовательский рисунок в 2D узле действительно полезен.Вот несколько примеров, почему:
Рисование фигур или логики, которые не обрабатываются узлами (пример: создание узла, который рисует круг, изображение со следами, особый вид анимированного многоугольника и т. д.).
Рисовать множество простых объектов, таких как сетка или поле для 2d-игры. Пользовательское рисование позволяет избежать накладных расходов, связанных с использованием большого количества узлов, что может снизить потребление памяти и поднять производительность.
Создание пользовательского элемента управления пользовательского интерфейса (UI). Существует множество доступных элементов управления, но когда у вас есть нестандартные требования, вероятно, вам потребуется создать собственный элемент управления.
Отрисовка
Добавьте скрипт к любому производному узлу CanvasItem, например Control или Node2D. Затем переопределите функцию _draw().
extends Node2D
func _draw():
pass # Your draw commands here.
using Godot;
public partial class MyNode2D : Node2D
{
public override void _Draw()
{
// Your draw commands here.
}
}
Команды рисования описаны в CanvasItem. Их прилично много, и мы посмотрим на них в примерах ниже.
Обновление
Функция _draw вызывается только единожды, затем команды рисования кэшируются и запоминаются, поэтому в дальнейших вызовах нет необходимости.
Если потребуется отрисовать все заново из-за изменения переменной или чего-то еще, вызовите CanvasItem.queue_redraw в том же узле, и произойдет новый вызов _draw().
Вот немного более сложный пример, где у нас есть переменная текстуры, которую можно изменить в любое время, и использование setter, которое принудительно перерисовывает текстуру при изменении:
extends Node2D
@export var texture : Texture2D:
set(value):
texture = value
queue_redraw()
func _draw():
draw_texture(texture, Vector2())
using Godot;
public partial class MyNode2D : Node2D
{
private Texture2D _texture;
[Export]
public Texture2D Texture
{
get
{
return _texture;
}
set
{
_texture = value;
QueueRedraw();
}
}
public override void _Draw()
{
DrawTexture(_texture, new Vector2());
}
}
Чтобы увидеть это в действии, вы можете установить текстуру в качестве значка Godot в редакторе, перетащив и отпустив icon.svg по умолчанию из вкладки FileSystem в свойство Texture на вкладке Inspector. При изменении значения свойства Texture во время выполнения предыдущего скрипта текстура также автоматически изменится.
В некоторых случаях нам может потребоваться заново перерисовать каждый кадр. Для этого вызовите queue_redraw из метода _process, следующим образом:
extends Node2D
func _draw():
pass # Your draw commands here.
func _process(_delta):
queue_redraw()
using Godot;
public partial class MyNode2D : Node2D
{
public override void _Draw()
{
// Your draw commands here.
}
public override void _Process(double delta)
{
QueueRedraw();
}
}
Выравнивание координат и ширины линии
API рисования использует систему координат CanvasItem, а не обязательно пиксельные координаты. Это означает, что _draw() использует координатное пространство, созданное после применения преобразования CanvasItem. Кроме того, вы можете применить пользовательское преобразование поверх него, используя draw_set_transform или draw_set_transform_matrix.
При использовании draw_line следует учитывать ширину линии. При использовании ширины нечетного размера положение начальной и конечной точек следует сместить на 0.5, чтобы линия оставалась центрированной, как показано ниже.
func _draw():
draw_line(Vector2(1.5, 1.0), Vector2(1.5, 4.0), Color.GREEN, 1.0)
draw_line(Vector2(4.0, 1.0), Vector2(4.0, 4.0), Color.GREEN, 2.0)
draw_line(Vector2(7.5, 1.0), Vector2(7.5, 4.0), Color.GREEN, 3.0)
public override void _Draw()
{
DrawLine(new Vector2(1.5f, 1.0f), new Vector2(1.5f, 4.0f), Colors.Green, 1.0f);
DrawLine(new Vector2(4.0f, 1.0f), new Vector2(4.0f, 4.0f), Colors.Green, 2.0f);
DrawLine(new Vector2(7.5f, 1.0f), new Vector2(7.5f, 4.0f), Colors.Green, 3.0f);
}
То же самое относится к методу draw_rect с filled = false.
func _draw():
draw_rect(Rect2(1.0, 1.0, 3.0, 3.0), Color.GREEN)
draw_rect(Rect2(5.5, 1.5, 2.0, 2.0), Color.GREEN, false, 1.0)
draw_rect(Rect2(9.0, 1.0, 5.0, 5.0), Color.GREEN)
draw_rect(Rect2(16.0, 2.0, 3.0, 3.0), Color.GREEN, false, 2.0)
public override void _Draw()
{
DrawRect(new Rect2(1.0f, 1.0f, 3.0f, 3.0f), Colors.Green);
DrawRect(new Rect2(5.5f, 1.5f, 2.0f, 2.0f), Colors.Green, false, 1.0f);
DrawRect(new Rect2(9.0f, 1.0f, 5.0f, 5.0f), Colors.Green);
DrawRect(new Rect2(16.0f, 2.0f, 3.0f, 3.0f), Colors.Green, false, 2.0f);
}
Сглаживание
Godot предлагает параметры метода в draw_line, чтобы включить сглаживание, но не все пользовательские методы рисования поддерживают antialiased параметр.
Для пользовательских методов рисования, которые не предоставляют параметр antialiased, вы можете вместо этого включить 2D MSAA, который влияет на рендеринг во всем окне просмотра. Это обеспечивает высококачественное сглаживание, но более высокую стоимость производительности и только для определенных элементов. См. 2D сглаживание для получения дополнительной информации.
Ниже приведено сравнение линии минимальной ширины (width=-1), нарисованной с antialiased=false, antialiased=true и antialiased=false при включенном 2D MSAA 2x, 4x и 8x.
Инструменты
Также может потребоваться нарисовать собственные узлы при их запуске в редакторе. Это можно использовать для предпросмотра или визуализации какой-либо функции или поведения.
Для этого можно использовать аннотацию инструмента как в GDScript, так и в C#. Для получения дополнительной информации см. пример ниже и Запуск кода в редакторе.
Вот простой пример того, как это работает
Теперь мы используем пользовательские функции рисования движка Godot, чтобы нарисовать то, для чего в Godot нет встроенных функций. Мы воссоздадим логотип Godot, но только кодом и функциями рисования.
Вам придется закодировать функцию для ее выполнения и нарисовать ее самостоятельно.
Примечание
В следующих инструкциях используется фиксированный набор координат, который может быть слишком мал для экранов с высоким разрешением (больше 1080p). Если это ваш случай, и рисунок слишком мал, рассмотрите возможность увеличения масштаба окна в настройках проекта Display > Window > Stretch > Scale, чтобы настроить проект на более высокое разрешение (масштаб 2 или 4 обычно работает хорошо).
Рисование пользовательского polygon shape (формы полигона)
Хотя для рисования пользовательских полигонов существует специальный узел ( Polygon2D), в данном случае мы будем использовать исключительно функции рисования более низкого уровня, чтобы объединить их в одном узле и иметь возможность создавать более сложные фигуры в дальнейшем.
Сначала мы определим набор точек (или координат X и Y), которые лягут в основу нашей фигуры:
extends Node2D
var coords_head : Array = [
[ 22.952, 83.271 ], [ 28.385, 98.623 ],
[ 53.168, 107.647 ], [ 72.998, 107.647 ],
[ 99.546, 98.623 ], [ 105.048, 83.271 ],
[ 105.029, 55.237 ], [ 110.740, 47.082 ],
[ 102.364, 36.104 ], [ 94.050, 40.940 ],
[ 85.189, 34.445 ], [ 85.963, 24.194 ],
[ 73.507, 19.930 ], [ 68.883, 28.936 ],
[ 59.118, 28.936 ], [ 54.494, 19.930 ],
[ 42.039, 24.194 ], [ 42.814, 34.445 ],
[ 33.951, 40.940 ], [ 25.637, 36.104 ],
[ 17.262, 47.082 ], [ 22.973, 55.237 ]
]
using Godot;
public partial class MyNode2D : Node2D
{
private float[,] _coordsHead =
{
{ 22.952f, 83.271f }, { 28.385f, 98.623f },
{ 53.168f, 107.647f }, { 72.998f, 107.647f },
{ 99.546f, 98.623f }, { 105.048f, 83.271f },
{ 105.029f, 55.237f }, { 110.740f, 47.082f },
{ 102.364f, 36.104f }, { 94.050f, 40.940f },
{ 85.189f, 34.445f }, { 85.963f, 24.194f },
{ 73.507f, 19.930f }, { 68.883f, 28.936f },
{ 59.118f, 28.936f }, { 54.494f, 19.930f },
{ 42.039f, 24.194f }, { 42.814f, 34.445f },
{ 33.951f, 40.940f }, { 25.637f, 36.104f },
{ 17.262f, 47.082f }, { 22.973f, 55.237f }
};
}
Этот формат, хотя и компактный, не является тем, который Godot понимает для рисования многоугольника. В другом сценарии нам пришлось бы загружать эти координаты из файла или вычислять позиции во время работы приложения, поэтому может потребоваться некоторое преобразование.
Чтобы преобразовать эти координаты в правильный формат, мы создадим новый метод float_array_to_Vector2Array(). Затем мы переопределим функцию _ready(), которую Godot вызовет только один раз - в начале выполнения - для загрузки этих координат в переменную:
var head : PackedVector2Array
func float_array_to_Vector2Array(coords : Array) -> PackedVector2Array:
# Convert the array of floats into a PackedVector2Array.
var array : PackedVector2Array = []
for coord in coords:
array.append(Vector2(coord[0], coord[1]))
return array
func _ready():
head = float_array_to_Vector2Array(coords_head);
private Vector2[] _head;
private Vector2[] FloatArrayToVector2Array(float[,] coords)
{
// Convert the array of floats into an array of Vector2.
int size = coords.GetUpperBound(0);
Vector2[] array = new Vector2[size + 1];
for (int i = 0; i <= size; i++)
{
array[i] = new Vector2(coords[i, 0], coords[i, 1]);
}
return array;
}
public override void _Ready()
{
_head = FloatArrayToVector2Array(_coordsHead);
}
Чтобы наконец нарисовать нашу первую фигуру, мы воспользуемся методом draw_polygon и передадим точки (как массив координат Vector2) и ее цвет, вот так:
func _draw():
# We are going to paint with this color.
var godot_blue : Color = Color("478cbf")
# We pass the PackedVector2Array to draw the shape.
draw_polygon(head, [ godot_blue ])
public override void _Draw()
{
// We are going to paint with this color.
Color godotBlue = new Color("478cbf");
// We pass the array of Vector2 to draw the shape.
DrawPolygon(_head, [godotBlue]);
}
Запустив, вы увидите что-то вроде этого:
Обратите внимание, что нижняя часть логотипа выглядит сегментированной — это потому, что для определения этой части было использовано малое количество точек. Чтобы смоделировать плавную кривую, мы могли бы добавить больше точек в наш массив или, может быть, использовать математическую функцию для интерполяции кривой и создания плавной формы из кода (см. example 2).
Многоугольники всегда соединяют свою последнюю определенную точку с первой, чтобы иметь замкнутую форму.
Отрисовка соединенных линий
Рисование последовательности соединенных линий, которые не замыкаются, чтобы сформировать многоугольник, очень похоже на предыдущий метод. Мы будем использовать соединенный набор линий, чтобы нарисовать рот логотипа Godot.
Сначала мы определим список координат, формирующих форму рта, например:
var coords_mouth = [
[ 22.817, 81.100 ], [ 38.522, 82.740 ],
[ 39.001, 90.887 ], [ 54.465, 92.204 ],
[ 55.641, 84.260 ], [ 72.418, 84.177 ],
[ 73.629, 92.158 ], [ 88.895, 90.923 ],
[ 89.556, 82.673 ], [ 105.005, 81.100 ]
]
private float[,] _coordsMouth =
{
{ 22.817f, 81.100f }, { 38.522f, 82.740f },
{ 39.001f, 90.887f }, { 54.465f, 92.204f },
{ 55.641f, 84.260f }, { 72.418f, 84.177f },
{ 73.629f, 92.158f }, { 88.895f, 90.923f },
{ 89.556f, 82.673f }, { 105.005f, 81.100f }
};
Мы загрузим эти координаты в переменную и определим дополнительную переменную с настраиваемой толщиной линии:
var mouth : PackedVector2Array
var _mouth_width : float = 4.4
func _ready():
head = float_array_to_Vector2Array(coords_head);
mouth = float_array_to_Vector2Array(coords_mouth);
private Vector2[] _mouth;
private float _mouthWidth = 4.4f;
public override void _Ready()
{
_head = FloatArrayToVector2Array(_coordsHead);
_mouth = FloatArrayToVector2Array(_coordsMouth);
}
И наконец, мы воспользуемся методом draw_polyline, чтобы нарисовать линию, вот так:
func _draw():
# We will use white to draw the line.
var white : Color = Color.WHITE
var godot_blue : Color = Color("478cbf")
draw_polygon(head, [ godot_blue ])
# We draw the while line on top of the previous shape.
draw_polyline(mouth, white, _mouth_width)
public override void _Draw()
{
// We will use white to draw the line.
Color white = Colors.White;
Color godotBlue = new Color("478cbf");
DrawPolygon(_head, [godotBlue]);
// We draw the while line on top of the previous shape.
DrawPolyline(_mouth, white, _mouthWidth);
}
Вы должны получить следующий вывод:
В отличие от draw_polygon(), полилинии могут иметь только один уникальный цвет для всех своих точек (второй аргумент). Этот метод имеет 2 дополнительных аргумента: ширину линии (которая по умолчанию минимально возможная) и включение или отключение сглаживания (по умолчанию оно отключено).
Порядок вызовов _draw важен - как и в случае с позициями Node в иерархии дерева, различные формы будут рисоваться сверху вниз, в результате чего последние формы будут скрывать более ранние, если они перекрываются. В этом случае мы хотим, чтобы рот был нарисован поверх головы, поэтому мы помещаем его после.
Обратите внимание, как мы можем определять цвета разными способами, либо с помощью шестнадцатеричного кода, либо с помощью предопределенного имени цвета. Проверьте класс Color для других констант и способов определения цветов.
Отрисовка кругов
Чтобы создать глаза, мы добавим 4 дополнительных вызова для рисования форм глаз, с различными размерами, цветами и позициями.
Чтобы отрисовать круг, вы располагаете его от центра используя метод draw_circle. Первый параметр это Vector2 координат его центра, второй - его радиус, а третий - его цвет:
func _draw():
var white : Color = Color.WHITE
var godot_blue : Color = Color("478cbf")
var grey : Color = Color("414042")
draw_polygon(head, [ godot_blue ])
draw_polyline(mouth, white, _mouth_width)
# Four circles for the 2 eyes: 2 white, 2 grey.
draw_circle(Vector2(42.479, 65.4825), 9.3905, white)
draw_circle(Vector2(85.524, 65.4825), 9.3905, white)
draw_circle(Vector2(43.423, 65.92), 6.246, grey)
draw_circle(Vector2(84.626, 66.008), 6.246, grey)
public override void _Draw()
{
Color white = Colors.White;
Color godotBlue = new Color("478cbf");
Color grey = new Color("414042");
DrawPolygon(_head, [godotBlue]);
DrawPolyline(_mouth, white, _mouthWidth);
// Four circles for the 2 eyes: 2 white, 2 grey.
DrawCircle(new Vector2(42.479f, 65.4825f), 9.3905f, white);
DrawCircle(new Vector2(85.524f, 65.4825f), 9.3905f, white);
DrawCircle(new Vector2(43.423f, 65.92f), 6.246f, grey);
DrawCircle(new Vector2(84.626f, 66.008f), 6.246f, grey);
}
При запуске, вы должны получить примерно это:
Для неполных, незаполненных дуг (частей формы круга между произвольными углами), вы можете использовать метод draw_arc.
Отрисовка линий
Что отрисовать последнюю часть (нос) мы используем линию.
draw_line может быть использован чтобы отрисовать одиночный сегмент используя начальные и конечные координаты для аргументов, подобно этому:
func _draw():
var white : Color = Color.WHITE
var godot_blue : Color = Color("478cbf")
var grey : Color = Color("414042")
draw_polygon(head, [ godot_blue ])
draw_polyline(mouth, white, _mouth_width)
draw_circle(Vector2(42.479, 65.4825), 9.3905, white)
draw_circle(Vector2(85.524, 65.4825), 9.3905, white)
draw_circle(Vector2(43.423, 65.92), 6.246, grey)
draw_circle(Vector2(84.626, 66.008), 6.246, grey)
# Draw a short but thick white vertical line for the nose.
draw_line(Vector2(64.273, 60.564), Vector2(64.273, 74.349), white, 5.8)
public override void _Draw()
{
Color white = Colors.White;
Color godotBlue = new Color("478cbf");
Color grey = new Color("414042");
DrawPolygon(_head, [godotBlue]);
DrawPolyline(_mouth, white, _mouthWidth);
DrawCircle(new Vector2(42.479f, 65.4825f), 9.3905f, white);
DrawCircle(new Vector2(85.524f, 65.4825f), 9.3905f, white);
DrawCircle(new Vector2(43.423f, 65.92f), 6.246f, grey);
DrawCircle(new Vector2(84.626f, 66.008f), 6.246f, grey);
// Draw a short but thick white vertical line for the nose.
DrawLine(new Vector2(64.273f, 60.564f), new Vector2(64.273f, 74.349f),
white, 5.8f);
}
Вы должны сейчас увидеть следующую форму на экране:
Заметьте что при отрисовке множественных несоединенных линий, вы можете получить дополнительную производительность рисуя все линии одним вызовом метода draw_multiline.
Отрисовка текста
Хотя использование узла Label является наиболее распространенным способом добавления текста в ваше приложение, низкоуровневая функция "_draw" включает в себя возможности для добавления текста в ваш пользовательский рисунок узла. Мы используем его, чтобы добавить "GODOT" под головой робота.
Мы будем использовать draw_string метод, для того чтобы сделать это, вот так:
var default_font : Font = ThemeDB.fallback_font;
func _draw():
var white : Color = Color.WHITE
var godot_blue : Color = Color("478cbf")
var grey : Color = Color("414042")
draw_polygon(head, [ godot_blue ])
draw_polyline(mouth, white, _mouth_width)
draw_circle(Vector2(42.479, 65.4825), 9.3905, white)
draw_circle(Vector2(85.524, 65.4825), 9.3905, white)
draw_circle(Vector2(43.423, 65.92), 6.246, grey)
draw_circle(Vector2(84.626, 66.008), 6.246, grey)
draw_line(Vector2(64.273, 60.564), Vector2(64.273, 74.349), white, 5.8)
# Draw GODOT text below the logo with the default font, size 22.
draw_string(default_font, Vector2(20, 130), "GODOT",
HORIZONTAL_ALIGNMENT_CENTER, 90, 22)
private Font _defaultFont = ThemeDB.FallbackFont;
public override void _Draw()
{
Color white = Colors.White;
Color godotBlue = new Color("478cbf");
Color grey = new Color("414042");
DrawPolygon(_head, [godotBlue]);
DrawPolyline(_mouth, white, _mouthWidth);
DrawCircle(new Vector2(42.479f, 65.4825f), 9.3905f, white);
DrawCircle(new Vector2(85.524f, 65.4825f), 9.3905f, white);
DrawCircle(new Vector2(43.423f, 65.92f), 6.246f, grey);
DrawCircle(new Vector2(84.626f, 66.008f), 6.246f, grey);
DrawLine(new Vector2(64.273f, 60.564f), new Vector2(64.273f, 74.349f),
white, 5.8f);
// Draw GODOT text below the logo with the default font, size 22.
DrawString(_defaultFont, new Vector2(20f, 130f), "GODOT",
HorizontalAlignment.Center, 90, 22);
}
Здесь мы сначала загружаем в переменную defaultFont настроенный шрифт темы по умолчанию (вместо него можно установить пользовательский шрифт), а затем передаем следующие параметры: шрифт, положение, текст, горизонтальное выравнивание, ширину и размер шрифта.
Вот, что вы увидите на экране:
Дополнительные параметры, а также другие методы, связанные с текстом и символами, можно найти в справочнике классов CanvasItem.
Показывать рисунок во время редактирования
Хотя код пока что может рисовать логотип в запущенном окне, он не будет отображаться в 2D view редактора. В некоторых случаях вы также захотите отобразить свой пользовательский Node2D или элемент управления в редакторе, чтобы разместить и масштабировать его соответствующим образом, как это делают большинство других узлов.
Чтобы отобразить логотип непосредственно в редакторе (не запуская его), можно использовать аннотацию @tool, чтобы запросить отображение пользовательского рисунка узла во время редактирования, например:
@tool
extends Node2D
using Godot;
[Tool]
public partial class MyNode2D : Node2D
Вам потребуется сохранить сцену, пересобрать проект (только для C#) и перезагрузить текущую сцену вручную с помощью пункта меню Сцена > Перезагрузить сохраненную сцену, чтобы обновить текущий узел в представлении 2D при первом добавлении или удалении аннотации @tool.
Анимация
Если бы мы хотели изменить пользовательскую форму во время выполнения, мы могли бы изменить вызываемые методы или их аргументы во время выполнения или применить преобразование.
Например, если мы хотим, чтобы только что созданная нами пользовательская фигура вращалась, мы можем добавить следующую переменную и код в методы _ready и _process:
extends Node2D
@export var rotation_speed : float = 1 # In radians per second.
func _ready():
rotation = 0
...
func _process(delta: float):
rotation -= rotation_speed * delta
[Export]
public float RotationSpeed { get; set; } = 1.0f; // In radians per second.
public override void _Ready()
{
Rotation = 0;
...
}
public override void _Process(double delta)
{
Rotation -= RotationSpeed * (float)delta;
}
Проблема с приведенным выше кодом заключается в том, что поскольку мы создали точки приблизительно на прямоугольнике, начиная с верхнего левого угла, координата (0, 0) и простираясь вправо и вниз, мы видим, что поворот выполняется с использованием верхнего левого угла в качестве опорной точки. Изменение преобразования положения на узле нам здесь не поможет, поскольку сначала применяется преобразование вращения.
Хотя мы могли бы переписать все координаты точек так, чтобы они были центрированы вокруг (0, 0), включая отрицательные координаты, это потребовало бы много работы.
Одним из возможных способов решения этой проблемы является использование метода нижнего уровня draw_set_transform для исправления этой проблемы, переводя все точки в собственном пространстве CanvasItem, а затем возвращая его в исходное место с помощью обычного преобразования узла либо в редакторе, либо в коде, например:
func _ready():
rotation = 0
position = Vector2(60, 60)
...
func _draw():
draw_set_transform(Vector2(-60, -60))
...
public override void _Ready()
{
Rotation = 0;
Position = new Vector2(60, 60);
...
}
public override void _Draw()
{
DrawSetTransform(new Vector2(-60.0f, -60.0f));
...
}
Вот результат вращения вокруг точки опоры на (60, 60):
Если мы хотим анимировать свойство внутри вызова _draw(), мы должны не забыть вызвать queue_redraw() для принудительного обновления, так как в противном случае оно не будет обновлено на экране.
Например, вот как мы можем заставить робота открывать и закрывать рот, изменяя ширину линии его рта по синусоидальной (sin) кривой:
var _mouth_width : float = 4.4
var _max_width : float = 7
var _time : float = 0
func _process(delta : float):
_time += delta
_mouth_width = abs(sin(_time) * _max_width)
queue_redraw()
func _draw():
...
draw_polyline(mouth, white, _mouth_width)
...
private float _mouthWidth = 4.4f;
private float _maxWidth = 7f;
private float _time = 0f;
public override void _Process(double delta)
{
_time += (float)delta;
_mouthWidth = Mathf.Abs(Mathf.Sin(_time) * _maxWidth);
QueueRedraw();
}
public override void _Draw()
{
...
DrawPolyline(_mouth, white, _mouthWidth);
...
}
Примерно так оно будет выглядеть при запуске:
Обратите внимание, что _mouth_width - это определяемое пользователем свойство, как и любое другое, и оно или любое другое, используемое в качестве аргумента рисования, может быть анимировано с использованием более стандартных и высокоуровневых методов, таких как узел Tween или AnimationPlayer. Единственное отличие заключается в том, что для применения этих изменений, чтобы они отображались на экране, необходим вызов queue_redraw().
Пример 2: рисование линии в динамике
Предыдущий пример был полезен для изучения того, как рисовать и изменять узлы с помощью пользовательских фигур и анимаций. Это может иметь некоторые преимущества, такие как использование точных координат и векторов для рисования, а не растровых изображений, что означает, что они будут хорошо масштабироваться при трансформации на экране. В некоторых случаях похожих результатов можно достичь, составляя функциональность более высокого уровня с узлами, такими как sprites или AnimatedSprites, загружающими ресурсы SVG (которые также являются изображениями, определенными с помощью векторов), и узлом AnimationPlayer.
В других случаях это будет невозможно, поскольку мы не будем знать, каким будет итоговое графическое представление до запуска кода. Здесь мы увидим, как нарисовать динамическую линию, координаты которой заранее неизвестны и на которые влияет ввод пользователя.
Проведем прямую линию между 2 точек
Предположим, мы хотим нарисовать прямую линию между двумя точками, первая из которых будет зафиксирована в верхнем левом углу (0, 0), а вторая будет определяться положением курсора на экране.
Мы могли бы провести динамическую линию между этими двумя точками следующим образом:
extends Node2D
var point1 : Vector2 = Vector2(0, 0)
var width : int = 10
var color : Color = Color.GREEN
var _point2 : Vector2
func _process(_delta):
var mouse_position = get_viewport().get_mouse_position()
if mouse_position != _point2:
_point2 = mouse_position
queue_redraw()
func _draw():
draw_line(point1, _point2, color, width)
using Godot;
using System;
public partial class MyNode2DLine : Node2D
{
public Vector2 Point1 { get; set; } = new Vector2(0f, 0f);
public int Width { get; set; } = 10;
public Color Color { get; set; } = Colors.Green;
private Vector2 _point2;
public override void _Process(double delta)
{
Vector2 mousePosition = GetViewport().GetMousePosition();
if (mousePosition != _point2)
{
_point2 = mousePosition;
QueueRedraw();
}
}
public override void _Draw()
{
DrawLine(Point1, _point2, Color, Width);
}
}
В этом примере мы получаем позицию мыши в области просмотра по умолчанию в каждом кадре с помощью метода get_mouse_position. Если позиция изменилась с момента последнего запроса на отрисовку (небольшая оптимизация, чтобы избежать перерисовки в каждом кадре) - мы запланируем перерисовку. Наш метод _draw() имеет только одну строку: запрос на отрисовку зеленой линии шириной 10 пикселей между верхним левым углом и полученной позицией.
Ширину, цвет и положение начальной точки можно настроить с помощью соответствующих свойств.
При запуске, мы увидим следующее:
Рисование дуги между 2 точками
Пример выше работает, но мы можем захотеть присоединить эти 2 точки с другой формой или функцией, отличной от прямой линии.
Давайте попробуем создать дугу (часть окружности) между двумя точками.
Экспортируя стартовую точку линии, сегменты, длину, цвет и сглаживание позволит нам очень просто модифицировать эти свойства напрямую из панели инспектора:
extends Node2D
@export var point1 : Vector2 = Vector2(0, 0)
@export_range(1, 1000) var segments : int = 100
@export var width : int = 10
@export var color : Color = Color.GREEN
@export var antialiasing : bool = false
var _point2 : Vector2
using Godot;
using System;
public partial class MyNode2DLine : Node2D
{
[Export]
public Vector2 Point1 { get; set; } = new Vector2(0f, 0f);
[Export]
public float Length { get; set; } = 350f;
[Export(PropertyHint.Range, "1,1000,")]
public int Segments { get; set; } = 100;
[Export]
public int Width { get; set; } = 10;
[Export]
public Color Color { get; set; } = Colors.Green;
[Export]
public bool AntiAliasing { get; set; } = false;
private Vector2 _point2;
}
Чтобы нарисовать дугу, мы можем использовать метод draw_arc. Существует множество дуг, проходящих через 2 точки, поэтому мы выберем для этого примера полукруг с центром в средней точке между 2 начальными точками.
Вычисление этой дуги может быть более сложным чем в случае линии:
func _draw():
# Average points to get center.
var center : Vector2 = Vector2((_point2.x + point1.x) / 2,
(_point2.y + point1.y) / 2)
# Calculate the rest of the arc parameters.
var radius : float = point1.distance_to(_point2) / 2
var start_angle : float = (_point2 - point1).angle()
var end_angle : float = (point1 - _point2).angle()
if end_angle < 0: # end_angle is likely negative, normalize it.
end_angle += TAU
# Finally, draw the arc.
draw_arc(center, radius, start_angle, end_angle, segments, color,
width, antialiasing)
public override void _Draw()
{
// Average points to get center.
Vector2 center = new Vector2((_point2.X + Point1.X) / 2.0f,
(_point2.Y + Point1.Y) / 2.0f);
// Calculate the rest of the arc parameters.
float radius = Point1.DistanceTo(_point2) / 2.0f;
float startAngle = (_point2 - Point1).Angle();
float endAngle = (Point1 - _point2).Angle();
if (endAngle < 0.0f) // endAngle is likely negative, normalize it.
{
endAngle += Mathf.Tau;
}
// Finally, draw the arc.
DrawArc(center, radius, startAngle, endAngle, Segments, Color,
Width, AntiAliasing);
}
Центр полукруга будет точкой посередине между обеими точками. Радиус будет половиной расстояния между обеими точками. Начальный и конечный углы будут углами вектора от точки1 до точки2 и наоборот. Заметьте что мы нормализуем end_angle в позитивные значения поскольку если end_angle меньше чем start_angle, дуга будет отрисована против часовой стрелки, что мы не хотим в этом случае (дуга будет смотреть сверху-вниз).
Результат будет выглядеть как-то так, с дугой смотрящей вниз и между точек:
Не стесняйтесь поиграть с параметрами в инспекторе для получения различных результатов: измените цвет, длину, сглаживание, и увеличьте количество сегментов для усиления сглаживания кривой, за цену производительности.