Dibujos personalizados en 2D

Introducción

Godot posee nodos para dibujar sprites, polígonos, partículas, etc., para la mayoría de los casos eso es suficiente, pero no siempre. Antes de desesperar al ver que no existe un nodo para dibujar algo específico, es bueno saber que es posible hacer que cualquier nodo 2D (ya sea uno basado en Control o Node2D ) puede aceptar comandos de dibujo personalizados. Esto es muy fácil de hacer.

Dibujar manualmente en un nodo 2D es realmente útil, aquí hay algunos casos de uso:

  • Dibujar formas o lógica que los nodos existentes no pueden hacer, como una imagen con estelas o un polígono animado especial.

  • Dibujar un gran número de objetos simples, como una cuadricula o un tablero para un juego 2D. El dibujo personalizado evita la sobrecarga del uso de nodos lo que lo hace menos intensivo en el uso de memoria y potencialmente más rápido.

  • Hacer un control de UI personalizado. Hay muchos nodos Control disponibles, Sin embargo, cuando tienes necesidades poco comunes, es probable que necesites un control personalizado.

Dibujando

Agrega un script a cualquier nodo derivado de CanvasItem, como Control o Node2D. Luego sobreescribe la función _draw()<class_CanvasItem_private_method__draw>.

extends Node2D

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

Los comandos están descriptos en la referencia de la clase CanvasItem. Hay varios de ellos y los veremos algunos en los siguientes ejemplos.

Actualizando

La función _draw solo es llamada una vez, y luego, los comandos de dibujo se almacenan en caché y se recuerdan, de modo que las llamadas adicionales son innecesarias.

Si es necesario volver a dibujar porque cambió una variable o algo más, llame a CanvasItem.queue_redraw en ese mismo nodo y se realizará una nueva llamada _draw().

Aquí hay un ejemplo un poco más complejo, donde tenemos una variable de textura que se puede modificar en cualquier momento, y usando un setter, fuerza un redibujado de la textura cuando se modifica:

extends Node2D

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

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

Para verlo en acción, puede configurar la textura para que sea el ícono de Godot en el editor arrastrando y soltando el icon.svg predeterminado desde la pestaña FileSystem a la propiedad Textura en la pestalla Inspector. Al cambiar el valor de la propiedad Textura mientras se ejecuta el script anterior, la textura también cambiará automáticamente.

En algunos casos, podríamos necesitar redibujar en cada frame. Para esto, llama queue_redraw desde el método _process tal que así:

extends Node2D

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

func _process(_delta):
    queue_redraw()

Coordenadas y ancho de línea

La API de dibujo utiliza el sistema de coordenadas de CanvasItem, no necesariamente coordenadas de píxeles. Esto significa que _draw() usa el espacio de coordenadas creado después de aplicar la transformación de CanvasItem. Además, se puede aplicar una transformación personalizada encima usando draw_set_transform o draw_set_transform_matrix.

Al usar draw_line, debes considerar el ancho de la línea. Cuando se utiliza un ancho de tamaño impar, la posición de los puntos inicial y final se debe cambiar en 0.5 para mantener la línea centrada, como se muestra a continuación.

../../_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 mismo se aplica al método 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)

Dibujo antialiasing

Godot ofrece parámetros de método en draw_line para habilitar el antialiasing, pero no todos los métodos personalizados de dibujo ofrecen este parámetro suavizado.

Para los métodos de dibujo personalizados que no proporcionan un parámetro antialiased, puede habilitar MSAA 2D, lo que afecta el renderizado en todo el viewport. Esto proporciona un antialiasing de alta calidad, pero con un mayor costo de rendimiento y solo en elementos específicos. Consulta Antialiasing 2D: para obtener más información.

Aquí hay una comparación de una línea de ancho mínimo (width=-1) dibujada con antialiased=false, antialiased=true y antialiased=false con MSAA 2D 2x , 4x y 8x habilitados.

../../_images/draw_antialiasing_options.webp

Herramientas

