Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Statische Typisierung in GDScript

In dieser Anleitung lernen Sie:

  • wie man die statische Typisierung in GDScript verwendet;

  • dass statische Typen zur Vermeidung von Bugs beitragen können;

  • dass die statische Typisierung Ihren Umgang mit dem Editor verbessert.

Wo und wie Sie dieses neue Sprachfeature verwenden, liegt ganz bei Ihnen: Sie können es nur in einigen kritischen GDScript-Dateien verwenden, es überall verwenden, oder es überhaupt nicht verwenden.

Statische Typen können für Variablen, Konstanten, Funktionen, Parameter und Rückgabetypen verwendet werden.

Ein kurzer Überblick über statische Typisierung

Mit statischer Typisierung kann GDScript mehr Fehler erkennen, ohne dass der Code überhaupt ausgeführt wird. Außerdem geben Typisierungshinweise Ihnen und Ihren Teamkollegen mehr Informationen während der Arbeit, da die Typen der Argumente beim Aufruf einer Methode angezeigt werden. Die statische Typisierung verbessert die Autovervollständigung im Editor und die Dokumentation Ihrer Skripte.

Stellen Sie sich vor, Sie programmieren ein Inventarsystem. Sie programmieren eine Item-Klasse, dann ein Inventory. Um Gegenstände zum Inventar hinzuzufügen, sollten die Leute, die mit Ihrem Code arbeiten, immer ein Item an die Methode Inventory.add() übergeben. Mit Typen können Sie dies erzwingen:

class_name Inventory


func add(reference: Item, amount: int = 1):
    var item := find_item(reference)
    if not item:
        item = _instance_item_from_db(reference)
    item.amount += amount

Statische Typen bieten auch bessere Optionen für die Code-Vervollständigung. Nachfolgend sehen Sie den Unterschied bei Code-Vervollständigung zwischen einer dynamischen und einer statischen Typisierung.

Wahrscheinlich haben Sie schon einmal festgestellt, dass nach einem Punkt keine Vorschläge für die automatische Code-Vervollständigung gemacht werden:

Vervollständigungsoptionen für dynamisch typisierten Code.

Dies ist auf den dynamischen Code zurückzuführen. Godot kann nicht wissen, welchen Werttyp Sie an die Funktion übergeben. Wenn Sie den Typ jedoch explizit angeben, erhalten Sie alle Methoden, Propertys, Konstanten usw. von dem Wert:

Vervollständigungsoptionen für statisch typisierten Code.

Tipp

Wenn Sie statische Typisierung bevorzugen, empfehlen wir Ihnen, die Editoreinstellung Texteditor > Vervollständigung > Typhinweise hinzufügen zu aktivieren. Ziehen Sie auch in Erwägung, einige Warnungen zu aktivieren, die standardmäßig deaktiviert sind.

Außerdem verbessert typisiertes GDScript die Performance durch die Verwendung optimierter Opcodes, wenn die Typen der Operanden/Argumente zur Kompilierungszeit bekannt sind. Für die Zukunft sind weitere GDScript-Optimierungen geplant, z. B. die JIT/AOT-Kompilierung.

Insgesamt bietet die typisierte Programmierung ein besser strukturiertes Erlebnis. Es hilft, Fehler zu vermeiden und verbessert den selbstdokumentierenden Aspekt Ihrer Skripte. Dies ist besonders hilfreich, wenn Sie in einem Team oder an einem langfristigen Projekt arbeiten: Studien haben gezeigt, dass Entwickler die meiste Zeit damit verbringen, den Code anderer Leute oder Skripte zu lesen, die sie in der Vergangenheit geschrieben und vergessen haben. Je klarer und strukturierter der Code ist, desto schneller ist er zu verstehen, desto schneller können Sie vorankommen.

Wie man statische Typisierung einsetzt

Um den Typ einer Variablen, eines Parameters oder einer Konstanten zu definieren, schreiben Sie einen Doppelpunkt nach dem Namen, gefolgt von seinem Typ. Z.B. var health: int. Dadurch bleibt der Typ der Variablen immer derselbe:

var damage: float = 10.5
const MOVE_SPEED: float = 50.0
func sum(a: float = 0.0, b: float = 0.0) -> float:
    return a + b

