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.

Disegno personalizzato in 2D

Introduzione

Godot include nodi per disegnare sprite, poligoni, particelle, testo e molti altri bisogni comuni nello sviluppo di giochi. Tuttavia, se hai bisogno di qualcosa di specifico non coperto dai nodi standard, puoi fare in modo che qualsiasi nodo 2D (ad esempio, basato su Control o Node2D) disegni sullo schermo utilizzando comandi personalizzati.

Il disegno personalizzato in un nodo 2D è davvero utile. Ecco alcuni casi d'uso:

  • Disegnare forme o logiche che i nodi esistenti non possono fare, come un'immagine con scie o un poligono animato speciale.

  • Disegnare un gran numero di oggetti semplici, come una griglia o una scacchiera per un gioco 2D. Il disegno personalizzato evita di utilizzare un gran numero di nodi, riducendo potenzialmente il consumo di memoria e migliorando le prestazioni.

  • Creare un controllo personalizzato per la propria interfaccia utente. Sono disponibili molti controlli, ma in caso di esigenze particolari, è probabile che sia necessario un controllo personalizzato.

Disegnare

Aggiungi uno script a qualsiasi nodo derivato da CanvasItem, come Control o Node2D. Poi sovrascrivi la funzione _draw().

extends Node2D

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

I comandi di disegno sono descritti nel riferimento della classe CanvasItem. Ce ne sono molti e ne vedremo alcuni negli esempi seguenti.

Aggiornamento

La funzione _draw viene chiamata una sola volta, dopodiché i comandi di disegno vengono memorizzati nella cache, rendendo inutili ulteriori chiamate.

Se è necessario ridisegnare perché una variabile o qualcos'altro è cambiato, chiama CanvasItem.queue_redraw nello stesso nodo e verrà eseguita una nuova chiamata a _draw().

Ecco un esempio un po' più complesso, in cui abbiamo una variabile di texture che può essere modificata in qualsiasi momento e, utilizzando un setter, forziamo un ridisegno della texture quando viene modificata:

extends Node2D

@export var texture : Texture2D:
    set(value):
        texture = value
        queue_redraw()

func _draw():
    draw_texture(texture, Vector2())

Per vederlo in azione, puoi impostare la texture così che sia icona di Godot nell'editor, trascinando e rilasciando il file icon.svg predefinito dalla scheda FileSystem alla proprietà Texture nella scheda Inspector. Quando modifichi il valore della proprietà Texture mentre lo script precedente è in esecuzione, anche la texture cambierà automaticamente.

In alcuni casi, potrebbe essere necessario ridisegnare ogni frame. Per farlo, chiama queue_redraw dal metodo _process, in questo modo:

extends Node2D

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

func _process(_delta):
    queue_redraw()

Coordinates and line width alignment

L'API di disegno utilizza il sistema di coordinate del CanvasItem, non necessariamente le coordinate in pixel. Significa che _draw() utilizza lo spazio di coordinate creato dopo aver applicato la trasformazione del CanvasItem. Inoltre, è possibile applicare una trasformazione personalizzata in più, usando draw_set_transform o draw_set_transform_matrix.

Usando draw_line, è necessario considerare lo spessore della linea. Con uno spessore dispari, la posizione dei punti iniziale e finale deve essere spostata di 0.5 per mantenere la linea centrata, come mostrato di seguito.

../../_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)

Lo stesso vale per il metodo draw_rect con 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)

Disegno con antialiasing

Godot offre parametri di metodo in draw_line per abilitare l'antialiasing, ma non tutti i metodi di disegno personalizzato offrono questo parametro antialiased.

Per i metodi di disegno personalizzato che non forniscono un parametro antialiased, è possibile abilitare invece l'MSAA 2D, che influisce sul rendering nell'intera viewport. Ciò fornisce un antialiasing di alta qualità, ma a un impatto maggiore sulle prestazioni e solo su elementi specifici. Consulta Antialiasing 2D per ulteriori informazioni.

Ecco un confronto tra una linea di larghezza minima (width=-1) disegnata con antialiased=false, antialiased=true e antialiased=false con l'MSAA 2D 2x, 4x e 8x abilitato.

../../_images/draw_antialiasing_options.webp

Strumenti

Disegnare i propri nodi potrebbe anche essere comodo mentre li si esegue nell'editor. Questo può servire da anteprima o visualizzazione di certe funzionalità o comportamenti.

Per farlo, è possibile usare l'annotazione tool su entrambi GDScript e C#. Consulta l'esempio seguente e Eseguire codice nell'editor per ulteriori informazioni.

