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.

Desenho personalizado em 2D

Introdução

Godot has nodes to draw sprites, polygons, particles, text, and many other common game development needs. However, if you need something specific not covered with the standard nodes you can make any 2D node (for example, Control or Node2D-based) draw on screen using custom commands.

O desenho personalizado em um nó 2D é realmente útil. Aqui estão alguns casos de uso:

  • Desenhar formas ou lógica que os nós existentes não podem fazer, como uma imagem com rastros ou um polígono animado especial.

  • Drawing a large number of simple objects, such as a grid or a board for a 2d game. Custom drawing avoids the overhead of using a large number of nodes, possibly lowering memory usage and improving performance.

  • Fazendo um controle de interface do usuário personalizado. Há muitos controles disponíveis, mas quando você tiver necessidades incomuns, provavelmente precisará de um controle personalizado.

Desenhando

Add a script to any CanvasItem derived node, like Control or Node2D. Then override the _draw() function.

extends Node2D

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

Draw commands are described in the CanvasItem class reference. There are plenty of them and we will see some of them in the examples below.

Atualizando

The _draw function is only called once, and then the draw commands are cached and remembered, so further calls are unnecessary.

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

Here is a little more complex example, where we have a texture variable that can be modified at any time, and using a setter, it forces a redraw of the texture when modified:

extends Node2D

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

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

To see it in action, you can set the texture to be the Godot icon on the editor by dragging and dropping the default icon.svg from the FileSystem tab to the Texture property on the Inspector tab. When changing the Texture property value while the previous script is running, the texture will also change automatically.

In some cases, we may need to redraw every frame. For this, call queue_redraw from the _process method, like this:

extends Node2D

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

func _process(_delta):
    queue_redraw()

Coordinates and line width alignment

The drawing API uses the CanvasItem's coordinate system, not necessarily pixel coordinates. This means _draw() 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 of the start and end points 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)

Desenho com suavização de serrilhado

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.

Here is a comparison of a line of minimal width (width=-1) drawn with antialiased=false, antialiased=true, and antialiased=false with 2D MSAA 2x, 4x, and 8x enabled.

../../_images/draw_antialiasing_options.webp

Ferramentas

Drawing your own nodes might also be desired while running them in the editor. This can be used as a preview or visualization of some feature or behavior.

To do this, you can use the tool annotation on both GDScript and C#. See the example below and Executando o código no editor for more information.

Example 1: drawing a custom shape

We will now use the custom drawing functionality of the Godot Engine to draw something that Godot doesn't provide functions for. We will recreate the Godot logo but with code- only using drawing functions.

You will have to code a function to perform this and draw it yourself.

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

Criando um nó de um polígono personalizado

Embora haja um nó dedicado para desenhar polígonos personalizados ( Polygon2D), vamos usar neste caso exclusivamente funções de desenho de nível inferior para combiná-los no mesmo nó e ser capaz de criar formas mais complexas mais tarde.

Primeiro, definiremos um conjunto de pontos -ou coordenadas X e Y - que formarão a base da nossa 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, enquanto compacto, não é aquele que Godot entende desenhar um polígono. Em um cenário diferente, poderíamos ter que carregar essas coordenadas de um arquivo ou calcular as posições enquanto o aplicativo está sendo executado, então alguma transformação pode ser necessária.

Para transformar essas coordenadas no formato certo, criaremos um novo método float_array_to_Vector2Array(). Então vamos substituir a função _ready(), que Godot vai chamar apenas uma vez - no início da execução- para carregar essas coordenadas em uma variável:

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 desenhar nossa primeira forma, vamos usar o método draw_polygon e passar os pontos (como uma matriz de coordenadas Vector2) e sua cor, como esta:

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

Ao executá-lo você deve ver algo assim:

../../_images/draw_godot_logo_polygon.webp

Note a parte inferior do logotipo parece segmentada - isto é porque uma baixa quantidade de pontos foram usados para definir essa parte. Para simular uma curva lisa, poderíamos adicionar mais pontos ao nosso array, ou talvez usar uma função matemática para interpor uma curva e criar uma forma lisa do código (ver example 2).