Godot versucht, die Typen zu ermitteln, wenn Sie einen Doppelpunkt schreiben, aber den Typ: weglassen:

var damage := 10.5
const MOVE_SPEED := 50.0
func sum(a := 0.0, b := 0.0) -> float:
    return a + b

Bemerkung

  1. Es gibt keinen Unterschied zwischen = und := für Konstanten.

  2. Sie brauchen keine Type-Hints für Konstanten zu schreiben, da Godot diese automatisch anhand des zugewiesenen Wertes festlegt. Sie können dies aber trotzdem tun, um die Absicht Ihres Codes zu verdeutlichen. Außerdem ist dies für typisierte Arrays nützlich (wie const A: Array[int] = [1, 2, 3]), da untypisierte Arrays standardmäßig verwendet werden.

Was ein Type-Hint sein kann

Hier finden Sie eine vollständige Liste der Elemente, die als Type-Hint verwendet werden können:

  1. Variant. Beliebiger Typ. In den meisten Fällen ist dies nicht viel anders als eine Deklaration ohne Typisierung, erhöht aber die Lesbarkeit. Als Rückgabetyp zwingt es die Funktion, explizit einen Wert zurückzugeben.

  2. (Nur Rückgabetyp) void. Gibt an, dass die Funktion keinen Wert zurückgibt.

  3. Built-in-Typen.

  4. Native Klassen (Object, Node, Area2D, Camera2D, etc.).

  5. Globale Klassen.

  6. Innere Klassen.

  7. Globale, native und benannte Enums. Beachten Sie, dass ein Enum-Typ nur ein int ist, es gibt keine Garantie, dass der Wert zur Menge der Enum-Werte gehört.

  8. Konstanten (einschließlich lokaler Konstanten), wenn sie eine vorgeladene Klasse oder Enum enthalten.

Sie können jede Klasse, einschließlich Ihrer benutzerdefinierten Klassen, als Typen verwenden. Es gibt zwei Möglichkeiten, sie in Skripten zu verwenden. Die erste Methode besteht darin, das Skript, das Sie als Typ verwenden möchten, in einer Konstante vorzuladen:

const Rifle = preload("res://player/weapons/rifle.gd")
var my_rifle: Rifle

Die zweite Methode ist die Verwendung des Schlüsselwortes class_name beim Erstellen. Für das obige Beispiel würde Ihre rifle.gd wie folgt aussehen:

class_name Rifle
extends Node2D

Wenn Sie class_name verwenden, registriert Godot den Typ Rifle global im Editor, und Sie können ihn überall verwenden, ohne ihn in eine Konstante vorladen zu müssen:

var my_rifle: Rifle

Geben Sie den Rückgabetyp einer Funktion mit dem Pfeil -> an

Um den Rückgabetyp einer Funktion zu definieren, schreiben Sie einen Bindestrich und eine rechteckige Klammer -> nach ihrer Deklaration, gefolgt vom Rückgabetyp:

func _process(delta: float) -> void:
    pass

Der Typ void bedeutet, dass die Funktion nichts zurückgibt. Sie können jeden Typ verwenden, wie bei Variablen:

func hit(damage: float) -> bool:
    health_points -= damage
    return health_points <= 0

Sie können auch Ihre eigenen Klassen als Rückgabetypen verwenden:

# Adds an item to the inventory and returns it.
func add(reference: Item, amount: int) -> Item:
    var item: Item = find_item(reference)
    if not item:
        item = ItemDatabase.get_instance(reference)

    item.amount += amount
    return item

Kovarianz und Kontravarianz

Bei der Vererbung von Basisklassen-Methoden sollten Sie das Liskov-Substitutionsprinzip befolgen.

Kovarianz: Wenn Sie eine Methode erben, können Sie einen Rückgabetyp angeben, der spezifischer ist (Subtyp) als die Parent-Methode.

Kontravarianz: Wenn Sie eine Methode erben, können Sie einen Parametertyp angeben, der weniger spezifisch ist (Supertyp) als die Parent-Methode.

Beispiel:

class_name Parent


func get_property(param: Label) -> Node:
    # ...
class_name Child extends Parent


# `Control` is a supertype of `Label`.
# `Node2D` is a subtype of `Node`.
func get_property(param: Control) -> Node2D:
    # ...