Esempio 1: disegnare una forma personalizzata

Ora sfrutteremo le funzionalità di disegno personalizzato del Godot Engine per disegnare qualcosa per cui Godot non fornisce funzioni integrate. Ricreeremo il logo di Godot, ma solo tramite codice, usando solamente le funzioni di disegno.

Dovrai programmare una funzione per farlo e disegnarlo tu stesso.

Nota

Le seguenti istruzioni utilizzano una serie fissa di coordinate che potrebbero apparire troppo piccole per gli schermi ad alta risoluzione (superiori a 1080p). In tal caso, se il disegno appare troppo piccolo, si consiglia di aumentare la scala della finestra nell' impostazioni del progetto Display > Finestra > Stira > Scala per adattare il progetto a una risoluzione più alta (una scala di 2 o 4 in genere funziona bene).

Disegnare una forma poligonale personalizzata

Sebbene esista un nodo dedicato per disegnare poligoni personalizzati ( Polygon2D), in questo caso useremo esclusivamente funzioni di disegno di livello più basso per combinarle nello stesso nodo e poter creare forme più complesse in seguito.

Innanzitutto, definiremo una serie di punti (coordinate X e Y) che formeranno la base della nostra forma:

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 ]
]

Questo formato, sebbene compatto, non è quello che Godot riconosce per disegnare un poligono. In uno scenario diverso, potremmo dover caricare queste coordinate da un file o calcolare le posizioni mentre l'applicazione è in esecuzione, quindi qualche trasformazione potrebbe essere necessaria.

Per trasformare queste coordinate nel formato corretto, creeremo un nuovo metodo float_array_to_Vector2Array(). Poi sovrascriveremo la funzione _ready(), che Godot chiamerà una sola volta, all'inizio dell'esecuzione, per caricare queste coordinate in una variabile:

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

Per disegnare finalmente la nostra prima forma, useremo il metodo draw_polygon e passeremo i punti (come un array di coordinate Vector2) e il suo colore, in questo modo:

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

Durante l'esecuzione, dovresti vedere qualcosa di simile a questo:

../../_images/draw_godot_logo_polygon.webp

Nota che la parte inferiore del logo appare segmentata: ciò è dovuto al numero basso di punti utilizzati per definire questa parte. Per simulare una curva liscia, potremmo aggiungere più punti al nostro array, o magari usare una funzione matematica per interpolare una curva e creare una forma liscia tramite codice (vedi esempio 2).

I poligoni collegheranno sempre il loro ultimo punto definito al primo per formare una figura chiusa.

Disegnare linee collegate

Disegnare una sequenza di linee collegate, che non si chiudono per formare un poligono, è molto simile al metodo precedente. Useremo una serie di linee collegate per disegnare la bocca del logo di Godot.

Innanzitutto, definiremo la lista di coordinate che formano la forma della bocca, così:

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 ]
]

Caricheremo queste coordinate in una variabile e definiremo un'ulteriore variabile contenente lo spessore di linea configurabile:

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

E infine useremo il metodo draw_polyline per disegnare la linea, in questo modo:

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)

Dovresti ottenere il seguente risultato:

../../_images/draw_godot_logo_polyline.webp

A differenza di draw_polygon(), le polilinee possono avere solo un colore unico per tutti i loro punti (il secondo argomento). Questo metodo ha 2 argomenti aggiuntivi: lo spessore della linea (che è il più piccolo possibile normalmente) e l'attivazione o la disattivazione dell'antialiasing (disabilitato normalmente).

L'ordine delle chiamate in _draw è importante: come per le posizioni dei nodi nella gerarchia dell'albero, le diverse forme verranno disegnate dall'alto verso il basso, con il risultato che le ultime forme nasconderanno quelle precedenti se si sovrappongono. In questo caso vogliamo che la bocca venga disegnata sopra la testa, quindi la mettiamo per ultima.

Nota come possiamo definire i colori in modi diversi, sia con un codice esadecimale sia con un nome di colore predefinito. Consulta la classe Color per altre costanti e modi di definire i colori.

Disegno di cerchi

Per creare gli occhi, aggiungeremo 4 chiamate in più per disegnare le forme degli occhi, in diverse dimensioni, colori e posizioni.

Per disegnare un cerchio, lo posizionamo in base al suo centro usando il metodo draw_circle. Il primo parametro è un Vector2 con le coordinate del suo centro, il secondo è il suo raggio e il terzo è il suo colore:

func _draw():
    var white : Color = Color.WHITE
    var godot_blue : Color = Color("478cbf")
    var gray : Color = Color("414042")

    draw_polygon(head, [ godot_blue ])
    draw_polyline(mouth, white, _mouth_width)

    # Four circles for the 2 eyes: 2 white, 2 gray.
    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, gray)
    draw_circle(Vector2(84.626, 66.008), 6.246, gray)

Durante l'esecuzione, dovresti avere qualcosa di simile a questo:

../../_images/draw_godot_logo_circle.webp

Per gli archi parziali e non riempiti (porzioni di una forma di cerchio tra certi angoli arbitrari), è possibile usare il metodo draw_arc.

Disegno di linee

Per disegnare la forma finale (il naso) useremo una linea per approssimarla.

draw_line serve per disegnare un singolo segmento fornendo le sue coordinate iniziali e finali come argomenti, in questo modo:

func _draw():
    var white : Color = Color.WHITE
    var godot_blue : Color = Color("478cbf")
    var gray : 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, gray)
    draw_circle(Vector2(84.626, 66.008), 6.246, gray)

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

Ora dovreste essere in grado di vedere la seguente forma sullo schermo:

../../_images/draw_godot_logo_line.webp

Tieni in mente che se hai bisogno di disegnare più linee non collegate allo stesso tempo, potresti ottenere prestazioni migliori disegnandole tutte in un'unica chiamata, usando il metodo draw_multiline.

Disegno di testo

While using the Label Node is the most common way to add text to your application, the low-level _draw function includes functionality to add text to your custom Node drawing. We will use it to add the name "GODOT" under the robot head.

Useremo il metodo draw_string per farlo, in questo modo:

var default_font : Font = ThemeDB.fallback_font;

func _draw():
    var white : Color = Color.WHITE
    var godot_blue : Color = Color("478cbf")
    var gray : 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, gray)
    draw_circle(Vector2(84.626, 66.008), 6.246, gray)
    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)

Qui prima carichiamo nella variabile defaultFont il font del tema predefinito configurato (è possibile impostarne uno personalizzato al suo posto) e poi passiamo i seguenti parametri: font, posizione, testo, allineamento orizzontale, larghezza e dimensione del font.

Dovresti vedere sul tuo schermo quanto segue:

../../_images/draw_godot_logo_text.webp

Informazioni su ulteriori parametri e altri metodi relativi a testo e caratteri sono disponibili nel riferimento della classe CanvasItem.

Mostra il disegno durante la modifica

Sebbene il codice ora sia in grado di disegnare il logo su una finestra in esecuzione, questo non apparirà nella vista 2D dell'editor. In alcuni casi, potrebbe essere utile mostrare anche il proprio Node2D o controllo personalizzato nell'editor, per posizionarlo e ridimensionarlo appropriatamente, come fa la gran parte degli altri nodi.

Per mostrare il logo direttamente nell'editor (senza eseguirlo), è possibile utilizzare l'annotazione @tool per richiedere che il disegno personalizzato del nodo appaia anche durante la modifica, in questo modo:

@tool
extends Node2D

