Benutzerdefinierte Zeichnung in 2D

Warum?

Godot hat Nodes um Sprites, Polygone, Partikel und alles Mögliche zu zeichnen. In den meisten Fällen reicht dies aus, aber nicht immer. Bevor Sie vor Angst, Wut und Verzweiflung weinen, weil ein Node zum Zeichnen dieses bestimmten Etwas nicht existiert ... wäre es gut zu wissen, wie einfach es ist jeden 2D-Node (sei es :ref:`Control <) class_Control> `oder :ref:`Node2D <class_Node2D>`basiert) dazu zu bringen benutzerdefinierte Befehle zu zeichnen. Es ist wirklich einfach dies zu tun.

Aber...

Das manuelle Zeichnen in einem Node ist wirklich nützlich. Hier einige Beispiele warum:

  • Zeichnen von Formen oder Logik, die nicht von Nodes bereitgestellt werden (zum Beispiel: Erstellen eines Nodes der einen Kreis zeichnet, eines Bildes das Spuren auf dem Weg hinterlässt, einer speziellen Art von animiertem Polygon usw.).
  • Visualisierungen, die nicht so kompatibel mit Nodes sind: (Beispiel: eine Tetris-Karte). Das Tetris-Beispiel verwendet eine benutzerdefinierte Zeichenfunktion, um die Blöcke zu zeichnen.
  • Zeichnen einer großen Anzahl einfacher Objekte. Durch benutzerdefinierte Zeichnungen wird der Overhead für die Verwendung von Nodes vermieden, wodurch sich die Speichernutzung verringert und möglicherweise schneller wird.
  • Erstellen eines benutzerdefinierten UI-Steuerelements. Es gibt viele Steuerelemente, aber es ist leicht ein neues, benutzerdefiniertes Steuerelement zu erstellen.

OK, wie?

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

extends Node2D

func _draw():
    # Your draw commands here
    pass
public override void _Draw()
{
    // Your draw commands here
}

Draw commands are described in the CanvasItem class reference. There are plenty of them.

aktualisieren

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 state or something else changed, simply call 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:

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());
    }
}

In some cases, it may be desired to draw every frame. For this, just call update() from the _process() callback, like this:

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();
    }
}

Beispiel: Kreisbögen zeichnen

We will now use the custom drawing functionality of the Godot Engine to draw something that Godot doesn't provide functions for. As an example, Godot provides a 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.

Bogen-Funktion

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.

Grundsätzlich erfordert das Zeichnen einer Form auf dem Bildschirm, dass sie in eine bestimmte Anzahl von Punkten zerlegt wird, die von einem zum nächsten verknüpft sind. Wie Sie sich vorstellen können, wird Ihre Form umso glatter, je mehr Punkte sie enthält, aber desto schlechter wird sie auch in Bezug auf die Prozessorleistung. Wenn Ihre Form sehr groß ist (oder in 3D in der Nähe der Kamera), müssen im Allgemeinen mehr Punkte gezeichnet werden, damit sie nicht eckig aussieht. Im Gegensatz dazu, wenn Ihre Form klein ist (oder in 3D, weit von der Kamera entfernt), können Sie die Anzahl der Punkte verringern, um Prozessorkosten zu sparen. Dies wird als Level of Detail (LOD) bezeichnet. In unserem Beispiel verwenden wir einfach eine feste Anzahl von Punkten, unabhängig vom Radius.

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);
}

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 PoolVector2Array, which is simply an array of Vector2s.

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.

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, to be simple, 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 PoolVector2Array which was previously defined.

Jetzt müssen wir tatsächlich unsere Punkte zeichnen. Wie Sie sich vorstellen können, werden wir nicht einfach unsere 32 Punkte zeichnen, wir müssen auch überall dazwischen zeichnen. Wir hätten jeden Punkt mit der vorherigen Methode selbst berechnen und einzeln nacheinander zeichnen können. Dies ist jedoch zu kompliziert und ineffizient (außer wenn dies ausdrücklich erforderlich ist), sodass wir einfach Linien zwischen jedem Punktepaar ziehen. Wenn der Radius unseres Hilfskreises nicht groß ist, wird die Länge jeder Linie zwischen zwei Punkten niemals lang genug sein, um sie zu sehen. Falls doch, müssten wir lediglich die Anzahl der Punkte erhöhen.

Zeichne den Bogen auf dem Bildschirm

Wir haben jetzt eine Funktion die etwas auf dem Bildschirm zeichnet; es wird Zeit sie innerhalb der``_draw()`` Funktion aufzurufen:

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);
}

Ergebnis:

../../_images/result_drawarc.png

Bogenpolygon-Funktion

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:

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);
}
../../_images/result_drawarc_poly.png

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.

First, we have to make both angle_from and angle_to variables global at the top of our script. Also note that you can store them in other nodes and access them using get_node().

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;
}

We make these values change in the _process(delta) function.

We also increment our angle_from and angle_to values here. However, we must not forget to 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.

Finally, we must not forget to call the update() 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)
    update()
private float Wrap(float value, float minVal, float maxVal)
{
    float f1 = value - minVal;
    float f2 = maxVal - minVal;
    return (f1 % f2) + minVal;
}

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 = Wrap(_angleFrom, 0, 360);
        _angleTo = Wrap(_angleTo, 0, 360);
    }
    Update();
}

Ebenso bitte nicht vergessen die Funktion ``_draw()``zu ändern, damit diese Variablen genutzt werden:

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);
}

Startet das Programm! Es läuft, aber der Bogen dreht sich wahnsinnig schnell! Was ist Falsch?

The reason is that your GPU is actually displaying the frames as fast as it can. We need to "normalize" the drawing by this speed; to achieve that, we have to make use of the delta parameter of the _process() function. delta contains the time elapsed between the two last rendered frames. It is generally small (about 0.0003 seconds, but this depends on your hardware), so using delta to control your drawing ensures that your program runs at the same speed on everybody's hardware.

In our case, we simply need to multiply our rotation_angle variable by delta in the _process() function. This way, our 2 angles will be increased by a much smaller value, which directly depends on the rendering speed.

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();
}

Starte das Programm noch einmal! Diesmal wird die Drehung korrekt angezeigt!

Werkzeuge

Das Zeichnen eigener Nodes kann auch erwünscht sein, wenn Sie diese im Editor ausführen, um sie als Vorschau oder Visualisierung einer Funktion oder eines Verhaltens zu verwenden.

Denken Sie daran, das Schlüsselwort "tool" oben im Skript zu verwenden (überprüfen Sie die Referenz GDScript Grundlagen wenn Sie vergessen haben was dies bewirkt).