Dibujar tus propios nodos también puede ser conveniente mientras los ejecutas en el editor. Esto puede usarse como una preview o visualización de alguna funcionalidad o comportamiento del mismo.

Para hacer esto, puede usar la anotación de herramienta tanto en GDScript como en C#. Consulta el siguiente ejemplo y Ejecutando código en el editor para obtener más información.

Ejemplo 1: dibujando una figura personalizada

Ahora usaremos la funcionalidad de dibujo personalizado de Godot Engine para dibujar algo para lo que Godot no ofrece ninguna función. Recrearemos el logo de Godot pero con código, utilizando únicamente las funciones de dibujo.

Tendrá que programar una función para realizar esto y dibujarla Ud. mismo.

Nota

The following instructions use a fixed set of coordinates that could be too small for high resolution screens (larger than 1080p). If that is your case, and the drawing is too small consider increasing your window scale in the project setting Display > Window > Stretch > Scale to adjust the project to a higher resolution (a 2 or 4 scale tends to work well).

Dibujando una forma de un polígono personalizada

Si bien hay un nodo dedicado para dibujar polígonos personalizados ( Polygon2D), en este caso usaremos exclusivamente funciones de dibujo de nivel inferior para combinarlas en el mismo nodo y poder crear formas más complejas más adelante. .

Primero, definiremos un conjunto de puntos (o coordenadas X e Y) que formarán la base de nuestra 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 ]
]

Este formato, aunque compacto, no es el que entiende Godot para dibujar un polígono. En un escenario diferente podríamos tener que cargar estas coordenadas desde un archivo o calcular las posiciones mientras la aplicación se está ejecutando, por lo que puede ser necesaria alguna transformación.

Para transformar estas coordenadas al formato correcto, crearemos un nuevo método float_array_to_Vector2Array(). Luego anularemos la función _ready(), que Godot llamará solo una vez -al inicio de la ejecución- para cargar esas coordenadas en una variable:

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

Para finalmente dibujar nuestra primera forma, usaremos el método draw_polygon y pasaremos los puntos (como una matriz de coordenadas Vector2) y su color, de esta manera:

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

Al ejecutarse deberías ver algo como esto:

../../_images/draw_godot_logo_polygon.webp

Ten en cuenta que la parte inferior del logotipo parece segmentada; esto se debe a que se utilizó una cantidad baja de puntos para definir esa parte. Para simular una curva suave, podríamos agregar más puntos a nuestra matriz, o tal vez usar una función matemática para interpolar una curva y crear una forma suave a partir del código (ver ejemplo 2).

Los polígonos siempre conectarán su último punto definido con el primero para tener una forma cerrada.

Dibujar líneas conectadas

Dibujar una secuencia de líneas conectadas que no se cierran para formar un polígono es muy similar al método anterior. Usaremos un conjunto de líneas conectadas para dibujar la boca del logo de Godot.

Primero, definiremos la lista de coordenadas que forman la forma de la boca, de esta manera:

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

Cargaremos estas coordenadas en una variable y definiremos una variable adicional con el grosor de línea configurable:

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

Y finalmente usaremos el método draw_polyline para dibujar la línea, de la siguiente manera:

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)

Deberías obtener la siguiente salida:

../../_images/draw_godot_logo_polyline.webp

Unlike draw_polygon(), polylines can only have a single unique color for all its points (the second argument). This method has 2 additional arguments: the width of the line (which is as small as possible by default) and enabling or disabling the antialiasing (it is disabled by default).

El orden de las llamadas _draw es importante; al igual que con las posiciones de los Nodos en la jerarquía del árbol, las diferentes formas se dibujarán de arriba a abajo, lo que dará como resultado que las últimas formas oculten las anteriores si se superponen. En este caso queremos que la boca esté dibujada sobre la cabeza, así que la ponemos después.

Observe cómo podemos definir los colores de diferentes formas, ya sea con un código hexadecimal o un nombre de color predefinido. Consulta la clase Color para conocer otras constantes y formas de definir colores.

Dibujando círculos

