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.
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
public override void _Draw()
{
// Your draw commands here
}
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.
Si es necesario redibujar porque algo ha cambiado, simplemente llama CanvasItem.update() en el nodo y se ejecutará una nueva llamada a _draw()
.
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.
update() # Update the node's visual representation.
func _draw():
draw_texture(texture, Vector2())
public class CustomNode2D : Node2D
{
private Texture _texture;
public Texture Texture
{
get
{
return _texture;
}
set
{
_texture = value;
Update();
}
}
public override void _Draw()
{
DrawTexture(_texture, new Vector2());
}
}
En algunos casos, puede ser necesario dibujar en cada frame. Para esto, sólo llama update()
dentro de _process()
, así:
extends Node2D
func _draw():
# Your draw commands here
pass
func _process(delta):
update()
public class CustomNode2D : Node2D
{
public override void _Draw()
{
// Your draw commands here
}
public override void _Process(float delta)
{
Update();
}
}
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 = PoolVector2Array()
for i in range(nb_points + 1):
var angle_point = deg2rad(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];
for (int i = 0; i < nbPoints; ++i)
{
float anglePoint = Mathf.Deg2Rad(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);
}
¿Recuerdas el número de puntos en el cual fue descompuesta nuestra figura? Fijamos este número en la variable nb_points
a un valor de 32
. Luego inicializamos un PoolVector2Array
vacío, que es un array de Vector2
.
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.
La razón por la que cada ángulo se reduce en 90° es que calcularemos las posiciones 2D de cada ángulo usando trigonometría (ya sabes, cosas de coseno y seno...). Sin embargo, para simplificarlo, cos()
y sin()
usan radianes, no grados. El ángulo de 0° (0 radianes) comienza a las 3 en punto, aunque queremos empezar a contar a las 12 en punto. Así que reducimos cada ángulo en 90° para empezar a contar desde las 12 en punto.
La posición actual de un punto ubicado en un angulo angle
(en radianes) es dada por Vector2(cos(angle), sin(angle))
. Como cos()
y sin()
retornan valores entre -1 y 1, la posición es ubicada en un círculo de radio 1. Para tener esta posición en nuestro círculo de soporte, que tiene un radio de radius
, simplemente tenemos que multiplicar la posición por radius
. Finalmente, tenemos la posición del círculo de soporte del arco en la posición center
, lo que se consigue agregando esto a nuestro valor Vector2
. Finalmente insertamos el punto en e PoolVector2Array
que fue definido previamente.
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)
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:
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 = PoolVector2Array()
points_arc.push_back(center)
var colors = PoolColorArray([color])
for i in range(nb_points + 1):
var angle_point = deg2rad(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 + 1];
pointsArc[0] = center;
var colors = new Color[] { color };
for (int i = 0; i < nbPoints; ++i)
{
float anglePoint = Mathf.Deg2Rad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90);
pointsArc[i + 1] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
}
DrawPolygon(pointsArc, colors);
}
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
public class CustomNode2D : Node2D
{
private float _rotationAngle = 50;
private float _angleFrom = 75;
private float _angleTo = 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.
Finalmente, no debemos olvidar llamar la función update()
, la que provoca una llamada a _draw()
. De este modo, podrás controlar cuando quieres que se actualice.
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)
update()
public override void _Process(float 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);
}
Update();
}
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 )
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);
}
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)
update()
public override void _Process(float delta)
{
_angleFrom += _rotationAngle * delta;
_angleTo += _rotationAngle * 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);
}
Update();
}
¡Ejecutémoslo de nuevo! ¡Esta vez, la rotación se muestra bien!
Dibujo antialiasing¶
Godot ofrece parámetros de método en draw_line para habilitar el antialiasing, pero no funciona de manera confiable en todas las situaciones (por ejemplo, en plataformas móviles/web o cuando el HDR está habilitado). Además, no hay un parámetro "antialiased" disponible en draw_polygon.
Como solución alternativa, puedes instalar y utilizar el complemento "Antialiased Line2D" <https://github.com/godot-extended-libraries/godot-antialiased-line2d>, el cual también admite el dibujo antialiasing de Polygon2D. Ten en cuenta que este complemento se basa en nodos de alto nivel en lugar de las funciones "_draw()" de bajo nivel.
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.