Spezifizieren des Elementtyps eines Arrays

Um den Typ eines Arrays zu definieren, schließen Sie den Typnamen in [] ein.

Der Typ eines Arrays gilt für for-Schleifenvariablen, sowie für einige Operatoren wie [], []= und +. Array-Methoden (wie push_back) und andere Operatoren (wie ==) sind weiterhin untypisiert. Built-in-Typen, native und benutzerdefinierte Klassen und Enums können als Elementtypen verwendet werden. Verschachtelte Array-Typen werden nicht unterstützt.

var scores: Array[int] = [10, 20, 30]
var vehicles: Array[Node] = [$Car, $Plane]
var items: Array[Item] = [Item.new()]
# var arrays: Array[Array] -- disallowed

for score in scores:
    # score has type `int`

# The following would be errors:
scores += vehicles
var s: String = scores[0]
scores[0] = "lots"

Seit Godot 4.2 können Sie auch einen Typ für die Schleifenvariable in einer for-Schleife angeben. Sie können zum Beispiel schreiben:

var names = ["John", "Marta", "Samantha", "Jimmy"]
for name: String in names:
    pass

Das Array bleibt untypisiert, aber die Variable name innerhalb der for-Schleife wird immer vom Typ String sein.

Typ-Casting

Typ-Casting ist ein wichtiges Konzept in typisierten Sprachen. Casting ist die Umwandlung eines Wertes von einem Typ in einen anderen.

Stellen Sie sich einen Enemy in Ihrem Spiel vor, der per extend eine Area2D erweitert. Sie wollen, dass er mit dem Player kollidiert, einem CharacterBody2D mit einem Skript namens PlayerController, das an ihm hängt. Sie benutzen das Signal body_entered, um die Kollision zu erkennen. Mit typisiertem Code wird der Body, den Sie erkennen, ein generischer PhysicsBody2D sein, und nicht Ihr PlayerController beim _on_body_entered-Callback.

Sie können überprüfen, ob dieser PhysicsBody2D Ihr Player ist, indem Sie das as-Schlüsselwort benutzen, und den Doppelpunkt : erneut verwenden, um die Variable zu zwingen, diesen Typ zu benutzen. Dies zwingt die Variable, sich an den PlayerController-Typ zu halten:

func _on_body_entered(body: PhysicsBody2D) -> void:
    var player := body as PlayerController
    if not player:
        return

    player.damage()

Da wir es mit einem benutzerdefinierten Typ zu tun haben, wird die Variable player auf null gesetzt, wenn der body nicht PlayerController erweitert. Wir können dies benutzen, um zu überprüfen, ob der Body der Spieler ist oder nicht. Dank dieses Casts erhalten wir auch eine vollständige Autovervollständigung für die Player-Variable.

Bemerkung

Das as-Schlüsselwort verwandelt die Variable im Falle einer zur Laufzeit stillschweigend in null, wenn der Typ nicht übereinstimmt, ohne dass ein Fehler oder eine Warnung ausgegeben wird. Obwohl dies in einigen Fällen bequem sein kann, kann es auch zu Fehlern führen. Verwenden Sie das Schlüsselwort as nur, wenn dieses Verhalten beabsichtigt ist. Eine sicherere Alternative ist die Verwendung des is Schlüsselwortes:

if not (body is PlayerController):
    push_error("Bug: body is not PlayerController.")

var player: PlayerController = body
if not player:
    return

player.damage()

oder assert() Anweisung:

assert(body is PlayerController, "Bug: body is not PlayerController.")

var player: PlayerController = body
if not player:
    return

player.damage()

Bemerkung

Wenn Sie versuchen, mit einem Built-in-Typ zu casten und dies fehlschlägt, gibt Godot einen Fehler aus.

Sichere Zeilen

Sie können auch Casting verwenden, um sichere Zeilen zu gewährleisten. Sichere Zeilen sind ein Werkzeug, das Ihnen sagt, wann mehrdeutige Codezeilen typsicher sind. Da Sie typisierten und dynamischen Code vermischen können, verfügt Godot manchmal nicht über genügend Informationen, um zu wissen, ob eine Anweisung zur Laufzeit einen Fehler auslösen wird oder nicht.

