Files
godot-docs/tutorials/2d/custom_drawing_in_2d.rst
Matthew b3b0b1e5e9 Merge pull request #9267 from JordanKlooster/patch-1
Fixed small code error in custom_drawing_in_2d.rst
2025-01-24 22:28:16 -05:00

1061 lines
34 KiB
ReStructuredText

.. _doc_custom_drawing_in_2d:
Custom drawing in 2D
====================
Introduction
------------
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,
:ref:`Control <class_Control>` or :ref:`Node2D <class_Node2D>`-based)
draw on screen using custom commands.
Custom drawing in a 2D node is *really* useful. Here are some use cases:
- Drawing shapes or logic that existing nodes can't do, such as an image
with trails or a special animated polygon.
- 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.
- Making a custom UI control. There are plenty of controls available,
but when you have unusual needs, you will likely need a custom
control.
Drawing
-------
Add a script to any :ref:`CanvasItem <class_CanvasItem>`
derived node, like :ref:`Control <class_Control>` or
:ref:`Node2D <class_Node2D>`. Then override the
:ref:`_draw()<class_CanvasItem_private_method__draw>` function.
.. tabs::
.. code-tab:: gdscript GDScript
extends Node2D
func _draw():
pass # Your draw commands here.
.. code-tab:: csharp
using Godot;
public partial class MyNode2D : Node2D
{
public override void _Draw()
{
// Your draw commands here.
}
}
Draw commands are described in the :ref:`CanvasItem <class_CanvasItem>`
class reference. There are plenty of them and we will see some of them
in the examples below.
Updating
--------
The :ref:`_draw <class_CanvasItem_private_method__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 :ref:`CanvasItem.queue_redraw <class_CanvasItem_method_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
:ref:`setter<doc_gdscript_basics_setters_getters>`, it forces a redraw
of the texture when modified:
.. tabs::
.. code-tab:: gdscript GDScript
extends Node2D
@export var texture : Texture2D:
set(value):
texture = value
queue_redraw()
func _draw():
draw_texture(texture, Vector2())
.. code-tab:: csharp
using Godot;
public partial class MyNode2D : Node2D
{
private Texture2D _texture;
[Export]
public Texture2D Texture
{
get
{
return _texture;
}
set
{
_texture = value;
QueueRedraw();
}
}
public override void _Draw()
{
DrawTexture(_texture, new 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 :ref:`queue_redraw <class_CanvasItem_method_queue_redraw>`
from the :ref:`_process <class_Node_private_method__process>` method, like this:
.. tabs::
.. code-tab:: gdscript GDScript
extends Node2D
func _draw():
pass # Your draw commands here.
func _process(_delta):
queue_redraw()
.. code-tab:: csharp
using Godot;
public partial class MyNode2D : Node2D
{
public override void _Draw()
{
// Your draw commands here.
}
public override void _Process(double delta)
{
QueueRedraw();
}
}
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
:ref:`draw_set_transform<class_CanvasItem_method_draw_set_transform>` or
:ref:`draw_set_transform_matrix<class_CanvasItem_method_draw_set_transform_matrix>`.
When using :ref:`draw_line <class_CanvasItem_method_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.
.. image:: img/draw_line.png
.. tabs::
.. code-tab:: gdscript GDScript
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)
.. code-tab:: csharp
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 :ref:`draw_rect <class_CanvasItem_method_draw_rect>`
method with ``filled = false``.
.. image:: img/draw_rect.png
.. tabs::
.. code-tab:: gdscript GDScript
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)
.. code-tab:: csharp
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);
}
Antialiased drawing
-------------------
Godot offers method parameters in :ref:`draw_line<class_CanvasItem_method_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 :ref:`doc_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.
.. image:: img/draw_antialiasing_options.webp
Tools
-----
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 :ref:`tool annotation<doc_gdscript_tool_mode>`
on both GDScript and C#. See
:ref:`the example below<doc_draw_show_drawing_while_editing_example>` and
:ref:`doc_running_code_in_the_editor` for more information.
.. _doc_draw_custom_example_1:
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.
.. note::
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
:ref:`Display > Window > Stretch > Scale<class_ProjectSettings_property_display/window/stretch/scale>`
to adjust the project to a higher resolution (a 2 or 4 scale tends to work well).
Drawing a custom polygon shape
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
While there is a dedicated node to draw custom polygons (
:ref:`Polygon2D <class_Polygon2D>`), we will use in this case exclusively lower
level drawing functions to combine them on the same node and be able to create
more complex shapes later on.
First, we will define a set of points -or X and Y coordinates- that will form
the base of our shape:
.. tabs::
.. code-tab:: gdscript GDScript
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 ]
]
.. code-tab:: csharp
using Godot;
public partial class MyNode2D : Node2D
{
private float[,] _coordsHead =
{
{ 22.952f, 83.271f }, { 28.385f, 98.623f },
{ 53.168f, 107.647f }, { 72.998f, 107.647f },
{ 99.546f, 98.623f }, { 105.048f, 83.271f },
{ 105.029f, 55.237f }, { 110.740f, 47.082f },
{ 102.364f, 36.104f }, { 94.050f, 40.940f },
{ 85.189f, 34.445f }, { 85.963f, 24.194f },
{ 73.507f, 19.930f }, { 68.883f, 28.936f },
{ 59.118f, 28.936f }, { 54.494f, 19.930f },
{ 42.039f, 24.194f }, { 42.814f, 34.445f },
{ 33.951f, 40.940f }, { 25.637f, 36.104f },
{ 17.262f, 47.082f }, { 22.973f, 55.237f }
};
}
This format, while compact, is not the one that Godot understands to
draw a polygon. In a different scenario we could have to load
these coordinates from a file or calculate the positions while the
application is running, so some transformation may be needed.
To transform these coordinates into the right format, we will create a new
method ``float_array_to_Vector2Array()``. Then we will override the ``_ready()``
function, which Godot will call only once -at the start of the execution-
to load those coordinates into a variable:
.. tabs::
.. code-tab:: gdscript GDScript
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);
.. code-tab:: csharp
private Vector2[] _head;
private Vector2[] FloatArrayToVector2Array(float[,] coords)
{
// Convert the array of floats into an array of Vector2.
int size = coords.GetUpperBound(0);
Vector2[] array = new Vector2[size + 1];
for (int i = 0; i <= size; i++)
{
array[i] = new Vector2(coords[i, 0], coords[i, 1]);
}
return array;
}
public override void _Ready()
{
_head = FloatArrayToVector2Array(_coordsHead);
}
To finally draw our first shape, we will use the method
:ref:`draw_polygon <class_CanvasItem_method_draw_polygon>`
and pass the points (as an array of Vector2 coordinates) and its color,
like this:
.. tabs::
.. code-tab:: gdscript GDScript
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 ])
.. code-tab:: csharp
public override void _Draw()
{
// We are going to paint with this color.
Color godotBlue = new Color("478cbf");
// We pass the array of Vector2 to draw the shape.
DrawPolygon(_head, [godotBlue]);
}
When running it you should see something like this:
.. image:: img/draw_godot_logo_polygon.webp
Note the lower part of the logo looks segmented- this is because a low
amount of points were used to define that part. To simulate a smooth curve,
we could add more points to our array, or maybe use a mathematical function to
interpolate a curve and create a smooth shape from code (see
:ref:`example 2<doc_draw_custom_example_2>`).
Polygons will always **connect its last defined point to its first
one** in order to have a closed shape.
Drawing connected lines
~~~~~~~~~~~~~~~~~~~~~~~
Drawing a sequence of connected lines that don't close down to form a polygon
is very similar to the previous method. We will use a connected set of lines to
draw Godot's logo mouth.
First, we will define the list of coordinates that form the mouth shape, like this:
.. tabs::
.. code-tab:: gdscript GDScript
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 ]
]
.. code-tab:: csharp
private float[,] _coordsMouth =
{
{ 22.817f, 81.100f }, { 38.522f, 82.740f },
{ 39.001f, 90.887f }, { 54.465f, 92.204f },
{ 55.641f, 84.260f }, { 72.418f, 84.177f },
{ 73.629f, 92.158f }, { 88.895f, 90.923f },
{ 89.556f, 82.673f }, { 105.005f, 81.100f }
};
We will load these coordinates into a variable and define an additional
variable with the configurable line thickness:
.. tabs::
.. code-tab:: gdscript GDScript
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);
.. code-tab:: csharp
private Vector2[] _mouth;
private float _mouthWidth = 4.4f;
public override void _Ready()
{
_head = FloatArrayToVector2Array(_coordsHead);
_mouth = FloatArrayToVector2Array(_coordsMouth);
}
And finally we will use the method
:ref:`draw_polyline <class_CanvasItem_method_draw_polyline>` to actually
draw the line, like this:
.. tabs::
.. code-tab:: gdscript GDScript
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)
.. code-tab:: csharp
public override void _Draw()
{
// We will use white to draw the line.
Color white = Colors.White;
Color godotBlue = new Color("478cbf");
DrawPolygon(_head, [godotBlue]);
// We draw the while line on top of the previous shape.
DrawPolyline(_mouth, white, _mouthWidth);
}
You should get the following output:
.. image:: img/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).
The order of the ``_draw`` calls is important- like with the Node positions on
the tree hierarchy, the different shapes will be drawn from top to bottom,
resulting in the latest shapes hiding earlier ones if they overlap. In this
case we want the mouth drawn over the head, so we put it afterwards.
Notice how we can define colors in different ways, either with a hexadecimal
code or a predefined color name. Check the class :ref:`Color <class_Color>` for other
constants and ways to define Colors.
Drawing circles
~~~~~~~~~~~~~~~
To create the eyes, we are going to add 4 additional calls to draw the eye
shapes, in different sizes, colors and positions.
To draw a circle, you position it based on its center using the
:ref:`draw_circle <class_CanvasItem_method_draw_circle>` method. The first
parameter is a :ref:`Vector2<class_Vector2>` with the coordinates of its center, the second is
its radius, and the third is its color:
.. tabs::
.. code-tab:: gdscript GDScript
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)
.. code-tab:: csharp
public override void _Draw()
{
Color white = Colors.White;
Color godotBlue = new Color("478cbf");
Color grey = new Color("414042");
DrawPolygon(_head, [godotBlue]);
DrawPolyline(_mouth, white, _mouthWidth);
// Four circles for the 2 eyes: 2 white, 2 grey.
DrawCircle(new Vector2(42.479f, 65.4825f), 9.3905f, white);
DrawCircle(new Vector2(85.524f, 65.4825f), 9.3905f, white);
DrawCircle(new Vector2(43.423f, 65.92f), 6.246f, grey);
DrawCircle(new Vector2(84.626f, 66.008f), 6.246f, grey);
}
When executing it, you should have something like this:
.. image:: img/draw_godot_logo_circle.webp
For partial, unfilled arcs (portions of a circle shape between certain
arbitrary angles), you can use the method
:ref:`draw_arc <class_CanvasItem_method_draw_arc>`.
Drawing lines
~~~~~~~~~~~~~
To draw the final shape (the nose) we will use a line to approximate it.
:ref:`draw_line <class_CanvasItem_method_draw_line>` can be used to draw
a single segment by providing its start and end coordinates as arguments,
like this:
.. tabs::
.. code-tab:: gdscript GDScript
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)
.. code-tab:: csharp
public override void _Draw()
{
Color white = Colors.White;
Color godotBlue = new Color("478cbf");
Color grey = new Color("414042");
DrawPolygon(_head, [godotBlue]);
DrawPolyline(_mouth, white, _mouthWidth);
DrawCircle(new Vector2(42.479f, 65.4825f), 9.3905f, white);
DrawCircle(new Vector2(85.524f, 65.4825f), 9.3905f, white);
DrawCircle(new Vector2(43.423f, 65.92f), 6.246f, grey);
DrawCircle(new Vector2(84.626f, 66.008f), 6.246f, grey);
// Draw a short but thick white vertical line for the nose.
DrawLine(new Vector2(64.273f, 60.564f), new Vector2(64.273f, 74.349f),
white, 5.8f);
}
You should now be able to see the following shape on screen:
.. image:: img/draw_godot_logo_line.webp
Note that if multiple unconnected lines are going to be drawn at the same time,
you may get additional performance by drawing all of them in a single call, using
the :ref:`draw_multiline <class_CanvasItem_method_draw_multiline>` method.
Drawing text
~~~~~~~~~~~~
While using the :ref:`Label <class_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.
We will use the :ref:`draw_string <class_CanvasItem_method_draw_string>` method
to do it, like this:
.. tabs::
.. code-tab:: gdscript GDScript
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)
.. code-tab:: csharp
private Font _defaultFont = ThemeDB.FallbackFont;
public override void _Draw()
{
Color white = Colors.White;
Color godotBlue = new Color("478cbf");
Color grey = new Color("414042");
DrawPolygon(_head, [godotBlue]);
DrawPolyline(_mouth, white, _mouthWidth);
DrawCircle(new Vector2(42.479f, 65.4825f), 9.3905f, white);
DrawCircle(new Vector2(85.524f, 65.4825f), 9.3905f, white);
DrawCircle(new Vector2(43.423f, 65.92f), 6.246f, grey);
DrawCircle(new Vector2(84.626f, 66.008f), 6.246f, grey);
DrawLine(new Vector2(64.273f, 60.564f), new Vector2(64.273f, 74.349f),
white, 5.8f);
// Draw GODOT text below the logo with the default font, size 22.
DrawString(_defaultFont, new Vector2(20f, 130f), "GODOT",
HorizontalAlignment.Center, 90, 22);
}
Here we first load into the defaultFont variable the configured default theme
font (a custom one can be set instead) and then we pass the following
parameters: font, position, text, horizontal alignment, width, and font size.
You should see the following on your screen:
.. image:: img/draw_godot_logo_text.webp
Additional parameters as well as other methods related to text and characters
can be found on the :ref:`CanvasItem <class_CanvasItem>` class reference.
.. _doc_draw_show_drawing_while_editing_example:
Show the drawing while editing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
While the code so far is able to draw the logo on a running window, it will
not show up on the ``2D view`` on the editor. In certain cases you would
also like to show your custom Node2D or control on the editor, to position
and scale it appropriately, like most other nodes do.
To show the logo directly on the editor (without running it), you can use the
:ref:`@tool<doc_gdscript_tool_mode>` annotation to request the custom drawing
of the node to also appear while editing, like this:
.. tabs::
.. code-tab:: gdscript GDScript
@tool
extends Node2D
.. code-tab:: csharp
using Godot;
[Tool]
public partial class MyNode2D : Node2D
You will need to save your scene, rebuild your project (for C# only) and reload
the current scene manually at the menu option ``Scene > Reload Saved Scene``
to refresh the current node in the ``2D`` view the first time you add or remove
the ``@tool`` annotation.
Animation
~~~~~~~~~
If we wanted to make the custom shape change at runtime, we could modify the
methods called or its arguments at execution time, or apply a transform.
For example, if we want the custom shape we just designed to rotate, we could add
the following variable and code to the ``_ready`` and ``_process`` methods:
.. tabs::
.. code-tab:: gdscript GDScript
extends Node2D
@export var rotation_speed : float = 1 # In radians per second.
func _ready():
rotation = 0
...
func _process(delta: float):
rotation -= rotation_speed * delta
.. code-tab:: csharp
[Export]
public float RotationSpeed { get; set; } = 1.0f; // In radians per second.
public override void _Ready()
{
Rotation = 0;
...
}
public override void _Process(double delta)
{
Rotation -= RotationSpeed * (float)delta;
}
The problem with the above code is that because we have created the points
approximately on a rectangle starting from the upper left corner, the ``(0, 0)``
coordinate and extending to the right and down, we see that the rotation is done
using the top left corner as pivot. A position transform change on the node
won't help us here, as the rotation transform is applied first.
While we could rewrite all of the points' coordinates to be centered around
``(0, 0)``, including negative coordinates, that would be a lot of work.
One possible way to work around this is to use the lower level
:ref:`draw_set_transform<class_CanvasItem_method_draw_set_transform>`
method to fix this issue, translating all points in the CanvasItem's own space,
and then moving it back to its original place with a regular node transform,
either in the editor or in code, like this:
.. tabs::
.. code-tab:: gdscript GDScript
func _ready():
rotation = 0
position = Vector2(60, 60)
...
func _draw():
draw_set_transform(Vector2(-60, -60))
...
.. code-tab:: csharp
public override void _Ready()
{
Rotation = 0;
Position = new Vector2(60, 60);
...
}
public override void _Draw()
{
DrawSetTransform(new Vector2(-60.0f, -60.0f));
...
}
This is the result, rotating around a pivot now on ``(60, 60)``:
.. image:: img/draw_godot_rotation.webp
If what we wanted to animate was a property inside the ``_draw()`` call, we must remember to
call ``queue_redraw()`` to force a refresh, as otherwise it would not be updated on screen.
For example, this is how we can make the robot appear to open and close its mouth, by
changing the width of its mouth line follow a sinusoidal (:ref:`sin<class_@globalscope_method_sin>`) curve:
.. tabs::
.. code-tab:: gdscript GDScript
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)
...
.. code-tab:: csharp
private float _mouthWidth = 4.4f;
private float _maxWidth = 7f;
private float _time = 0f;
public override void _Process(double delta)
{
_time += (float)delta;
_mouthWidth = Mathf.Abs(Mathf.Sin(_time) * _maxWidth);
QueueRedraw();
}
public override void _Draw()
{
...
DrawPolyline(_mouth, white, _mouthWidth);
...
}
It will look somewhat like this when run:
.. image:: img/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 :ref:`Tween<class_Tween>` or an
:ref:`AnimationPlayer<class_AnimationPlayer>` Node. The only difference is
that a ``queue_redraw()`` call is needed to apply those changes so they get
shown on screen.
.. _doc_draw_custom_example_2:
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
:ref:`sprites<class_Sprite2D>` or
:ref:`AnimatedSprites<class_AnimatedSprite2D>` loading SVG resources (which are
also images defined with vectors) and the
:ref:`AnimationPlayer<class_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:
.. tabs::
.. code-tab:: gdscript GDScript
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)
.. code-tab:: csharp
using Godot;
using System;
public partial class MyNode2DLine : Node2D
{
public Vector2 Point1 { get; set; } = new Vector2(0f, 0f);
public int Width { get; set; } = 10;
public Color Color { get; set; } = Colors.Green;
private Vector2 _point2;
public override void _Process(double delta)
{
Vector2 mousePosition = GetViewport().GetMousePosition();
if (mousePosition != _point2)
{
_point2 = mousePosition;
QueueRedraw();
}
}
public override void _Draw()
{
DrawLine(Point1, _point2, Color, Width);
}
}
In this example we obtain the position of the mouse in the default viewport
every frame with the method
:ref:`get_mouse_position <class_Viewport_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:
.. image:: img/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:
.. tabs::
.. code-tab:: gdscript GDScript
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
.. code-tab:: csharp
using Godot;
using System;
public partial class MyNode2DLine : Node2D
{
[Export]
public Vector2 Point1 { get; set; } = new Vector2(0f, 0f);
[Export]
public float Length { get; set; } = 350f;
[Export(PropertyHint.Range, "1,1000,")]
public int Segments { get; set; } = 100;
[Export]
public int Width { get; set; } = 10;
[Export]
public Color Color { get; set; } = Colors.Green;
[Export]
public bool AntiAliasing { get; set; } = false;
private Vector2 _point2;
}
.. image:: img/draw_dynamic_exported_properties.webp
To draw the arc, we can use the method
:ref:`draw_arc<class_CanvasItem_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:
.. tabs::
.. code-tab:: gdscript GDScript
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)
.. code-tab:: csharp
public override void _Draw()
{
// Average points to get center.
Vector2 center = new Vector2((_point2.X + Point1.X) / 2.0f,
(_point2.Y + Point1.Y) / 2.0f);
// Calculate the rest of the arc parameters.
float radius = Point1.DistanceTo(_point2) / 2.0f;
float startAngle = (_point2 - Point1).Angle();
float endAngle = (Point1 - _point2).Angle();
if (endAngle < 0.0f) // endAngle is likely negative, normalize it.
{
endAngle += Mathf.Tau;
}
// Finally, draw the arc.
DrawArc(center, radius, startAngle, endAngle, 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:
.. image:: img/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.