Up to date
This page is up to date for Godot 4.2
.
If you still find outdated information, please open an issue.
Desenho personalizado em 2D¶
Introdução¶
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.
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.
Visualizações que não são compatíveis com nós, como um quadro de tetris. (O exemplo do tetris usa uma função de desenho personalizada para desenhar os blocos.)
Desenhar um grande número de objetos simples. O desenho personalizado evita a sobrecarga de usar um grande número de nós, possivelmente diminuindo o uso de memória e melhorando o desempenho.
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¶
Adicione um script a qualquer nó derivado de CanvasItem, como Control ou Node2D. Em seguida, sobrescreva a função _draw()
.
extends Node2D
func _draw():
# Your draw commands here
pass
public override void _Draw()
{
// Your draw commands here
}
Os comandos de desenho são descritos na referência de classe CanvasItem. Existem muitos deles.
Atualizando¶
A função _draw()
é chamada apenas uma vez e, em seguida, os comandos de desenho são armazenados em cache e lembrados, portanto, chamadas adicionais são desnecessárias.
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.
Aqui está um exemplo um pouco mais complexo, uma variável de textura que será redesenhada se modificada:
extends Node2D
@export var texture: Texture:
set = _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())
using Godot;
public partial class MyNode2D : Node2D
{
private Texture _texture;
public Texture Texture
{
get
{
return _texture;
}
set
{
_texture = value;
QueueRedraw();
}
}
public override void _Draw()
{
DrawTexture(_texture, new 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()
using Godot;
public partial class CustomNode2D : Node2D
{
public override void _Draw()
{
// Your draw commands here
}
public override void _Process(double delta)
{
QueueRedraw();
}
}
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.
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)
public override void _Draw()
{
DrawLine(new Vector2(1.5f, 1.0f), new Vector2(1.5f, 4.0f), Colors.Green, 1.0f);
DrawLine(new Vector2(4.0f, 1.0f), new Vector2(4.0f, 4.0f), Colors.Green, 2.0f);
DrawLine(new Vector2(7.5f, 1.0f), new Vector2(7.5f, 4.0f), Colors.Green, 3.0f);
}
The same applies to the draw_rect
method with filled = false
.
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)
public override void _Draw()
{
DrawRect(new Rect2(1.0f, 1.0f, 3.0f, 3.0f), Colors.Green);
DrawRect(new Rect2(5.5f, 1.5f, 2.0f, 2.0f), Colors.Green, false, 1.0f);
DrawRect(new Rect2(9.0f, 1.0f, 5.0f, 5.0f), Colors.Green);
DrawRect(new Rect2(16.0f, 2.0f, 3.0f, 3.0f), Colors.Green, false, 2.0f);
}
Um exemplo: desenhando arcos circulares¶
Agora usaremos a funcionalidade de desenho personalizado do Godot Engine para desenhar algo para o qual Godot não fornece funções. Como exemplo, Godot fornece uma função draw_circle()
que desenha um círculo inteiro. No entanto, que tal desenhar uma parte de um círculo? Você terá que codificar uma função para executar isso e desenhá-la você mesmo.
Função arco¶
Um arco é definido pelos parâmetros do círculo de suporte, ou seja, a posição central e o raio. O próprio arco é então definido pelo ângulo a partir do qual começa e o ângulo em que termina. Estes são os 4 argumentos que temos que fornecer à nossa função de desenho. Também forneceremos o valor da cor, para que possamos desenhar o arco em cores diferentes, se desejarmos.
Basicamente, desenhar uma forma na tela requer que ela seja decomposta em um certo número de pontos vinculados de um para o outro. Como você pode imaginar, quanto mais pontos sua forma for feita, mais lisa ela aparecerá, mas mais pesada também será em termos de custo de processamento. Em geral, se sua forma for enorme (ou em 3D, perto da câmera), será necessário que mais pontos sejam desenhados sem ter uma aparência angular. Ao contrário, se o seu formato for pequeno (ou em 3D, longe da câmera), você pode diminuir o número de pontos para economizar custos de processamento; isso é conhecido como Nível de Detalhe (LOD). Em nosso exemplo, vamos simplesmente usar um número fixo de pontos, não importa o raio.
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)
public void DrawCircleArc(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
{
int nbPoints = 32;
var pointsArc = new Vector2[nbPoints + 1];
for (int i = 0; i <= nbPoints; i++)
{
float anglePoint = Mathf.DegToRad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90f);
pointsArc[i] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
}
for (int i = 0; i < nbPoints - 1; i++)
{
DrawLine(pointsArc[i], pointsArc[i + 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 Vector2
s.
O próximo passo consiste em computar as posições reais desses 32 pontos que compõem um arco. Isso é feito no primeiro loop-for: iteramos sobre o número de pontos para os quais queremos calcular as posições, mais um para incluir o último ponto. Primeiro determinamos o ângulo de cada ponto, entre os ângulos inicial e 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.
Agora, precisamos realmente desenhar nossos pontos. Como você pode imaginar, não iremos simplesmente sortear nossos 32 pontos: precisamos sortear tudo o que está entre cada um deles. Poderíamos ter calculado todos os pontos usando o método anterior e desenhado um por um. Mas isso é muito complicado e ineficiente (exceto se explicitamente necessário), então simplesmente desenhamos linhas entre cada par de pontos. A menos que o raio do nosso círculo de suporte seja grande, o comprimento de cada linha entre um par de pontos nunca será longo o suficiente para vê-los. Se isso acontecesse, simplesmente precisaríamos aumentar o número de pontos.
Desenhe o arco na tela¶
Agora temos uma função que desenha coisas na tela; é hora de chamá-lo dentro da função _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)
public override void _Draw()
{
var center = new Vector2(200, 200);
float radius = 80;
float angleFrom = 75;
float angleTo = 195;
var color = new Color(1, 0, 0);
DrawCircleArc(center, radius, angleFrom, angleTo, color);
}
Resultado:
Função de polígono de arco¶
Podemos dar um passo adiante e não apenas escrever uma função que desenha a parte plana do disco definida pelo arco, mas também sua forma. O método é exatamente o mesmo de antes, exceto que desenhamos um polígono em vez de linhas:
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)
public void DrawCircleArcPoly(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
{
int nbPoints = 32;
var pointsArc = new Vector2[nbPoints + 2];
pointsArc[0] = center;
var colors = new Color[] { color };
for (int i = 0; i <= nbPoints; i++)
{
float anglePoint = Mathf.DegToRad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90);
pointsArc[i + 1] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
}
DrawPolygon(pointsArc, colors);
}
Desenho personalizado dinâmico¶
Tudo bem, agora podemos desenhar coisas personalizadas na tela. No entanto, é estático; vamos fazer essa forma girar em torno do centro. A solução para fazer isso é simplesmente alterar os valores angle_from e angle_to ao longo do tempo. Para o nosso exemplo, iremos simplesmente incrementá-los em 50. Este valor de incremento deve permanecer constante ou então a velocidade de rotação mudará de acordo.
Primeiro, temos que tornar as variáveis angle_from e angle_to globais no topo do nosso script. Observe também que você pode armazená-los em outros nós e acessá-los usando get_node()
.
extends Node2D
var rotation_angle = 50
var angle_from = 75
var angle_to = 195
using Godot;
public partial class MyNode2D : Node2D
{
private float _rotationAngle = 50;
private float _angleFrom = 75;
private float _angleTo = 195;
}
Fazemos a alteração desses valores na função _process(delta).
Também incrementamos nossos valores de angle_from e angle_to aqui. No entanto, não devemos esquecer de usar o wrap()
nos valores resultantes entre 0 e 360°! Ou seja, se o ângulo é 361°, então na verdade é 1°. Se você não agrupar esses valores, o script funcionará corretamente, mas os valores dos ângulos crescerão cada vez mais com o tempo até atingirem o valor inteiro máximo que Godot pode gerenciar (2^31 - 1
). Quando isso acontece, o Godot pode travar ou produzir um comportamento 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()
public override void _Process(double delta)
{
_angleFrom += _rotationAngle;
_angleTo += _rotationAngle;
// We only wrap angles when both of them are bigger than 360.
if (_angleFrom > 360 && _angleTo > 360)
{
_angleFrom = Mathf.Wrap(_angleFrom, 0, 360);
_angleTo = Mathf.Wrap(_angleTo, 0, 360);
}
QueueRedraw();
}
Além disso, não se esqueça de modificar a função _draw()
para fazer uso dessas variáveis:
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 )
public override void _Draw()
{
var center = new Vector2(200, 200);
float radius = 80;
var color = new Color(1, 0, 0);
DrawCircleArc(center, radius, _angleFrom, _angleTo, color);
}
Vamos executá-la! Funciona, mas o arco está girando incrivelmente rápido! O que está errado?
O motivo é que sua GPU está realmente exibindo os quadros o mais rápido possível. Precisamos "normalizar" o desenho nessa velocidade; para conseguir isso, temos que fazer uso do parâmetro delta
da função _process()
. delta
contém o tempo decorrido entre os dois últimos quadros renderizados. Geralmente é pequeno (cerca de 0,0003 segundos, mas isso depende do seu hardware), portanto, usar delta
para controlar seu desenho garante que seu programa seja executado na mesma velocidade no hardware de todos.
No nosso caso, simplesmente precisamos multiplicar nossa variável rotation_angle
por delta
na função _process()
. Dessa forma, nossos 2 ângulos serão aumentados em um valor bem menor, que depende diretamente da velocidade de renderização.
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()
public override void _Process(double delta)
{
_angleFrom += _rotationAngle * (float)delta;
_angleTo += _rotationAngle * (float)delta;
// We only wrap angles when both of them are bigger than 360.
if (_angleFrom > 360 && _angleTo > 360)
{
_angleFrom = Wrap(_angleFrom, 0, 360);
_angleTo = Wrap(_angleTo, 0, 360);
}
QueueRedraw();
}
Vamos executar de novo! Desta vez, a rotação parece boa!
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.
Ferramentas¶
Desenhar seus próprios nós também pode ser desejado ao executá-los no editor. Isso pode ser usado como uma prévia ou visualização de algum recurso ou comportamento. Veja Executando o código no editor para mais informações.