Para crear los ojos, vamos a añadir 4 llamadas adicionales para dibujar las formas de los ojos, en diferentes tamaños, colores y posiciones.

Para dibujar un círculo, colóquelo según su centro usando el método draw_circle. El primer parámetro es un Vector2 con las coordenadas de su centro, el segundo es su radio y el tercero es su color:

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)

Al ejecutarse deberías ver algo como esto:

../../_images/draw_godot_logo_circle.webp

Para arcos parciales y sin relleno (porciones de una forma de círculo entre ciertos ángulos arbitrarios), puede usar el método draw_arc.

Dibujar líneas

Para dibujar la última forma (la nariz) usaremos una línea para aproximarla.

draw_line se puede usar para dibujar un solo segmento proporcionando sus coordenadas inicial y final como argumentos, como éste:

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)

Ahora deberías ser capaz de ver la siguiente figura en pantalla:

../../_images/draw_godot_logo_line.webp

Ten en cuenta que si se van a dibujar varias líneas no conectadas al mismo tiempo, puede obtener un mejor rendimiento dibujándolas todas en una sola llamada, utilizando el método draw_multiline.

Dibujando texto

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.

Usaremos el método draw_string para hacerlo, de este modo:

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)

Aquí primero cargamos en la variable defaultFont la fuente del tema configurada de manera predeterminada (en su lugar se puede configurar una personalizada) y luego pasamos los siguientes parámetros: fuente, posición, texto, alineación horizontal, ancho y tamaño de fuente.

Deberías ver lo siguiente en tu pantalla:

../../_images/draw_godot_logo_text.webp

Se pueden encontrar parámetros adicionales, así como otros métodos relacionados con texto y caracteres, en la referencia de la clase CanvasItem.

Mostrar el dibujo durante la edición

Si bien hasta ahora el código puede dibujar el logotipo en una ventana en ejecución, no aparecerá en la vista 2D del editor. En ciertos casos, también le gustaría mostrar su Node2D personalizado o control en el editor, para posicionarlo y escalarlo adecuadamente, como lo hacen la mayoría de los otros nodos.

Para mostrar el logotipo directamente en el editor (sin ejecutarlo), puede usar la anotación @tool para solicitar que el dibujo personalizado del nodo también aparezca durante la edición, así:

@tool
extends Node2D

