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.

Dibujos personalizados en 2D

Introducción

Godot has nodes to draw sprites, polygons, particles, and all sorts of stuff. For most cases, this is enough. If there's no node to draw something specific you need, you can make any 2D node (for example, Control or Node2D based) draw custom commands.

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.

  • Visualizaciones que no son tan compatibles con los nodos, como un tablero de tetris. (El ejemplo del tetris utiliza una función de dibujo personalizada para dibujar los bloques)

  • Dibujar un gran número de objetos simples. 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().

extends Node2D

func _draw():
    # Your draw commands here
    pass

Los comandos están descriptos en la referencia de la clase CanvasItem.

Actualizando

La función _draw() es llamada una vez y los comandos son guardados y recordados, así que no es necesario llamarla continuamente.

If re-drawing is required because a state or something else changed, call CanvasItem.queue_redraw() in that same node and a new _draw() call will happen.

Aquí hay un ejemplo más complejo. Una textura variable que se dibujará nuevamente si es modificada:

extends Node2D

export (Texture) var texture setget _set_texture

func _set_texture(value):
    # If the texture variable is modified externally,
    # this callback is called.
    texture = value  # Texture was changed.
    queue_redraw()  # Trigger a redraw of the node.

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

In some cases, it may be desired to draw every frame. For this, call queue_redraw() from the _process() callback, like this:

extends Node2D

func _draw():
    # Your draw commands here
    pass

func _process(delta):
    queue_redraw()

Coordenadas

The drawing API uses the CanvasItem's coordinate system, not necessarily pixel coordinates. Which means it uses the coordinate space created after applying the CanvasItem's transform. Additionally, you can apply a custom transform on top of it by using draw_set_transform or draw_set_transform_matrix.

When using draw_line, you should consider the width of the line. When using a width that is an odd size, the position should be shifted by 0.5 to keep the line centered as shown below.

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

The same applies to the draw_rect method with 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)

Un ejemplo: dibujando arcos de curva

Ahora usaremos la funcionalidad de dibujo personalizado de Godot Engine para dibujar algo para lo que Godot no ofrece ninguna función. Por ejemplo, Godot proporciona una función draw_circle() que dibuja un círculo entero. Sin embargo, ¿qué tal dibujar una porción de un círculo? Tendrás que crear tu mismo una función para realizarlo y dibujarlo.

Función arco

Un arco está definido por los parámetros de un círculo de soporte, es decir, posición central y radio. El arco en sí es definido por el ángulo en el que comienza y el ángulo en el que termina. Esos son los 4 parámetros que pasamos a nuestra función de dibujo. Podemos también agregar un valor de color para dibujar el arco en diferentes colores si o deseamos.

Básicamente, dibujar una figura requiere descomponerla en un cierto número de puntos, vinculados uno al otro. Como puedes imaginar, mientras más puntos posea la figura, más suave que se verá, pero será más "pesada" en términos de costo de procesamiento. En general, si la forma de un figura es muy grande (o en 3D, cercana a la cámara), requerirá más puntos para ser dibujada sin verse muy angulosa. Al contrario, si una figura es muy pequeña(o en 3D, lejos de la cámara), se pueden reducir los puntos para ahorrar costo de procesamiento. Esto es conocido Nivel de Detalle (LoD, por sus siglas en inglés). En nuestro ejemplo usaremos un número fijo de puntos, sin importar el radio.

func draw_circle_arc(center, radius, angle_from, angle_to, color):
    var nb_points = 32
    var points_arc = PackedVector2Array()

    for i in range(nb_points + 1):
        var angle_point = deg_to_rad(angle_from + i * (angle_to-angle_from) / nb_points - 90)
        points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)

    for index_point in range(nb_points):
        draw_line(points_arc[index_point], points_arc[index_point + 1], color)

Remember the number of points our shape has to be decomposed into? We fixed this number in the nb_points variable to a value of 32. Then, we initialize an empty PackedVector2Array, which is simply an array of Vector2s.

El siguiente paso consiste en calcular las posiciones reales de estos 32 puntos que componen un arco. Esto se hace en el primer bucle for: iteramos sobre el número de puntos para los que queremos calcular las posiciones, más uno para incluir el último punto. Primero determinamos el ángulo de cada punto, entre el ángulo inicial y el ángulo final.

The reason why each angle is decreased by 90° is that we will compute 2D positions out of each angle using trigonometry (you know, cosine and sine stuff...). However, cos() and sin() use radians, not degrees. The angle of 0° (0 radian) starts at 3 o'clock, although we want to start counting at 12 o'clock. So we decrease each angle by 90° in order to start counting from 12 o'clock.

The actual position of a point located on a circle at angle angle (in radians) is given by Vector2(cos(angle), sin(angle)). Since cos() and sin() return values between -1 and 1, the position is located on a circle of radius 1. To have this position on our support circle, which has a radius of radius, we simply need to multiply the position by radius. Finally, we need to position our support circle at the center position, which is performed by adding it to our Vector2 value. Finally, we insert the point in the PackedVector2Array which was previously defined.

Ahora necesitamos dibujar los puntos. Como puedes imaginar, no dibujaremos nuestros 32 puntos solamente, tendremos que dibujar todo lo que está entre ellos. Podríamos haber calculado los puntos nosotros mismos utilizando el método previo y dibujarlos uno a uno, pero esto es muy complicado e ineficiente (a menos que sea necesario). Así que simplemente dibujaremos líneas entre cada par de puntos. A menos que el radio de nuestro círculo de soporte sea muy grande, el largo de la línea entre cada par de puntos nunca será lo suficientemente larga para verlos. Si esto sucede, simplemente aumentamos el número de puntos.