Dies geschieht, wenn Sie einen Child-Node erhalten. Nehmen wir zum Beispiel einen Timer: mit dynamischem Code können Sie den Node mit $Timer erhalten. GDScript unterstützt duck-typing, d.h. selbst wenn Ihr Timer vom Typ Timer ist, ist er auch ein Node und ein Object, zwei Klassen, die er erweitert. Mit dynamischem GDScript ist der Typ des Nodes auch egal, solange er die Methoden hat, die Sie aufrufen müssen.

Sie können das Casting verwenden, um Godot mitzuteilen, welchen Typ Sie erwarten, wenn Sie einen Node erhalten: ($Timer as Timer), ($Player as CharacterBody2D), usw. Godot überprüft, ob der Typ funktioniert, und wenn ja, wird die Zeilennummer links im Skript-Editor grün angezeigt.

Unsichere vs. sichere Zeile

Unsichere Zeile (Zeile 7) vs. sichere Zeilen (Zeilen 6 und 8)

Bemerkung

Sichere Zeilen bedeuten nicht immer besseren oder zuverlässigeren Code. Siehe die obige Anmerkung über das Schlüsselwort as. Zum Beispiel:

@onready var node_1 := $Node1 as Type1 # Safe line.
@onready var node_2: Type2 = $Node2 # Unsafe line.

Auch wenn die node_2-Deklaration als unsichere Zeile markiert ist, ist sie zuverlässiger als die Node_1-Deklaration. Denn wenn Sie den Node-Typ in der Szene ändern und versehentlich vergessen, ihn im Skript zu ändern, wird der Fehler sofort entdeckt, wenn die Szene geladen wird. Im Gegensatz zu node_1, das stillschweigend zu null gecastet wird und der Fehler erst später entdeckt wird.

Bemerkung

Sie können sichere Zeilen deaktivieren oder ihre Farbe in den Editoreinstellungen ändern.

Typisiert oder dynamisch: Halten Sie sich an einen Stil

Typisiertes GDScript und dynamisches GDScript können im selben Projekt koexistieren. Aus Gründen der Konsistenz in Ihrer Codebasis und für Ihre Kollegen ist es jedoch empfehlenswert, durchgängug einen der beiden Stile zu verwenden. Es ist für alle einfacher, zusammenzuarbeiten, wenn Sie dieselben Richtlinien befolgen, und macht es einfacher und schneller, den Code anderer Leute zu lesen und zu verstehen.

Typisierter Code erfordert ein wenig mehr Schreibarbeit, aber Sie erhalten die oben beschriebenen Vorteile. Hier ist ein Beispiel für dasselbe leere Skript in dynamischem Stil:

extends Node


func _ready():
    pass


func _process(delta):
    pass

Und mit statischer Typisierung:

extends Node


func _ready() -> void:
    pass


func _process(delta: float) -> void:
    pass

Wie Sie sehen können, können Sie auch Typen mit den virtuellen Methoden der Engine verwenden. Signal-Callbacks können, wie alle Methoden, auch Typen verwenden. Hier ist ein body_entered-Signal in dynamischem Stil:

func _on_area_2d_body_entered(body):
    pass

Und dasselbe Callback mit Type-Hints:

func _on_area_entered(area: CollisionObject2D) -> void:
    pass

Warnsystem

Bemerkung

Die ausführliche Dokumentation über das GDScript-Warnsystem wurde nach GDScript Warnungs-System verschoben.

Ab Version 3.1 gibt Godot bereits während des Schreibens Warnungen zu Ihrem Code aus: Die Engine identifiziert Abschnitte Ihres Codes, die zur Laufzeit zu Problemen führen könnten, lässt Ihnen aber die Entscheidung, ob Sie den Code so belassen wollen, wie er ist oder nicht.

Es gibt eine Reihe von Warnungen, die sich speziell an Benutzer von typisiertem GDScript richten. Standardmäßig sind diese Warnungen deaktiviert, Sie können sie in den Projekteinstellungen aktivieren (Debug > GDScript, stellen Sie sicher, dass Erweiterte Einstellungen aktiviert ist).