Necesitará guardar su escena, reconstruir tu proyecto (solo para C#) y recargar la escena actual manualmente. Eso se hace con la opción de menú Escena > Recargar Escena Guardada para actualizar el nodo actual en la vista 2D la primera vez que agrega o elimina la anotación @tool.

Animación

Si quisiéramos hacer que la forma personalizada cambie en tiempo de ejecución, podríamos modificar los métodos llamados o sus argumentos en tiempo de ejecución, o aplicar una transformación.

Por ejemplo, si queremos que la forma personalizada que acabamos de diseñar gire, podríamos agregar la siguiente variable y código a los métodos _ready y _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

El problema con el código anterior es que debido a que hemos creado los puntos aproximadamente en un rectángulo comenzando desde la esquina superior izquierda, la coordenada (0, 0), y extendiéndose hacia la derecha y hacia abajo, vemos que la rotación se hace usando la esquina superior izquierda como pivote. Un cambio de transformación de posición en el nodo no nos ayudará aquí, ya que la transformación de rotación se aplica primero.

Si bien podríamos reescribir todas las coordenadas de los puntos para que se centren alrededor de (0, 0), incluidas las coordenadas negativas, eso requeriría mucho trabajo.

Una forma posible de solucionar esto es utilizar el método de bajo nivel draw_set_transform para solucionar este problema, traduciendo todos los puntos en el propio espacio del CanvasItem. Y luego moviéndolo de nuevo a su lugar original con un nodo de transformación normal, ya sea en el editor o en código. Sería así:

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

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

Este es el resultado de girar alrededor de un pivote ahora en (60, 60):

../../_images/draw_godot_rotation.webp

Si lo que queríamos animar era una propiedad dentro de la llamada _draw(), debemos recordar llamar a queue_redraw() para forzar una actualización, ya que de lo contrario no se actualizaría en pantalla.

Por ejemplo, así es como podemos hacer que parezca que el robot abre y cierra la boca, cambiando el ancho de la línea de su boca y siga una curva sinusoidal (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)
    ...

Debería verse similar a esto al ejecutarse:

../../_images/draw_godot_mouth_animation.webp

Please note that _mouth_width is a user defined property like any other and it or any other used as a drawing argument can be animated using more standard and high-level methods such as a Tween or an AnimationPlayer Node. The only difference is that a queue_redraw() call is needed to apply those changes so they get shown on screen.

Ejemplo 2: dibujando una linea dinámica

El ejemplo anterior fue útil para aprender a dibujar y modificar nodos con formas y animaciones personalizadas. Esto podría tener algunas ventajas, como utilizar coordenadas y vectores exactos para dibujar, en lugar de mapas de bits, lo que significa que se escalarán bien cuando se transformen en la pantalla. En algunos casos, se podrían lograr resultados similares componiendo funcionalidades de nivel superior con nodos como sprites o AnimatedSprites cargando recursos SVG (que también son imágenes definidas con vectores) y el nodo AnimationPlayer.

En otros casos eso no será posible porque no sabremos cuál será la representación gráfica resultante antes de ejecutar el código. Aquí veremos cómo dibujar una línea dinámica cuyas coordenadas no se conocen de antemano y se ven afectadas por la entrada del usuario.

Dibujar una línea recta entre 2 puntos

Supongamos que queremos dibujar una línea recta entre 2 puntos, el primero estará fijo en la esquina superior izquierda (0, 0) y el segundo estará definido por la posición del cursor en la pantalla.

Podríamos trazar una línea dinámica entre esos 2 puntos así:

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)

En este ejemplo obtenemos la posición del puntero del ratón en la ventana gráfica predeterminada en cada frame con el método get_mouse_position. Si la posición ha cambiado desde la última solicitud de dibujo (una pequeña optimización para evitar volver a dibujar en cada frame), programaremos un nuevo redibujo. Nuestro método _draw() solo tiene una línea: solicitando el dibujo de una línea verde de 10 píxeles de ancho entre la esquina superior izquierda y la posición obtenida.

El ancho, color y posición del punto inicial pueden ser configurados con las propiedades correspondientes.

Debería verse como esto al ejecutarse:

../../_images/draw_line_between_2_points.webp

Dibujar un arco entre 2 puntos

El ejemplo anterior funciona, pero es posible que queramos unir esos 2 puntos con una forma o función diferente, que no sea una línea recta.

Intentemos ahora crear un arco (una porción de circunferencia) entre ambos puntos.

Exportar el punto de inicio de la línea, los segmentos, el ancho, el color y el suavizado nos permitirá modificar esas propiedades muy fácilmente directamente desde el panel del inspector del 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

Para dibujar el arco, podemos usar el método draw_arc. Hay muchos arcos que pasan por 2 puntos, por eso elegiremos para este ejemplo el semicírculo que tiene su centro en el punto medio entre los 2 puntos iniciales.

Calcular este arco será más complejo que en el caso de la recta:

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)

El centro del semicírculo será el punto medio entre ambos puntos. El radio será la mitad de la distancia entre ambos puntos. Los ángulos inicial y final serán los ángulos del vector del punto1 al punto2 y viceversa. Tenga en cuenta que tuvimos que normalizar end_angle en valores positivos porque si end_angle es menor que start_angle, el arco se dibujará en el sentido contrario a las agujas del reloj, lo cual no queremos en este caso (el arco estaría al revés).

El resultado debería ser algo como esto, con el arco bajando y entre los puntos:

../../_images/draw_arc_between_2_points.webp

Siéntete libre de jugar con los parámetros del inspector para obtener diferentes resultados: cambia el color, el ancho, el antialiasing y aumenta el número de segmentos para aumentar la suavidad de la curva, a costa de un rendimiento adicional.