Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Власне малювання в 2D

Вступ

Godot має вузли для малювання спрайтів, багатокутників, частинок, тексту та багатьох інших поширених потреб для розробки ігор. Однак, якщо вам потрібно щось конкретне, не охоплене стандартними вузлами, ви можете змусити будь-який 2D-вузол (наприклад, Control або Node2D-базований) малювати на екрані за допомогою спеціальних команди.

Власне малювання в 2D вузлах дійсно корисне. Ось деякі випадки використання:

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

  • Малювання великої кількості простих об'єктів, таких як сітка або дошка для двовимірної гри. Настроюване креслення дозволяє уникнути накладних витрат на використання великої кількості вузлів, можливо, зменшуючи використання пам’яті та покращуючи продуктивність.

  • Створення власного елемента керування інтерфейсом користувача. Доступно багато елементів керування, але коли у вас є незвичайні потреби, вам, ймовірно, знадобиться власний контроль.

Малюнок

Додайте скрипт до будь-якого похідного вузла CanvasItem, наприклад Control або Node2D. Потім замініть функцію _draw().

extends Node2D

func _draw():
    pass  # 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())

Щоб побачити це в дії, ви можете встановити текстуру як значок Годо в редакторі, перетягнувши файл за замовчуванням icon.svg з вкладки FileSystem до властивості Texture на вкладці Inspector. Під час зміни значення властивості Texture під час роботи попереднього скрипта текстура також зміниться автоматично.

У деяких випадках може знадобитися перемалювати кожен кадр. Для цього викличте queue_redraw з методу _process, ось так:

extends Node2D

func _draw():
    pass  # Your draw commands here.

func _process(_delta):
    queue_redraw()

Вирівнювання координат і ширини лінії

API малювання використовує систему координат CanvasItem, не обов’язково піксельні координати. Це означає, що _draw() використовує простір координат, створений після застосування перетворення CanvasItem. Крім того, ви можете застосувати спеціальне перетворення поверх нього за допомогою draw_set_transform або draw_set_transform_matrix.

Використовуючи draw_line, слід враховувати ширину лінії. Якщо використовується ширина непарного розміру, положення початкової та кінцевої точок має бути зміщено на 0,5, щоб зберегти лінію по центру, як показано нижче.

../../_images/draw_line.png
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)

Те саме стосується методу draw_rect із filled = false.

../../_images/draw_rect.png
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)

Згладжування

Godot пропонує параметри методу в draw_line для ввімкнення згладжування, але не всі спеціальні методи малювання пропонують цей параметр antialiased.

Для спеціальних методів малювання, які не забезпечують параметр antialiased, ви можете натомість увімкнути 2D MSAA, що впливає на рендеринг у всьому вікні перегляду. Це забезпечує високоякісне згладжування, але вищу вартість продуктивності та лише для певних елементів. Перегляньте 2D згладжування для отримання додаткової інформації.

Ось порівняння лінії мінімальної ширини (width=-1), намальованої за допомогою antialiased=false, antialiased=true і antialiased=false з 2D MSAA 2x , 4x і 8x увімкнено.

../../_images/draw_antialiasing_options.webp

Інструменти

Також може знадобитися малювати власні вузли під час їх запуску в редакторі. Це можна використовувати як попередній перегляд або візуалізацію певної функції чи поведінки.

Для цього ви можете використати tool annotation як на GDScript, так і на C#. Перегляньте приклад нижче та Запуск коду в редакторі для отримання додаткової інформації.

Приклад 1: малювання нестандартної форми

Тепер ми скористаємося спеціальними функціями малювання Godot Engine, щоб намалювати щось, для чого Godot не надає функцій. Ми відтворимо логотип Godot, але з кодом, лише за допомогою функцій малювання.

Вам доведеться закодувати функцію, щоб виконати це, і намалювати її самостійно.

Примітка

У наведених нижче інструкціях використовується фіксований набір координат, який може бути замалим для екранів із високою роздільною здатністю (більше 1080p). Якщо це ваш випадок, а малюнок замалий, спробуйте збільшити масштаб вікна в налаштуваннях проекту Display > Window > Stretch > Scale, щоб налаштувати проект на вищу роздільну здатність (масштаб 2 або 4, як правило, добре працює).

Малювання спеціальної багатокутної форми

Хоча існує спеціальний вузол для малювання власних багатокутників ( 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 ]
]

Цей формат, хоч і компактний, не є тим, який 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);

Щоб нарешті намалювати нашу першу фігуру, ми використаємо метод 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 ])

Під час запуску ви повинні побачити щось на зразок цього:

../../_images/draw_godot_logo_polygon.webp

Зверніть увагу, що нижня частина логотипу виглядає сегментованою – це тому, що для визначення цієї частини було використано низьку кількість точок. Щоб імітувати плавну криву, ми могли б додати більше точок до нашого масиву або, можливо, використати математичну функцію для інтерполяції кривої та створення плавної форми з коду (див. приклад 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 ]
]

Ми завантажимо ці координати в змінну та визначимо додаткову змінну з настроюваною товщиною лінії:

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);

І, нарешті, ми використаємо метод 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)

Ви маєте отримати наступний результат:

../../_images/draw_godot_logo_polyline.webp

На відміну від 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)

Під час його виконання ви повинні мати щось на зразок цього:

../../_images/draw_godot_logo_circle.webp

Для часткових незаповнених дуг (частин форми кола між певними довільними кутами) ви можете використовувати метод 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)

Тепер ви зможете побачити на екрані таку форму:

../../_images/draw_godot_logo_line.webp

