Иногда случается такое, что движок попросту ограничивает нескончаемый поток идей. Так, однажды и мне захотелось рисовать откуда угодно, что угодно и где угодно. Об этом и пойдет речь.
Для начала представим такую тривиальную задачу: по клику мыши нужно рисовать некоторый примитив, который задает пользователь, например, нажатием клавиш. Звучит просто, однако с Godot это может вызвать головную боль.
Кто не знает, поясню: в Godot рисование примитивов осуществляется в виртуальном методе _draw
объекта CanvasItem
, т.е. нельзя просто так взять, и нарисовать линию, например, из метода_process
. Как выход, можно создать массивы для каждого примитива, а в методе _draw
прогонять каждый массив, рисуя соответствующий примитив. При обработке же клика записывать в соответствующий массив позицию мыши. Уже звучит страшно, не так ли? А если пользователь может менять размер? Создавать свои примитивы? Так массивов с объектами не напасешься.
Наверно во всех приемлемых движках есть сурфейсы – «холсты» для отрисовки всякого разного. Godot – не исключение, и хоть на первый взгляд кажется что они скрыты где-то глубоко в бэкэнде, на самом деле это не так. Все ноды в godot используют четыре основных сервера как API для различных модулей, меня же будет интересовать один – это сервер для проработки всего визуала под названием VisualServer
. Подробнее про него (правда на английском) можно прочитать в официальной документации Godot. Также есть небольшая статья, описывающая работу сия волшебства на практике, причем не только про VisualServer
.
Каждый CanvasItem
при рисовании чего-либо на самом деле обращается к этому самому серверу, так зачем же нам эти лишние переадресации, когда мы можем сами обращаться за рисованием? Сам объект этого сервера имеет несметное количество методов для работы с графикой, я для примера буду использовать простейшие методы для отрисовки примитивов. Каждый такой метод требует RID
(Resource ID), по сути просто номер сурфейса в World2D
. У любого CanvasItem
этот его номер можно получить через метод get_canvas_item()
, и нагло рисовать на холсте этого объекта из любой точки программы, чего нельзя добиться с методом _draw
. От теории перейдем к практике, создадим простейшую сцену, состоящую из трех узлов:
Задача: Нода Controller
рисует на холсте ноды Drawer
без ее ведомства.
Решить ее "традиционным" методом никак не выйдет, поэтому прибегнем к вышеописанному серверу. В ноду Controller
засунем следующий код:
extends Node2D
onready var drawer = $"../Drawer"
var counter = 0
func custom_draw_line(start, goal, color, width=1.0, antialising=false):
VisualServer.canvas_item_add_line(drawer.get_canvas_item(), start, goal, color, width, antialising)
func _process(delta):
if Input.is_action_just_pressed("mouse_left"):
counter += 2
custom_draw_line(Vector2(100, 100)+Vector2(counter, counter), Vector2(300, 150)+Vector2(counter, counter), Color.green)
В методе custom_draw_line(...)
по сути и происходит вся магия, мы обращаемся к VisualServer
за рисованием линии через метод canvas_item_add_line(...)
, а в качестве холста передаем холст ноды Drawer
. Особенность заключается в том, что как бы мы не влияли на Controller
, изменяя его трансформацию или позицию, это никак не отражается на том что мы рисуем, потому как холст принадлежит другому узлу.
Тут можно было бы радоваться и хлопать в ладоши, однако все не так гладко. Если мы изменим размер окна или каким либо еще другим образом заставим движок отправить ноду на перерисовку, заметим, что линии пропали. Почему так происходит? Обратимся к исходникам Godot, благо его лицензия позволяет. Вот код, который перерисовывает все содержимое:
void CanvasItem::_update_callback() {
if (!is_inside_tree()) {
pending_update = false;
return;
}
RenderingServer::get_singleton()->canvas_item_clear(get_canvas_item());
//todo updating = true - only allow drawing here
if (is_visible_in_tree()) { //todo optimize this!!
if (first_draw) {
notification(NOTIFICATION_VISIBILITY_CHANGED);
first_draw = false;
}
drawing = true;
current_item_drawn = this;
notification(NOTIFICATION_DRAW);
emit_signal(SceneStringNames::get_singleton()->draw);
if (get_script_instance()) {
get_script_instance()->call(SceneStringNames::get_singleton()->_draw);
}
current_item_drawn = nullptr;
drawing = false;
}
//todo updating = false
pending_update = false; // don't change to false until finished drawing (avoid recursive update)
}
Нас конкретно интересует строчка 7:
RenderingServer::get_singleton()->canvas_item_clear(get_canvas_item())
из которой мы можем понять, что каждый раз перед перерисовкой холст очищается. Получается что рисовать на холсте самого объекта не совсем правильно, т.к. придется запоминать все предыдущие рендеры, что очень муторно и не есть хорошо. Вместо этого мы можем использовать еще один сурфейс, дочерний по отношению к холсту ноды Drawer
. Создать холст мы можем командой VisualServer.canvas_item_create()
, однако пока это просто сурфейс, летающий где то в памяти, что бы его увидеть, он должен наследоваться от World2D.canvas
(это "главный" холст) или от его наследников и т.д. Мы же хотим наследовать все свойства ноды Drawer
, значит надо унаследовать новоиспеченный холст от холста этого узла. Это также делается через VisualServer
командойVisualServer.canvas_item_set_parent(item, parent)
. Используя такой метод мы убиваем двух зайцев сразу: теперь при перерисовке наш холст никто не трогает, а также мы можем различать холст самой ноды от нашего. Например, можно очищать этот холст, не трогая при этом основной. Делается это опять же через сервер:VisualServer.canvas_item_clear(surface)
. В конце концов получим следующий код:
extends Node2D
onready var drawer = $"../Drawer"
var counter = 0
var surface
func _ready():
surface = VisualServer.canvas_item_create()
VisualServer.canvas_item_set_parent(surface, drawer.get_canvas_item())
func custom_draw_line(start, goal, color, width=1.0, antialising=false):
VisualServer.canvas_item_add_line(surface, start, goal, color, width, antialising)
func _process(delta):
if Input.is_action_just_pressed("mouse_left"):
counter += 2
custom_draw_line(Vector2(100, 100)+Vector2(counter, counter), Vector2(300, 150)+Vector2(counter, counter), Color.green)
elif Input.is_action_just_pressed("mouse_right"):
VisualServer.canvas_item_clear(surface)
counter = 0
На ЛКМ мы рисуем линии с некоторым интервалом, на ПКМ очищаем холст и сбрасываем интервалы. Подмечу, что я ни разу не тронул саму ноду Drawer
, она в данном примере служит просто бездушным телом.
Также может возникнуть вопрос об оптимизации, однако внимательный читатель догадается, что с ней тоже все более чем хорошо. Как уже было сказано, любой CanvasItem
обращается к серверу за перерисовкой, мы можем убедиться в этом взглянув на исходники класса CanvasItem
(функция рисования линии):
void CanvasItem::draw_line(const Point2 &p_from, const Point2 &p_to, const Color &p_color, real_t p_width) {
ERR_FAIL_COND_MSG(!drawing, "Drawing is only allowed inside NOTIFICATION_DRAW, _draw() function or 'draw' signal.");
RenderingServer::get_singleton()->canvas_item_add_line(canvas_item, p_from, p_to, p_color, p_width);
}
Как мы видим в строке 4, нода сама говорит VisualServer
рисовать на ее холсте, поэтому скорость отрисовки будет даже немного выше, за счет того что мы напрямую обращаемся к серверу.
А где вообще это все может пригодится? Например для любителей разделения обязанностей, т.е. за отрисовку по каким-либо иным алгоритмам отвечает один объект, никак не привязанный к сцене. Например, создать наследника класса Resource
со статическими методами, отвечающими за необходимую отрисовку. Данный подход поможет в целом упростить работу с графикой в силу отсутствия объекта отрисовки на сцене, а как следствие к нему не придется обращаться через дерево сцены, достаточно описать статические методы в некотором классе или воспользоваться автозагрузкой.
Также такими модулями гораздо проще делиться с сообществом, их можно сохранить как скрипт и передавать остальным просто этот файл, а не мудрить тексты узлов с подробным описанием подключения сценария к сцене. Или же просто сохранить для себя, "на будущее". Так, например, я реализовал функцию отрисовки гексагональной сетки (пример ниже), теперь она никуда от меня не денется, и я смогу просто при необходимости загружать скрипт в проект и использовать его "из коробки", без присоединения его к ноде и настройки зависимостей.
О том как рисовать такие сетки может когда-либо расскажу, однако уже есть полноценная статья о шестиугольных тайлмапах на английском и ее перевод на хабре. Основные знания брал оттуда, хотя я не согласен с некоторыми моментами.
Оставлять исходники проекта не вижу смысла, т.к. код и структура на мой взгляд достаточно просты для понимания.
В целом, такой подход облегчает отрисовку примитивов и сложных форм, состоящих из оных. Также, он поможет сократить количество зависимостей на сцене и позволит рисовать из любого метода, не только из _draw
объекта CanvasItem
. Однако, я сильно сомневаюсь что это поможет при рисовании спрайтов и тем более рендеринга 3D объектов, хотя и это, разумеется, возможно.
Надеюсь столь небольшой пост имеет не столь маленькое значение. Я хочу сказать следующее: не слушайте меня, слушайте официальную документацию Godot и его исходники. Напоследок пожелаю удачи в работе со столь гибким и доступным движком, коих сейчас так мало!