Dovrai salvare la scena, ricompilare il progetto (solo per C#) e ricaricare manualmente la scena attuale tramite l'opzione di menu Scena > Ricarica la scena salvata per aggiornare il nodo attuale nella vista 2D la prima volta che aggiungi o rimuovi l'annotazione @tool.

Animazione

Se volessimo modificare la forma personalizzata in fase di esecuzione, potremmo modificare i metodi chiamati o i suoi argomenti al momento dell'esecuzione, oppure applicare una trasformazione.

Ad esempio, se vogliamo che la forma personalizzata che abbiamo progettato ruoti, possiamo aggiungere la seguente variabile e il seguente codice ai metodi _ready e _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

Il problema con il codice sopra riportato è che, poiché abbiamo creato i punti approssimativamente su un rettangolo partendo dall'angolo superiore sinistro, la coordinata (0, 0) ed estendendoci verso destra e verso il basso, notiamo che la rotazione viene effettuata utilizzando l'angolo superiore sinistro come perno. Una modifica della trasformazione di posizione sul nodo non ci aiuterà qui, poiché la trasformazione di rotazione viene applicata per prima.

Sebbene potessimo riscrivere tutte le coordinate dei punti in modo che siano centrate attorno a (0, 0), comprese le coordinate negative, ciò richiederebbe molto lavoro.

Un possibile modo per aggirare questo problema è utilizzare il metodo di basso livello draw_set_transform per risolvere il problema, traslando tutti i punti nello spazio del CanvasItem stesso e poi riportandolo nella sua posizione originale con una regolare trasformazione di nodo, o nell'editor o nel codice, in questo modo:

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

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

Questo è il risultato, ruotando attorno a un perno ora in (60, 60):

../../_images/draw_godot_rotation.webp

Se ciò che vogliamo animare è una proprietà all'interno della chiamata _draw(), dobbiamo ricordarci di chiamare queue_redraw() per forzare un aggiornamento, altrimenti non verrebbe aggiornata sullo schermo.

Ad esempio, ecco come possiamo far aprire e chiudere la bocca del robot, cambiando lo spessore della linea della sua bocca in modo che segua una curva sinusoidale (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)
    ...

All'esecuzione, dovrebbe avere più o meno questo aspetto:

../../_images/draw_godot_mouth_animation.webp

Si prega di notare che _mouth_width è una proprietà definita dall'utente come qualsiasi altra ed è possibile animare essa, o qualsiasi altra proprietà utilizzata come argomento di disegno, attraverso metodi più standard e di alto livello come un nodo Tween o un nodo AnimationPlayer. L'unica differenza è che è necessaria una chiamata a queue_redraw() per applicare tali modifiche e mostrarle sullo schermo.

Esempio 2: disegnare una linea dinamica

L'esempio precedente è stato utile per imparare a disegnare e modificare i nodi con forme e animazioni personalizzate. Questo può offrire alcuni vantaggi, come l'uso di coordinate e vettori esatti per il disegno, anziché bitmap, il che significa che si adatteranno bene allo schermo quando sono trasformati. In alcuni casi, si potrebbero ottenere risultati simili componendo funzionalità di livello più alto con nodi come sprite o AnimatedSprite, caricando risorse SVG (che sono anch'esse immagini definite con vettori) e il nodo AnimationPlayer.

In altri casi ciò non sarà possibile perché non sapremo quale sarà la rappresentazione grafica risultante prima di eseguire il codice. Qui vedremo come disegnare una linea dinamica le cui coordinate non sono note in anticipo e sono influenzate dall'input dell'utente.

Disegnare una linea retta tra 2 punti

Supponiamo di voler tracciare una linea retta tra due punti: il primo sarà fisso nell'angolo in alto a sinistra (0, 0) e il secondo sarà definito dalla posizione del cursore sullo schermo.

Potremmo disegnare una linea dinamica tra questi due punti in questo modo:

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)

In questo esempio otteniamo la posizione del mouse nella viewport predefinita ad ogni frame con il metodo get_mouse_position. Se la posizione è cambiata dall'ultima richiesta di disegno (una piccola ottimizzazione per evitare di ridisegnare ad ogni frame), pianificheremo un ridisegno. Il nostro metodo _draw() ha una sola riga: richiede il disegno di una linea verde, con uno spessore di 10 pixel, tra l'angolo in alto a sinistra e la posizione ottenuta.

La larghezza, il colore e la posizione del punto di partenza possono essere configurati con le proprietà corrispondenti.

Quando viene eseguito dovrebbe apparire così:

../../_images/draw_line_between_2_points.webp

Disegnare un arco tra 2 punti

L'esempio precedente funziona, ma potremmo voler unire quei due punti con una forma o una funzione diversa da una linea retta.

Proviamo ora a creare un arco (una porzione di circonferenza) tra i due punti.

Esportando il punto di partenza della linea, i segmenti, lo spessore, il colore e l'antialiasing, potremo modificare queste proprietà molto facilmente direttamente dal pannello Ispettore nell'editor:

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

Per disegnare l'arco, possiamo usare il metodo draw_arc. Esistono molti archi che passano per 2 punti, quindi per questo esempio sceglieremo il semicerchio che ha il centro nel punto medio tra i 2 punti iniziali.

Il calcolo di questo arco sarà più complesso rispetto al caso della linea:

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)

Il centro del semicerchio sarà il punto medio tra i due punti. Il raggio sarà la metà della distanza tra i due punti. Gli angoli iniziale e finale saranno gli angoli del vettore che va dal punto 1 al punto 2 e viceversa. Si noti che abbiamo dovuto normalizzare end_angle in valori positivi perché se end_angle è minore di start_angle, l'arco verrà disegnato in senso antiorario, cosa che non desideriamo in questo caso (l'arco risulterebbe capovolto).

Il risultato dovrebbe essere qualcosa del genere, con l'arco che scende e passa tra i punti:

../../_images/draw_arc_between_2_points.webp

Sentiti liberi di sperimentare con i parametri nell'ispettore per ottenere risultati diversi: cambia il colore, lo spessore, l'antialiasing e aumenta il numero di segmenti per migliorare la levigatezza della curva, a scapito delle prestazioni.