Зауважте, що якщо кілька незв’язаних ліній малюватимуться одночасно, ви можете отримати додаткову продуктивність, намалювавши їх усі за один виклик за допомогою методу draw_multiline.

Малювання тексту

Хоча використання вузла Label є найпоширенішим способом додавання тексту до вашої програми, функція низького рівня _draw включає функції для додавання тексту до вашого власного малюнка Node. Ми використаємо його, щоб додати назву «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)

Тут ми спочатку завантажуємо в змінну defaultFont налаштований шрифт теми за замовчуванням (замість нього можна встановити спеціальний), а потім передаємо наступні параметри: шрифт, положення, текст, горизонтальне вирівнювання, ширину та розмір шрифту.

Ви повинні побачити на екрані наступне:

../../_images/draw_godot_logo_text.webp

Додаткові параметри, а також інші методи, пов’язані з текстом і символами, можна знайти в довідці про клас CanvasItem.

Показувати малюнок під час редагування

Хоча код наразі може намалювати логотип у запущеному вікні, він не відображатиметься у 2D-виді редактора. У деяких випадках ви також бажаєте показати свій власний Node2D або елемент керування в редакторі, щоб розташувати та масштабувати його належним чином, як це робить більшість інших вузлів.

Щоб показати логотип безпосередньо в редакторі (не запускаючи його), ви можете використати анотацію @tool, щоб надіслати запит на те, щоб настроюваний малюнок вузла також з’являвся під час редагування, ось так:

@tool
extends 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

Проблема з наведеним вище кодом полягає в тому, що оскільки ми створили точки приблизно на прямокутнику, починаючи з верхнього лівого кута, координати (0, 0) і простягаючись праворуч і вниз, ми бачимо, що обертання виконано за допомогою верхнього лівого кута як опори. Зміна трансформації позиції на вузлі не допоможе нам тут, оскільки спочатку застосовується трансформація обертання.

Хоча ми могли б переписати всі координати точок, щоб вони були зосереджені навколо (0, 0), включно з від’ємними координатами, це було б дуже складно.

Одним із можливих способів обійти це є використання методу нижнього рівня draw_set_transform для усунення цієї проблеми, перекладаючи всі точки у власному просторі CanvasItem, а потім переміщуючи його назад у вихідне місце за допомогою звичайного вузла перетворення, або в редакторі, або в коді, як це:

func _ready():
    rotation = 0
    position = Vector2(60, 60)
    ...

func _draw():
    draw_set_transform(Vector2(-60, -60))
    ...

Це результат обертання навколо опорної точки на (60, 60):

../../_images/draw_godot_rotation.webp

Якщо те, що ми хочемо анімувати, було властивістю всередині виклику _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)
    ...

При запуску це виглядатиме приблизно так:

../../_images/draw_godot_mouth_animation.webp

Зауважте, що _mouth_width — це властивість, визначена користувачем, як і будь-яка інша, і її або будь-яку іншу властивість, яка використовується як аргумент малювання, можна анімувати за допомогою більш стандартних і високорівневих методів, таких як вузол Tween або AnimationPlayer. Єдина відмінність полягає в тому, що виклик queue_redraw() необхідний для застосування цих змін, щоб вони відображалися на екрані.

Приклад 2: малювання динамічної лінії

Попередній приклад був корисним, щоб навчитися малювати та змінювати вузли за допомогою власних форм і анімації. Це може мати деякі переваги, наприклад використання точних координат і векторів для малювання замість растрових зображень, що означає, що вони добре масштабуються під час трансформації на екрані. У деяких випадках подібних результатів можна досягти, створивши функціональні можливості вищого рівня з такими вузлами, як sprites або AnimatedSprites, завантажуючи ресурси SVG (які також є зображеннями, визначеними за допомогою векторів) і AnimationPlayer вузол.

В інших випадках це буде неможливо, оскільки ми не будемо знати, яким буде кінцеве графічне представлення до запуску коду. Тут ми побачимо, як намалювати динамічну лінію, координати якої невідомі заздалегідь і на які впливає введення користувача.

Проведення прямої між 2 точками

Припустімо, що ми хочемо намалювати пряму лінію між 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)

У цьому прикладі ми отримуємо положення миші у вікні перегляду за замовчуванням у кожному кадрі за допомогою методу get_mouse_position. Якщо позиція змінилася з моменту останнього запиту на розіграш (невелика оптимізація, щоб уникнути перемальовування кожного кадру), ми заплануємо перемальовування. Наш метод _draw() має лише один рядок: запит на малювання зеленої лінії шириною 10 пікселів між верхнім лівим кутом і отриманою позицією.

Ширину, колір і положення початкової точки можна налаштувати за допомогою відповідних властивостей.

Під час запуску це має виглядати так:

../../_images/draw_line_between_2_points.webp

Проведення дуги між 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
../../_images/draw_dynamic_exported_properties.webp

Щоб намалювати дугу, ми можемо використати метод 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)

Центр півкола буде серединою між обома точками. Радіус буде дорівнювати половині відстані між обома точками. Початковий і кінцевий кути будуть кутами вектора від точки1 до точки2 і навпаки. Зауважте, що нам довелося нормалізувати end_angle у додатних значеннях, оскільки якщо end_angle менший за start_angle, дуга буде намальована проти годинникової стрілки, чого ми не хочемо в цьому випадку ( дуга буде перевернута).

У результаті має вийти щось на кшталт цього з дугою вниз і між точками:

../../_images/draw_arc_between_2_points.webp

Не соромтеся грати з параметрами в інспекторі, щоб отримати різні результати: змініть колір, ширину, згладжування та збільште кількість сегментів, щоб збільшити плавність кривої, ціною додаткової продуктивності.