Custom drawing in 2D
Introduction
something does not exist… it would be good to know that it is possible to easily make any 2D node (be it Control or Node2D based) draw custom commands. It is really easy to do it, too. 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.
- Visualizations that are not that compatible with nodes, such as a tetris board. (The tetris example uses a custom draw function to draw the blocks.)
- Drawing a large number of simple objects. 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
CanvasItem derived node, like Control or Node2D. Then override the_draw()function. GDScript C#extends Node2Dfunc _draw(): # Your draw commands here pass
CanvasItem class reference. There are plenty of them.public override void _Draw(){ // Your draw commands here}
Updating
_draw()function is only called once, and then the draw commands are cached and remembered, so further calls are unnecessary. CanvasItem.update() in that same node and a new_draw()call will happen. Here is a little more complex example, a texture variable that will be redrawn if modified: GDScript C#extends Node2Dexport (Texture) var texture setget _set_texturefunc _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()); }}
update()from the_process()callback, like this: GDScript C#extends Node2Dfunc _draw(): # Your draw commands here passfunc _process(delta): update()
public class CustomNode2D : Node2D{ public override void _Draw() { // Your draw commands here } public override void _Process(float delta) { Update(); }}
An example: drawing circular arcs
draw_circle()function that draws a whole circle. However, what about drawing a portion of a circle? You will have to code a function to perform this and draw it yourself.Arc function
An arc is defined by its support circle parameters, that is, the center position and the radius. The arc itself is then defined by the angle it starts from and the angle at which it stops. These are the 4 arguments that we have to provide to our drawing function. We’ll also provide the color value, so we can draw the arc in different colors if we wish. Level of Detail (LOD). In our example, we will simply use a fixed number of points, no matter the radius. GDScript C#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);}
nb_pointsvariable to a value of32. Then, we initialize an emptyPoolVector2Array, which is simply an array ofVector2s. The next step consists of computing the actual positions of these 32 points that compose an arc. This is done in the first for-loop: we iterate over the number of points for which we want to compute the positions, plus one to include the last point. We first determine the angle of each point, between the starting and ending angles.cos()andsin()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.angle(in radians) is given byVector2(cos(angle), sin(angle)). Sincecos()andsin()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 ofradius, we simply need to multiply the position byradius. Finally, we need to position our support circle at thecenterposition, which is performed by adding it to ourVector2value. Finally, we insert the point in thePoolVector2Arraywhich was previously defined. Now, we need to actually draw our points. As you can imagine, we will not simply draw our 32 points: we need to draw everything that is between each of them. We could have computed every point ourselves using the previous method, and drew it one by one. But this is too complicated and inefficient (except if explicitly needed), so we simply draw lines between each pair of points. Unless the radius of our support circle is big, the length of each line between a pair of points will never be long enough to see them. If that were to happen, we would simply need to increase the number of points.Draw the arc on the screen
_draw()function: GDScript C#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)
Result: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);}
Arc polygon function
We can take this a step further and not only write a function that draws the plain portion of the disc defined by the arc, but also its shape. The method is exactly the same as before, except that we draw a polygon instead of lines: GDScript C#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);}
Dynamic custom drawing
All right, we are now able to draw custom stuff on the screen. However, it is static; let’s make this shape turn around the center. The solution to do this is simply to change the angle_from and angle_to values over time. For our example, we will simply increment them by 50. This increment value has to remain constant or else the rotation speed will change accordingly.get_node(). GDScript C#extends Node2Dvar rotation_angle = 50var angle_from = 75var angle_to = 195
We make these values change in the _process(delta) function.public class CustomNode2D : Node2D{ private float _rotationAngle = 50; private float _angleFrom = 75; private float _angleTo = 195;}
wrap()the resulting values between 0 and 360°! That is, if the angle is 361°, then it is actually 1°. If you don’t wrap these values, the script will work correctly, but the angle values will grow bigger and bigger over time until they reach the maximum integer value Godot can manage (2^31 - 1). When this happens, Godot may crash or produce unexpected behavior.update()function, which automatically calls_draw(). This way, you can control when you want to refresh the frame. GDScript C#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();}
_draw()function to make use of these variables: GDScript C#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 )
Let’s run! It works, but the arc is rotating insanely fast! What’s wrong?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);}
deltaparameter of the_process()function.deltacontains the time elapsed between the two last rendered frames. It is generally small (about 0.0003 seconds, but this depends on your hardware), so usingdeltato control your drawing ensures that your program runs at the same speed on everybody’s hardware.rotation_anglevariable bydeltain the_process()function. This way, our 2 angles will be increased by a much smaller value, which directly depends on the rendering speed. GDScript C#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()
Let’s run again! This time, the rotation displays fine!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();}
Antialiased drawing
draw_line to enable antialiasing, but it doesn’t work reliably in all situations (for instance, on mobile/web platforms, or when HDR is enabled). There is also noantialiasedparameter available in draw_polygon. Antialiased Line2D add-on (which also supports antialiased Polygon2D drawing). Note that this add-on relies on high-level nodes, rather than low-level_draw()functions.Tools
Running code in the editor for more information.
