Пользовательская отрисовка в 2D¶
Введение¶
В Godot есть узлы для рисования спрайтов, многоугольников, частиц и прочего. В большинстве случаев этого достаточно; но не всегда. Прежде чем плакать от страха, тоски и ярости из-за того, что узла для рисования этого конкретного чего-то не существует... было бы хорошо знать, что можно легко заставить любой 2D-узел (на основе Control < class_Control> `или :ref: Node2D <class_Node2D> `) рисовать пользовательские команды. Также это действительно легко сделать.
Custom drawing in a 2D node is really useful. Here are some use cases:
Drawing shapes or logic that existing nodes can't do, such as an image with trails or a special animated polygon.
Визуализации, несовместимые с узлами, как доска тетриса. (В примере с тетрисом для рисования блоков используется пользовательская функция рисования блоков.)
Drawing a large number of simple objects. Custom drawing avoids the overhead of using a large number of nodes, possibly lowering memory usage and improving performance.
Making a custom UI control. There are plenty of controls available, but when you have unusual needs, you will likely need a custom control.
Отрисовка¶
Добавьте сценарий к любому производному узлу CanvasItem, например Control или Node2D. Затем переопределите функцию _draw ()
.
extends Node2D
func _draw():
# Your draw commands here
pass
public override void _Draw()
{
// Your draw commands here
}
Команды рисования описаны в справочнике по классу CanvasItem. Их много.
Обновление¶
Функция _draw ()
вызывается только один раз, а затем команды рисования кэшируются и запоминаются, поэтому дальнейшие вызовы не нужны.
If re-drawing is required because a state or something else changed,
call CanvasItem.update()
in that same node and a new _draw()
call will happen.
Вот немного более сложный пример, переменная текстуры, которая будет перерисована при изменении:
extends Node2D
export (Texture) var texture setget _set_texture
func _set_texture(value):
# If the texture variable is modified externally,
# this callback is called.
texture = value # Texture was changed.
update() # Update the node's visual representation.
func _draw():
draw_texture(texture, Vector2())
public class CustomNode2D : Node2D
{
private Texture _texture;
public Texture Texture
{
get
{
return _texture;
}
set
{
_texture = value;
Update();
}
}
public override void _Draw()
{
DrawTexture(_texture, new Vector2());
}
}
В некоторых случаях может потребоваться отрисовка каждого кадра. Для этого просто вызовите update ()
из обратного вызова _process ()
, например:
extends Node2D
func _draw():
# Your draw commands here
pass
func _process(delta):
update()
public class CustomNode2D : Node2D
{
public override void _Draw()
{
// Your draw commands here
}
public override void _Process(float delta)
{
Update();
}
}
Пример: рисование дуг окружности¶
Теперь мы будем использовать пользовательские функции рисования Godot Engine, чтобы рисовать то, для чего Godot не предоставляет функций. В качестве примера Godot предоставляет функцию draw_circle ()
, которая рисует весь круг. Однако как насчет рисования части круга? Вам нужно будет написать функцию для этого и нарисовать ее самостоятельно.
Функция дуги¶
Дуга определяется параметрами опорной окружности, то есть положением центра и радиусом. Сама дуга затем определяется углом, с которого она начинается, и углом, под которым она заканчивается. Это 4 аргумента, которые мы должны предоставить нашей функции рисования. Мы также предоставим значение цвета, чтобы при желании можно было нарисовать дугу разными цветами.
По сути, рисование фигуры на экране требует, чтобы она была разложена на определенное количество точек, связанных между собой. Как вы понимаете, чем из большего количества точек состоит ваша фигура, тем более гладкой она будет выглядеть, но тем тяжелее она будет также с точки зрения затрат на обработку. В общем, если ваша фигура огромна (или в 3D, близко к камере), для ее рисования потребуется больше точек, но она не будет угловатой. Напротив, если ваша фигура маленькая (или в 3D, вдали от камеры), вы можете уменьшить количество точек, чтобы сократить затраты на обработку; это известно как Уровень Детализации (LOD). В нашем примере мы просто будем использовать фиксированное количество точек независимо от радиуса.
func draw_circle_arc(center, radius, angle_from, angle_to, color):
var nb_points = 32
var points_arc = PoolVector2Array()
for i in range(nb_points + 1):
var angle_point = deg2rad(angle_from + i * (angle_to-angle_from) / nb_points - 90)
points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)
for index_point in range(nb_points):
draw_line(points_arc[index_point], points_arc[index_point + 1], color)
public void DrawCircleArc(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
{
int nbPoints = 32;
var pointsArc = new Vector2[nbPoints];
for (int i = 0; i < nbPoints; ++i)
{
float anglePoint = Mathf.Deg2Rad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90f);
pointsArc[i] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
}
for (int i = 0; i < nbPoints - 1; ++i)
DrawLine(pointsArc[i], pointsArc[i + 1], color);
}
Помните, на сколько точек нужно разложить нашу фигуру? Мы зафиксировали это число в переменной nb_points
на значение 32
. Затем мы инициализируем пустой PoolVector2Array
, который представляет собой просто массив ``Vector2 ``s.
Следующий шаг состоит в вычислении фактического положения этих 32 точек, составляющих дугу. Это делается в первом цикле for: мы перебираем количество точек, для которых мы хотим вычислить позиции, плюс одна, чтобы включить последнюю точку. Сначала мы определяем угол каждой точки между начальным и конечным углами.
Причина, по которой каждый угол уменьшается на 90°, заключается в том, что мы будем вычислять 2D-положения для каждого угла, используя тригонометрию (вы знаете, косинус и синус...). Однако для простоты cos ()
и sin ()
используют радианы, а не градусы. Угол 0° (0 радиан) начинается с 3 часов, хотя мы хотим начать отсчет с 12 часов. Таким образом, мы уменьшаем каждый угол на 90 °, чтобы начать отсчет с 12 часов.
Фактическое положение точки, расположенной на окружности под углом angle
(в радианах), определяется как Vector2(cos (angle), sin (angle))
. Так как cos ()
и sin ()
возвращают значения от -1 до 1, позиция находится на окружности с радиусом 1. Чтобы это положение было на нашей опорной окружности, которая имеет радиус radius
, нам просто нужно умножить позицию на radius
. Наконец, нам нужно расположить наш опорный круг в центральном
положении, что выполняется путем добавления его к нашему значению Vector2
. Наконец, мы вставляем точку в массив PoolVector2Array
, который был определен ранее.
Теперь нам нужно на самом нарисовать наши точки. Как вы можете себе представить, мы не просто нарисуем наши 32 точки: нам нужно нарисовать все, что находится между каждой из них. Мы могли бы сами вычислить каждую точку, используя предыдущий метод, и нарисовать ее одну за другой. Но это слишком сложно и неэффективно (за исключением случаев, когда это явно необходимо), поэтому мы просто проводим линии между каждой парой точек. Если радиус нашего круга поддержки не велик, длина каждой линии между парой точек никогда не будет достаточной, чтобы их увидеть. Если бы это произошло, нам просто нужно было бы увеличить количество точек.
Отрисовка дуги на экране¶
Теперь у нас есть функция, которая рисует что-то на экране; пришло время вызвать ее внутри функции _draw ()
:
func _draw():
var center = Vector2(200, 200)
var radius = 80
var angle_from = 75
var angle_to = 195
var color = Color(1.0, 0.0, 0.0)
draw_circle_arc(center, radius, angle_from, angle_to, color)
public override void _Draw()
{
var center = new Vector2(200, 200);
float radius = 80;
float angleFrom = 75;
float angleTo = 195;
var color = new Color(1, 0, 0);
DrawCircleArc(center, radius, angleFrom, angleTo, color);
}
Результат:

Функция сегмента¶
Мы можем сделать шаг вперед и не только написать функцию, которая рисует простую часть диска, определенную дугой, но и ее форму. Метод точно такой же, как и раньше, за исключением того, что мы рисуем многоугольник вместо линий:
func draw_circle_arc_poly(center, radius, angle_from, angle_to, color):
var nb_points = 32
var points_arc = PoolVector2Array()
points_arc.push_back(center)
var colors = PoolColorArray([color])
for i in range(nb_points + 1):
var angle_point = deg2rad(angle_from + i * (angle_to - angle_from) / nb_points - 90)
points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)
draw_polygon(points_arc, colors)
public void DrawCircleArcPoly(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
{
int nbPoints = 32;
var pointsArc = new Vector2[nbPoints + 1];
pointsArc[0] = center;
var colors = new Color[] { color };
for (int i = 0; i < nbPoints; ++i)
{
float anglePoint = Mathf.Deg2Rad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90);
pointsArc[i + 1] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
}
DrawPolygon(pointsArc, colors);
}

Динамическое пользовательское рисование¶
Хорошо, теперь мы можем рисовать на экране пользовательские объекты. Однако они статичны; давайте заставим эту фигуру вращаться вокруг центра. Решение состоит в том, чтобы просто изменять значения angle_from и angle_to с течением времени. Для нашего примера мы просто увеличим их на 50. Это значение приращения должно оставаться постоянным, иначе скорость вращения соответственно изменится.
Во-первых, мы должны сделать переменные angle_from и angle_to глобальными в верхней части нашего скрипта. Также обратите внимание, что вы можете хранить их в других узлах и получать к ним доступ с помощью get_node()
.
extends Node2D
var rotation_angle = 50
var angle_from = 75
var angle_to = 195
public class CustomNode2D : Node2D
{
private float _rotationAngle = 50;
private float _angleFrom = 75;
private float _angleTo = 195;
}
Мы изменяем эти значения в функции _process(delta).
Здесь мы также увеличиваем наши значения angle_from и angle_to. Однако мы не должны забывать применять wrap()
в диапазоне от 0 до 360° для результирующих значений! То есть, если угол равен 361°, то на самом деле он равен 1°. Если вы не обернете эти значения, скрипт будет работать правильно, но значения угла будут со временем увеличиваться и увеличиваться, пока не достигнут максимального целочисленного значения, которым может управлять Godot (2^31 - 1
). Когда это произойдет, Godot может выйти из строя или привести к неожиданному поведению.
Наконец, мы не должны забывать вызывать функцию update()
, которая автоматически вызывает _draw()
. Таким образом, вы можете контролировать, когда вы хотите обновить кадр.
func _process(delta):
angle_from += rotation_angle
angle_to += rotation_angle
# We only wrap angles when both of them are bigger than 360.
if angle_from > 360 and angle_to > 360:
angle_from = wrapf(angle_from, 0, 360)
angle_to = wrapf(angle_to, 0, 360)
update()
public override void _Process(float delta)
{
_angleFrom += _rotationAngle;
_angleTo += _rotationAngle;
// We only wrap angles when both of them are bigger than 360.
if (_angleFrom > 360 && _angleTo > 360)
{
_angleFrom = Mathf.Wrap(_angleFrom, 0, 360);
_angleTo = Mathf.Wrap(_angleTo, 0, 360);
}
Update();
}
Кроме того, не забудьте изменить функцию _draw()
, чтобы использовать эти переменные:
func _draw():
var center = Vector2(200, 200)
var radius = 80
var color = Color(1.0, 0.0, 0.0)
draw_circle_arc( center, radius, angle_from, angle_to, color )
public override void _Draw()
{
var center = new Vector2(200, 200);
float radius = 80;
var color = new Color(1, 0, 0);
DrawCircleArc(center, radius, _angleFrom, _angleTo, color);
}
Давайте запустим! Это работает, но дуга вращается безумно быстро! Что не так?
Причина в том, что ваш графический процессор на самом деле отображает кадры так быстро, как только может. Нам нужно "нормализовать" рисунок под эту скорость; для этого мы должны использовать параметр delta
функции _process ()
. delta
содержит время, прошедшее между двумя последними отрисованными кадрами. Как правило, он невелик (около 0,0003 секунды, но это зависит от вашего оборудования), поэтому использование delta
для управления вашим рисунком гарантирует, что ваша программа будет работать с одинаковой скоростью на любом оборудовании.
В нашем случае нам просто нужно умножить нашу переменную rotation_angle
на delta
в функции _process()
. Таким образом, наши 2 угла будут увеличены на гораздо меньшее значение, которое напрямую зависит от скорости рендеринга.
func _process(delta):
angle_from += rotation_angle * delta
angle_to += rotation_angle * delta
# We only wrap angles when both of them are bigger than 360.
if angle_from > 360 and angle_to > 360:
angle_from = wrapf(angle_from, 0, 360)
angle_to = wrapf(angle_to, 0, 360)
update()
public override void _Process(float delta)
{
_angleFrom += _rotationAngle * delta;
_angleTo += _rotationAngle * delta;
// We only wrap angles when both of them are bigger than 360.
if (_angleFrom > 360 && _angleTo > 360)
{
_angleFrom = Wrap(_angleFrom, 0, 360);
_angleTo = Wrap(_angleTo, 0, 360);
}
Update();
}
Давайте снова запустим! На этот раз вращение отображается правильно!
Antialiased drawing¶
Godot предлагает параметры метода в draw_line для включения сглаживания, но это не работает надёжно во всех ситуациях (например, на мобильных/веб-платформах, или когда включен HDR). В draw_polygon также отсутствует параметр antialiased
.
В качестве временного решения установите и используйте надстройку Antialiased Line2D (которая также поддерживает сглаженное рисование Polygon2D). Обратите внимание, что это дополнение опирается на высокоуровневые узлы, а не на низкоуровневые функции _draw()
.
Инструменты¶
Drawing your own nodes might also be desired while running them in the editor. This can be used as a preview or visualization of some feature or behavior. See Запуск кода в редакторе for more information.