Dibujar el arco en pantalla

Ahora tenemos una función que dibuja cosas en la pantalla: Es momento de llamar a la función _draw():

func _draw():
    var center = Vector2(200, 200)
    var radius = 80
    var angle_from = 75
    var angle_to = 195
    var color = Color(1.0, 0.0, 0.0)
    draw_circle_arc(center, radius, angle_from, angle_to, color)

Resultado:

../../_images/result_drawarc.png

Función de polígono arco

Podemos llevar esto un paso más allá y no sólo escribir una función que dibuje la parte plana del disco definida por el arco, sino también su forma. El método es exactamente el mismo que el anterior, excepto que dibujamos un polígono en lugar de líneas:

func draw_circle_arc_poly(center, radius, angle_from, angle_to, color):
    var nb_points = 32
    var points_arc = PackedVector2Array()
    points_arc.push_back(center)
    var colors = PackedColorArray([color])

    for i in range(nb_points + 1):
        var angle_point = deg_to_rad(angle_from + i * (angle_to - angle_from) / nb_points - 90)
        points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)
    draw_polygon(points_arc, colors)
../../_images/result_drawarc_poly.png

Dibujos personalizados dinámicos

Muy bien, ahora podemos hacer dibujos personalizados en la pantalla. Sin embargo, es estático: Hagamos que esta forma gire alrededor del centro. La solución para hacer esto es simplemente cambiar los valores angle_from y angle_to a lo largo del tiempo. Para nuestro ejemplo, simplemente los incrementaremos en 50. Este valor de incremento tiene que permanecer constante o bien la velocidad de rotación cambiará en consecuencia.

Primero, tenemos que hacer que ambos, angle_from y angle_to sean variables del ámbito de la instancia (al alcance de cualquier función) al principio del script. También se pueden colocar en otros nodos y accederlas mediante get_node("nombre_nodo").nombre_variable.

extends Node2D

var rotation_angle = 50
var angle_from = 75
var angle_to = 195

Haremos que esos valores cambien en la función _process(delta).

También aquí incrementamos nuestros valores angle_from y angle_to. Sin embargo, no debemos olvidarnos de usar una función wrap() para ajustar los valores resultantes entre 0 y 360°, es decir, si el ángulo es 361°, entonces en realidad es 1°. Si no ajustas estos valores, el script funcionará correctamente, pero los valores de los ángulos crecerán más y más con el tiempo hasta que alcancen el máximo valor entero que Godot puede manejar (2^31 - 1). Cuando esto suceda, Godot puede fallar o producir un comportamiento inesperado.

Finally, we must not forget to call the queue_redraw() function, which automatically calls _draw(). This way, you can control when you want to refresh the frame.

func _process(delta):
    angle_from += rotation_angle
    angle_to += rotation_angle

    # We only wrap angles when both of them are bigger than 360.
    if angle_from > 360 and angle_to > 360:
        angle_from = wrapf(angle_from, 0, 360)
        angle_to = wrapf(angle_to, 0, 360)
    queue_redraw()

Y no olvides modificar la función _draw() para que haga uso de estas variables:

func _draw():
   var center = Vector2(200, 200)
   var radius = 80
   var color = Color(1.0, 0.0, 0.0)

   draw_circle_arc( center, radius, angle_from, angle_to, color )

Ejecutémoslo y veamos como funciona. Podemos notar que el arco rota demasiado rápido, qué está mal?

La razón es que tu GPU está mostrando frames tan rápido como puede, necesitamos "estabilizar" la velocidad de dibujado acorde a esta. Para conseguirlo, nos aseguraremos de utilizar el parámetro delta de la función _process(). delta contiene un número que representa el tiempo transcurrido entre dos frames. Generalmente es un valor muy bajo (alrededor de 0.0003 segundos, pero depende del hardware). Así que utilizando delta se puede asegurar que el programa se ejecutará a la misma velocidad en todo tipo de hardware.

En nuestro caso, simplemente multiplicaremos nuestra variable rotation_angle por delta en la función _process(). De este modo, nuestros 2 ángulos se incrementarán por un valor muy bajo, dependiendo directamente de nuestra velocidad de procesamiento.

func _process(delta):
    angle_from += rotation_angle * delta
    angle_to += rotation_angle * delta

    # We only wrap angles when both of them are bigger than 360.
    if angle_from > 360 and angle_to > 360:
        angle_from = wrapf(angle_from, 0, 360)
        angle_to = wrapf(angle_to, 0, 360)
    queue_redraw()

¡Ejecutémoslo de nuevo! ¡Esta vez, la rotación se muestra bien!

Dibujo antialiasing

Godot offers method parameters in draw_line to enable antialiasing, but not all custom drawing methods offer this antialiased parameter.

For custom drawing methods that don't provide an antialiased parameter, you can enable 2D MSAA instead, which affects rendering in the entire viewport. This provides high-quality antialiasing, but a higher performance cost and only on specific elements. See 2D antialiasing for more information.

Herramientas

Dibujar tus propios nodos también puede ser conveniente mientras los ejecutas en el editor para usarlos como vista previa o visualización de alguna característica o comportamiento. Consulta Ejecutando código en el editor para obtener más información.