Os polígonos sempre conectam seu último ponto definido ao seu primeiro a fim de ter uma forma fechada.

Desenhando linhas conectadas

Desenhar uma sequência de linhas conectadas que não se fecham para formar um polígono é muito semelhante ao método anterior. Usaremos um conjunto de linhas conectadas para desenhar a boca do logotipo do Godot.

Primeiro, vamos definir a lista de coordenadas que formam a forma da boca, assim:

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

Vamos carregar essas coordenadas em uma variável e definir uma variável adicional com a espessura da linha configurável:

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 finalmente usaremos o método draw_polyline para realmente desenhar a linha, assim:

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)

Você deve obter a seguinte saída:

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

A ordem das chamadas _draw é importante - como que com as posições dos Nós na hierarquia da árvore, as diferentes formas serão desenhadas de cima para baixo, resultando nas formas mais recentes que se escondem mais cedo se eles se sobrepõem. Neste caso queremos a boca desenhada sobre a cabeça, então colocamos depois.

Observe como podemos definir cores de maneiras diferentes, seja com um código hexadecimal ou um nome de cor predefinido. Confira a classe Color para outras constantes e formas de definir Cores.

Desenhando Círculos

Para criar os olhos, vamos adicionar 4 chamadas adicionais para desenhar as formas dos olhos, em diferentes tamanhos, cores e posições.

Para desenhar um círculo, você posiciona-o com base em seu centro usando o método draw_circle. O primeiro parâmetro é um :ref:`Vector2<class_Vector2> com as coordenadas de seu centro, o segundo é seu raio, e o terceiro é sua cor:

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)

Ao executá-lo, você deve ter algo assim:

../../_images/draw_godot_logo_circle.webp

Para arcos parciais, não preenchidos (porções de uma forma círculo entre certos ângulos arbitrários), você pode usar o método draw_arc.

Desenhando linhas

Para desenhar a forma final (o nariz) usaremos uma linha para aproximá-la.

draw_line pode ser usado para desenhar um único segmento fornecendo suas coordenadas de início e fim como argumentos, como este:

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)

Agora você deve ser capaz de ver a seguinte forma na tela:

../../_images/draw_godot_logo_line.webp

Note que se várias linhas não conectadas forem desenhadas ao mesmo tempo, você pode obter desempenho adicional desenhando todas elas em uma única chamada, usando o método draw_multiline.

Desenhando 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.

Vamos usar o método draw_string para fazê-lo, assim:

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)

Aqui nós primeiro carregamos a variável defaultFont a fonte padrão configurada no tema(uma personalizada pode ser definida no lugar) e então passamos os seguintes parâmetros: fonte, posição, texto, alinhamento horizontal, largura e tamanho da fonte.

Você deve ver o seguinte em sua tela:

../../_images/draw_godot_logo_text.webp

Parâmetros adicionais, bem como outros métodos relacionados ao texto e caracteres podem ser encontrados na referência de classe CanvasItem.

Mostrar o desenho durante a edição

Enquanto o código até agora é capaz de desenhar o logotipo em uma janela em execução, ele não aparecerá na view 2D no editor. Em certos casos, você também gostaria de mostrar seu Node2D personalizado ou controle no editor, posicionar e dimensioná-lo adequadamente, como a maioria dos outros nós fazem.

Para mostrar o logotipo diretamente no editor (sem executá-lo), você pode usar o @tool anotação para solicitar o desenho personalizado do nó também aparecer enquanto edita, assim:

@tool
extends Node2D

Você precisará salvar sua cena, refazer a build seu projeto (apenas para C#) e recarregar a cena atual manualmente na opção menu Scene > Recarregue Saved Scene para refrescar o nó atual na view 2D da primeira vez que você adicionar ou remover a anotação @tool.

Animação

Se quiséssemos fazer a mudança de forma personalizada no tempo de execução, poderíamos modificar os métodos chamados ou seus argumentos no tempo de execução, ou aplicar uma transformação.

Por exemplo, se quisermos a forma personalizada que acabamos de projetar para girar, poderíamos adicionar a seguinte variável e código aos métodos _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

O problema com o código acima é que porque criamos os pontos aproximadamente em um retângulo a partir do canto superior esquerdo, a coordenada (0, 0)` e estendendo-se para a direita e para baixo, vemos que a rotação é feita usando o canto superior esquerdo como pivô. Uma mudança de posição de transformação no nó não nos ajudará aqui, pois a transformação de rotação é aplicada primeiro.