Sie können die Warnung UNTYPED_DECLARATION aktivieren, wenn Sie immer statische Typen verwenden wollen. Zusätzlich können Sie die Warnung INFERRED_DECLARATION aktivieren, wenn Sie eine lesbarere und zuverlässigere, aber ausführlichere Syntax bevorzugen.

UNSAFE_*-Warnungen machen unsichere Operationen deutlicher als unsichere Zeilen. Derzeit decken UNSAFE_*-Warnungen nicht alle Fälle ab, die von unsicheren Zeilen abgedeckt werden.

Common unsafe operations and their safe counterparts

UNSAFE_PROPERTY_ACCESS and UNSAFE_METHOD_ACCESS warnings

In this example, we aim to set a property and call a method on an object that has a script attached with class_name MyScript and that extends Node2D. If we have a reference to the object as a Node2D (for instance, as it was passed to us by the physics system), we can first check if the property and method exist and then set and call them if they do:

if "some_property" in node_2d:
    node_2d.some_property = 20  # Produces UNSAFE_PROPERTY_ACCESS warning.

if node_2d.has_method("some_function"):
    node_2d.some_function()  # Produces UNSAFE_METHOD_ACCESS warning.

However, this code will produce UNSAFE_PROPERTY_ACCESS and UNSAFE_METHOD_ACCESS warnings as the property and method are not present in the referenced type - in this case a Node2D. To make these operations safe, you can first check if the object is of type MyScript using the is keyword and then declare a variable with the type MyScript on which you can set its properties and call its methods:

if node_2d is MyScript:
    var my_script: MyScript = node_2d
    my_script.some_property = 20
    my_script.some_function()

Alternatively, you can declare a variable and use the as operator to try to cast the object. You'll then want to check whether the cast was successful by confirming that the variable was assigned:

var my_script := node_2d as MyScript
if my_script != null:
    my_script.some_property = 20
    my_script.some_function()

UNSAFE_CAST warning

In this example, we would like the label connected to an object entering our collision area to show the area's name. Once the object enters the collision area, the physics system sends a signal with a Node2D object, and the most straightforward (but not statically typed) solution to do what we want could be achieved like this:

func _on_body_entered(body: Node2D) -> void:
    body.label.text = name  # Produces UNSAFE_PROPERTY_ACCESS warning.

This piece of code produces an UNSAFE_PROPERTY_ACCESS warning because label is not defined in Node2D. To solve this, we could first check if the label property exist and cast it to type Label before settings its text property like so:

func _on_body_entered(body: Node2D) -> void:
    if "label" in body:
        (body.label as Label).text = name  # Produces UNSAFE_CAST warning.

However, this produces an UNSAFE_CAST warning because body.label is of a Variant type. To safely get the property in the type you want, you can use the Object.get() method which returns the object as a Variant value or returns null if the property doesn't exist. You can then determine whether the property contains an object of the right type using the is keyword, and finally declare a statically typed variable with the object:

func _on_body_entered(body: Node2D) -> void:
    var label_variant: Variant = body.get("label")
    if label_variant is Label:
        var label: Label = label_variant
        label.text = name

Fälle, in denen Sie keine Typen spezifizieren können

Zum Abschluss dieser Einführung wollen wir noch die Fälle erwähnen, in denen Sie keine Type-Hints verwenden können. Dies führt zu einem Syntaxfehler.

  1. Sie können den Typ der einzelnen Elemente in einem Array oder einem Dictionary nicht angeben:

    var enemies: Array = [$Goblin: Enemy, $Zombie: Enemy]
    var character: Dictionary = {
        name: String = "Richard",
        money: int = 1000,
        inventory: Inventory = $Inventory,
    }
    
  2. Verschachtelte Typen werden derzeit nicht unterstützt:

    var teams: Array[Array[Character]] = []
    
  3. Typisierte Dictionarys werden derzeit nicht unterstützt:

    var map: Dictionary[Vector2i, Item] = {}
    

Zusammenfassung

Typisiertes GDScript ist ein leistungsstarkes Werkzeug. Es hilft Ihnen, strukturierteren Code zu schreiben, häufige Fehler zu vermeiden und skalierbare und zuverlässige Systeme zu erstellen. Statische Typen verbessern die Leistung von GDScript, und weitere Optimierungen sind für die Zukunft geplant.