2D benutzerdefiniertes zeichnen

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?

Fügen Sie ein Skript zu einem beliebigen von CanvasItem abgeleiteten Node hinzu, wie zum Beispiel Control oder: ref: Node2D <class_Node2D>. Überschreiben Sie dann die Funktion ``_draw() ``.

extends Node2D

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

Es gibt viele Zeichenbefehle, die alle in der Klassenreferenz CanvasItem beschrieben werden.

aktualisieren

Die Funktion ``_draw() `` wird nur einmal aufgerufen , womit dann die Zeichenbefehle gecached und zwischengespeichert werden, sodass weitere Aufrufe nicht erforderlich sind.

Wenn ein erneutes Zeichnen erforderlich ist, weil sich ein Status oder etwas anderes geändert hat, rufen Sie einfach CanvasItem.update() in demselben Node auf und ein neuer ``_draw() `` Aufruf wird ausgeführt.

Hier ist ein etwas komplexeres Beispiel, eine Texturvariable, die bei Änderung neu gezeichnet wird:

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 einigen Fällen kann es wünschenswert sein jedes Frame zu zeichnen. Rufen Sie dazu einfach ``update() `` aus dem ``_process() `` Rückruf auf:

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

Wir werden jetzt die benutzerdefinierte Zeichenfunktion der Godot Engine verwenden um etwas zu zeichnen für das Godot keine Funktionen bereitstellt. Als Beispiel bietet Godot eine Funktion draw_circle() die einen ganzen Kreis zeichnet. Was ist jedoch mit dem Zeichnen eines Teils eines Kreises? Sie müssen eine Funktion schreiben um dies auszuführen und selbst zeichnen.

Bogen-Funktion

Ein Bogen wird durch seine Kreisparameter definiert, d.h. den Mittelpunkt und den Radius. Der Bogen selbst wird dann durch den Winkel definiert bei dem er beginnt und durch den Winkel bei dem er stoppt. Dies sind die 4 Argumente, die wir für unsere Zeichenfunktion angeben müssen. Wir geben auch den Farbwert an, damit wir den Bogen auf Wunsch in verschiedenen Farben zeichnen können.

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

Erinnern Sie sich an die Anzahl der Punkte, in die unsere Form zerlegt werden muss? Wir haben diese Zahl in der Variablen nb_points auf einen Wert von 32 festgelegt. Dann initialisieren wir ein leeres PoolVector2Array das einfach ein Array von Vector2 ist.

Der nächste Schritt besteht darin die tatsächlichen Positionen dieser 32 Punkte zu berechnen, aus denen ein Bogen besteht. Dies geschieht in der ersten for-Schleife: Wir iterieren über die Anzahl der Punkte, für die wir die Positionen berechnen möchten, plus einen Punkt, der den letzten Punkt enthält. Wir bestimmen zuerst den Winkel jedes Punktes zwischen dem Start- und dem Endwinkel.

Der Grund, warum jeder Winkel um 90° verringert wird besteht darin, dass wir aus jedem Winkel mithilfe der Trigonometrie 2D-Positionen berechnen (Sie wissen, Sinus und Cosinus ...). Um es einfacher zu machen, verwenden ``sin() `` und ``cos() `` Bogenmaß, nicht Grad. Der Winkel von 0° (0 Bogenmaß) beginnt bei 3 Uhr, obwohl wir bei 12 Uhr mit dem Zählen beginnen möchten. Also verringern wir jeden Winkel um 90° um bei 12 Uhr mit dem Zählen zu beginnen.

Die tatsächliche Position eines Punktes auf einem Kreis im Winkel angle (im Bogenmaß) wird durch Vector2(cos(angle), sin(angle)) angegeben. Da sin() und cos() Werte zwischen -1 und 1 zurückliefern, befindet sich die Position auf einem Kreis mit Radius 1. Um diese Position auf unserem Hilfskreis zu haben, der einen Radius von radius hat, müssen wir einfach die Position mit radius multiplizieren. Schließlich müssen wir unseren Hilfskreis an der Position center positionieren, indem wir ihn zu unserem Wert Vector2 hinzufügen. Schließlich fügen wir den Punkt in das zuvor definierte PoolVector2Array ein.

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

Wir können noch einen Schritt weiter gehen und nicht nur eine Funktion schreiben, die den durch den Bogen definierten Teil der Scheibe zeichnet, sondern auch deren gesamte Form. Die Methode ist genau die gleiche wie zuvor, außer dass wir anstelle von Linien ein Polygon zeichnen:

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

Dynamische benutzerdefinierte Zeichnung

In Ordnung, wir können jetzt benutzerdefinierte Inhalte auf dem Bildschirm zeichnen. Es ist jedoch statisch; Lassen Sie uns diese Form um den Mittepunkt drehen. Die Lösung hierfür besteht einfach darin, die Werte angle_from und angle_to im Laufe der Zeit zu ändern. In unserem Beispiel erhöhen wir sie einfach um 50. Dieser Inkrementwert muss konstant bleiben, sonst ändert sich die Drehzahl entsprechend.

Zuerst müssen wir die Variablen angle_from und angle_to oben in unserem Skript global anlegen. Beachten Sie auch, dass Sie sie in anderen Nodes speichern und mit get_node() darauf zugreifen können.

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

Wir ändern diese Werte in der Funktion _process(delta).

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).