Embora pudéssemos reescrever todas as coordenadas dos pontos para serem centradas em torno de (0, 0), incluindo coordenadas negativas, isso seria muito trabalho.

Uma maneira possível de trabalhar em torno disso é usar o método de nível mais baixo draw_set_transform para corrigir este problema, traduzindo todos os pontos no próprio espaço do CanvasItem, e depois movendo-o de volta para o seu lugar original com um nó regular transforma, tanto no editor ou no código, como este:

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

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

Este é o resultado, girando em torno de um pivô agora em (60, 60):

../../_images/draw_godot_rotation.webp

Se o que queríamos animar era uma propriedade dentro da chamada _draw(), devemos lembrar de chamar queue_redraw() para forçar uma atualização, como de outra forma não seria atualizado na tela.

Por exemplo, é assim que podemos fazer o robô parecer abrir e fechar a boca, alterando a largura de sua linha da boca seguir uma 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)
    ...

Vai parecer um pouco assim quando executar:

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

Example 2: drawing a dynamic line

The previous example was useful to learn how to draw and modify nodes with custom shapes and animations. This could have some advantages, such as using exact coordinates and vectors for drawing, rather than bitmaps -which means they will scale well when transformed on screen. In some cases, similar results could be achieved composing higher level functionality with nodes such as sprites or AnimatedSprites loading SVG resources (which are also images defined with vectors) and the AnimationPlayer node.

In other cases that will not be possible because we will not know what the resulting graphical representation will be before running the code. Here we will see how to draw a dynamic line whose coordinates are not known beforehand, and are affected by the user's input.

Drawing a straight line between 2 points

Let's assume we want to draw a straight line between 2 points, the first one will be fixed on the upper left corner (0, 0) and the second will be defined by the cursor position on screen.

We could draw a dynamic line between those 2 points like this:

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 this example we obtain the position of the mouse in the default viewport every frame with the method get_mouse_position. If the position has changed since the last draw request (a small optimization to avoid redrawing on every frame)- we will schedule a redraw. Our _draw() method only has one line: requesting the drawing of a green line of width 10 pixels between the top left corner and that obtained position.

The width, color, and position of the starting point can be configured with with the corresponding properties.

It should look like this when run:

../../_images/draw_line_between_2_points.webp

Drawing an arc between 2 points

The above example works, but we may want to join those 2 points with a different shape or function, other than a straight line.

Let's try now creating an arc (a portion of a circumference) between both points.

Exporting the line starting point, segments, width, color, and antialiasing will allow us to modify those properties very easily directly from the editor inspector panel:

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

To draw the arc, we can use the method draw_arc. There are many arcs that pass through 2 points, so we will chose for this example the semicircle that has its center in the middle point between the 2 initial points.

Calculating this arc will be more complex than in the case of the line:

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)

The center of the semicircle will be the middle point between both points. The radius will be half the distance between both points. The start and end angles will be the angles of the vector from point1 to point2 and vice-versa. Note we had to normalize the end_angle in positive values because if end_angle is less than start_angle, the arc will be drawn counter-clockwise, which we don't want in this case (the arc would be upside-down).

The result should be something like this, with the arc going down and between the points:

../../_images/draw_arc_between_2_points.webp

Feel free to play with the parameters in the inspector to obtain different results: change the color, the width, the antialiasing, and increase the number of segments to increase the curve smoothness, at the cost of extra performance.