2D benutzerdefiniertes zeichnen

Einführung

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.

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.

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

Zeichnen

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

extends Node2D

func _draw():
    # Your draw commands here
    pass

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.

If re-drawing is required because a state or something else changed, call CanvasItem.update() in that same node and a new _draw() call will happen.

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

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

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)

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)

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

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

Wir erhöhen hier auch unsere Werte angle_from und angle_to. Wie dem auch sei, wir dürfen jedoch nicht vergessen, die resultierenden Werte zwischen 0 und 360 ° zu begrenzen (wrap())! Das heißt, wenn der Winkel 361 ° beträgt, ist er tatsächlich 1 °. Wenn Sie diese Werte nicht begrenzen, funktioniert das Skript trotzdem korrekt, aber die Winkelwerte werden mit der Zeit immer größer, bis sie den maximalen ganzzahligen Wert erreichen, den Godot verwalten kann (2 ^ 31 - 1). In diesem Fall kann Godot abstürzen oder unerwartetes Verhalten hervorrufen.

Schließlich dürfen wir nicht vergessen, die Funktion update() aufzurufen, die automatisch _draw() aufruft. Auf diese Weise können Sie steuern, wann Sie den Frame aktualisieren möchten.

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

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 )

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

Der Grund ist, dass Ihre GPU die Frames tatsächlich so schnell wie möglich anzeigt. Wir müssen das Zeichnen mit dieser Geschwindigkeit "normalisieren"; Um dies zu erreichen, müssen wir den Parameter delta der Funktion _process() verwenden. delta enthält die Zeit, die zwischen den beiden zuletzt gerenderten Frames vergangen ist. Es ist im Allgemeinen klein (ungefähr 0,0003 Sekunden, dies hängt jedoch von Ihrer Hardware ab). Wenn Sie also delta zur Steuerung für das Zeichnen verwenden, stellen Sie sicher, dass Ihr Programm auf der Hardware aller Benutzer mit der gleichen Geschwindigkeit ausgeführt wird.

In unserem Fall müssen wir einfach unsere Variable rotation_angle mit delta in der Funktion _process() multiplizieren. Auf diese Weise werden unsere 2 Winkel um einen viel kleineren Wert vergrößert, der direkt von der Rendergeschwindigkeit abhängt.

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

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

Antialiased drawing

Godot offers method parameters in 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 no antialiased parameter available in draw_polygon.

As a workaround, install and use the 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.

Werkzeuge

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. See Code im Editor ausführen for more information.