Иногда случается такое, что движок попросту ограничивает нескончаемый поток идей. Так, однажды и мне захотелось рисовать откуда угодно, что угодно и где угодно. Об этом и пойдет речь.
Для начала представим такую тривиальную задачу: по клику мыши нужно рисовать некоторый примитив, который задает пользователь, например, нажатием клавиш. Звучит просто, однако с 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 и его исходники. Напоследок пожелаю удачи в работе со столь гибким и доступным движком, коих сейчас так мало!
LibrarianOok
Уважаемый автор, положа руку на самое дорогое, скажите, можно ли ожидать ещё статьи про Godot?
Goerging Автор
Конкретно про годо пока не планировал, но по крайней мере косвенно он будет участвовать точно, как средство реализации. Пока просто нет идей для постов про годо, кроме очередных туториалов для новичков. Если я этим и займусь, то точно не в ближайшее время.
stalker320
А что насчёт более серьёзных туториалов, не для новичков? Как насчёт дерева навыков?
Goerging Автор
Стыдно признаться — на годо такие типичные вещи как дерево навыков или тот же инвентарь не реализовывал, но идеи насчет этого уже есть. В любом случае я пока работаю над материалом к следующему посту, а ваша тема кстати как раз является логическим продолженем моей. Так что для себя попробую точно, если что то получится то и с хабром поделюсь.
OpenMind4423
Спасибо огромное за статью!
Но хотелось бы увидеть ещё темы:
1. Splatmap. Как пользоваться правильно.
2. Spline объекты. Аддон Scatter.
3. ShellFur. Настрока и использование.
4. Как создать портал для прохождения через него игрока.
5. interior mapping.
6. Decals. Пока 4 не вышел, тема актуальна.
Вообще ещё интересно было бы как сделать стилизированный текст. Ибо стандартная нода грустна.
Надо ещё подумать что я забыл.
Goerging Автор
Большинство ваших идей относятся к 3D, с которым я к сожалению
(скорее к счастью)никогда не работал, да и пока не собираюсь. Я только не понял насчет стилизированного текста, что именно имеется в виду? Шрифты меняются во вкладкеCustomFonts
или встраиваются в тему ноды.OpenMind4423
я имел ввиду создание текста залитого текстурой. Например, мне нужно сделать надписи краской на бетонной стене. Понятно, что я могу просто нарисовать эти буквы на материале целиком, или что ещё более безумно, просто двигать квадратик по развертке, выбирая нужное. Но это всё конечно совсем адско.