Up to date

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

GDScript: Eine Einführung in dynamische Programmiersprachen

Über

Dieses Tutorial soll eine kurze Referenz sein um GDScript effektiver nutzen zu können. Er konzentriert sich auf allgemeine, sprachspezifische Fälle, enthält aber auch viele Informationen über dynamisch typisierte Sprachen.

Dies ist besonders nützlich für Programmierer, die bisher nur wenig oder keine Erfahrung mit dynamischen Programmiersprachen haben.

Eigenschaften dynamischer Programmiersprachen

Vor- und Nachteile von dynamischer Typisierung

GDScript ist eine dynamische Programmiersprache und hat damit folgende Vorteile:

  • Der Einstieg in die Sprache ist einfach.

  • Der meiste Code kann schnell und problemlos geschrieben und geändert werden.

  • Weniger geschriebener Code bedeutet weniger mögliche Fehler .

  • Der Code ist leicht zu lesen (wenig Ballast).

  • Es ist keine Kompilierung nötig, um den Code zu testen.

  • Laufzeit ist gering.

  • Sie hat von Natur aus Duck-Typing und Polymorphismus.

Die größten Nachteile sind:

  • Geringere Performance als statische Programmiersprachen.

  • Refactoring ist schwieriger (Symbole können nicht zurückverfolgt werden).

  • Einige Fehler, die normalerweise zur Kompilierungszeit in statisch typisierten Sprachen erkannt werden, erscheinen nur während der Ausführung des Codes (da das Parsen von Ausdrücken strenger ist).

  • Weniger flexibel für Code-Vervollständigung (einige Variablentypen sind erst zur Laufzeit bekannt).

In der Realität bedeutet dies, dass Godot in Verbindung mit GDScript eine Kombination darstellt, mit der sich Spiele schnell und effizient erstellen lassen. Für Spiele, die sehr rechenintensiv sind und nicht von den integrierten Werkzeugen der Engine profitieren können (wie die Vektortypen, die Physik-Engine, die mathematische Bibliothek usw.), gibt es auch die Möglichkeit, C++ zu verwenden. So können Sie den größten Teil des Spiels in GDScript erstellen und in den Bereichen, in denen eine Performance-Steigerung erforderlich ist, kleine Teile von C++ hinzufügen.

Variablen & Zuweisungen

Alle Variablen in einer dynamisch typisierten Sprache sind "Varianten"-artig. Das bedeutet, dass ihr Typ nicht festgelegt ist, sondern nur durch Zuweisung geändert wird. Beispiel:

Statisch:

int a; // Value uninitialized.
a = 5; // This is valid.
a = "Hi!"; // This is invalid.

Dynamisch:

var a # 'null' by default.
a = 5 # Valid, 'a' becomes an integer.
a = "Hi!" # Valid, 'a' changed to a string.

Als Funktions-Argumente:

Funktionen sind ebenfalls dynamisch, was bedeutet, dass sie mit unterschiedlichen Argumenten aufgerufen werden können, zum Beispiel:

Statisch:

void print_value(int value) {

    printf("value is %i\n", value);
}

[..]

print_value(55); // Valid.
print_value("Hello"); // Invalid.

Dynamisch:

func print_value(value):
    print(value)

[..]

print_value(55) # Valid.
print_value("Hello") # Valid.

Pointer & Referenzen:

In statischen Sprachen wie C oder C++ (und einigen weiteren wie Java und C#), gibt es einen Unterschied zwischen einer Variablen und einem Pointer bzw. Referenz auf eine Variable. Dies erlaubt dem Objekt, durch andere Funktionen verändert zu werden, durch Übergabe einer Referenz auf das Original.

In C# oder Java ist alles, was kein Built-in-Typ ist (int, float, manchmal String) immer ein Pointer oder eine Referenz. Referenzen werden auch automatisch vom Garbage Collector bereinigt: Sie werden gelöscht werden, wenn sie nicht mehr benötigt werden. Dynamische Sprachen bieten meistens ebenfalls dieses Model. Hier einige Beispiele:

  • C++:

void use_class(SomeClass *instance) {

    instance->use();
}

void do_something() {

    SomeClass *instance = new SomeClass; // Created as pointer.
    use_class(instance); // Passed as pointer.
    delete instance; // Otherwise it will leak memory.
}
  • Java:

@Override
public final void use_class(SomeClass instance) {

    instance.use();
}

public final void do_something() {

    SomeClass instance = new SomeClass(); // Created as reference.
    use_class(instance); // Passed as reference.
    // Garbage collector will get rid of it when not in
    // use and freeze your game randomly for a second.
}
  • GDScript:

func use_class(instance): # Does not care about class type
    instance.use() # Will work with any class that has a ".use()" method.

func do_something():
    var instance = SomeClass.new() # Created as reference.
    use_class(instance) # Passed as reference.
    # Will be unreferenced and deleted.

In GDScript werden nur Basistypen (int, float, String und die Vektortypen) als Wert an Funktionen übergeben (der Wert wird kopiert). Alles andere (Instanzen, Arrays, Dictionarys, etc.) wird als Referenz übergeben. Klassen, die von RefCounted erben (der Default, wenn nichts angegeben ist), werden freigegeben, wenn sie nicht verwendet werden, aber manuelle Speicherverwaltung ist ebenfalls erlaubt, wenn sie manuell von Object erben.

Arrays

Arrays in dynamischen Programmiersprachen können viele verschiedene Datentypen enthalten und sind immer dynamisch (können also jederzeit in der Größe verändert werden). Vergleichen wir hierzu Arrays in statischen Programmiersprachen:

int *array = new int[4]; // Create array.
array[0] = 10; // Initialize manually.
array[1] = 20; // Can't mix types.
array[2] = 40;
array[3] = 60;
// Can't resize.
use_array(array); // Passed as pointer.
delete[] array; // Must be freed.

// or

std::vector<int> array;
array.resize(4);
array[0] = 10; // Initialize manually.
array[1] = 20; // Can't mix types.
array[2] = 40;
array[3] = 60;
array.resize(3); // Can be resized.
use_array(array); // Passed reference or value.
// Freed when stack ends.

Und in GDScript:

var array = [10, "hello", 40, 60] # You can mix types.
array.resize(3) # Can be resized.
use_array(array) # Passed as reference.
# Freed when no longer in use.

In dynamisch typisierten Sprachen können Arrays auch als andere Datentypen, wie etwa Listen, verwendet werden:

var array = []
array.append(4)
array.append(5)
array.pop_front()

Oder unsortierte Sets:

var a = 20
if a in [10, 20, 30]:
    print("We have a winner!")

Dictionarys

Dictionarys sind ein leistungsstarkes Werkzeug in dynamisch typisierten Sprachen. Die meisten Programmierer, die aus statisch typisierten Sprachen (wie C++ oder C#) stammen, ignorieren ihre Existenz und erschweren ihr Leben unnötig. Dieser Datentyp ist in solchen Sprachen im Allgemeinen nicht (oder nur in begrenzter Form) vorhanden.

Dictionarys können jeden Wert jedem anderen Wert zuordnen, wobei es völlig egal ist, ob der Datentyp als Key oder Value genutzt wird. Entgegen der landläufigen Meinung sind sie effizient, da sie mit Hash-Tabellen implementiert werden können. Sie sind in der Tat so effizient, dass einige Sprachen sogar Arrays als Dictionaries implementieren.

Beispiel für ein Dictionary:

var d = {"name": "John", "age": 22}
print("Name: ", d["name"], " Age: ", d["age"])

Dictionarys sind auch dynamisch, Keys können jederzeit mit geringen Aufwand hinzugefügt oder entfernt werden:

d["mother"] = "Rebecca" # Addition.
d["age"] = 11 # Modification.
d.erase("name") # Removal.

In den meisten Fällen können zweidimensionale Arrays einfacher mit Dictionarys implementiert werden. Hier ist ein Beispiel für ein "Schiffe Versenken"-Spiel:

# Battleship Game

const SHIP = 0
const SHIP_HIT = 1
const WATER_HIT = 2

var board = {}

func initialize():
    board[Vector2(1, 1)] = SHIP
    board[Vector2(1, 2)] = SHIP
    board[Vector2(1, 3)] = SHIP

func missile(pos):
    if pos in board: # Something at that position.
        if board[pos] == SHIP: # There was a ship! hit it.
            board[pos] = SHIP_HIT
        else:
            print("Already hit here!") # Hey dude you already hit here.
    else: # Nothing, mark as water.
        board[pos] = WATER_HIT

func game():
    initialize()
    missile(Vector2(1, 1))
    missile(Vector2(5, 8))
    missile(Vector2(2, 3))

Dictionarys können auch als Datenmarkup oder schnelle Strukturen verwendet werden. Während die Dictionarys von GDScript Python-Dictionarys ähneln, unterstützt es auch die Syntax und Indizierung im Lua-Stil, was es nützlich macht, Inititalzustände und schnelle Strukturen zu schreiben:

# Same example, lua-style support.
# This syntax is a lot more readable and usable.
# Like any GDScript identifier, keys written in this form cannot start
# with a digit.

var d = {
    name = "John",
    age = 22
}

print("Name: ", d.name, " Age: ", d.age) # Used "." based indexing.

# Indexing

d["mother"] = "Rebecca"
d.mother = "Caroline" # This would work too to create a new key.

For & while

Die Iteration mit der C-ähnlichen for-Schleife in von C abgeleiteten Sprachen kann recht komplex sein:

const char** strings = new const char*[50];

[..]

for (int i = 0; i < 50; i++) {
            printf("Value: %c Index: %d\n", strings[i], i);
    }

// Even in STL:
std::list<std::string> strings;

[..]

    for (std::string::const_iterator it = strings.begin(); it != strings.end(); it++) {
            std::cout << *it << std::endl;
    }

Aus diesem Grund hat GDScript sich entschieden, stattdessen eine for-in-Schleife über Iterables zu verwenden:

for s in strings:
    print(s)

Container-Datentypen (Arrays und Dictionarys) sind iterierbar. Dictionarys erlauben die Iteration über ihre Keys:

for key in dict:
    print(key, " -> ", dict[key])

Die Iteration mit Indizes ist ebenfalls möglich:

for i in range(strings.size()):
    print(strings[i])

Die range()-Funktion kann bis zu 3 Argumente annehmen:

range(n) # Will count from 0 to n in steps of 1. The parameter n is exclusive.
range(b, n) # Will count from b to n in steps of 1. The parameters b is inclusive. The parameter n is exclusive.
range(b, n, s) # Will count from b to n, in steps of s. The parameters b is inclusive. The parameter n is exclusive.

Einige Beispiele mit for-Schleifen im C-Stil:

for (int i = 0; i < 10; i++) {}

for (int i = 5; i < 10; i++) {}

for (int i = 5; i < 10; i += 2) {}

Entsprechen:

for i in range(10):
    pass

for i in range(5, 10):
    pass

for i in range(5, 10, 2):
    pass

Und die Rückwärtsschleife wird durch einen negativen Zähler ausgeführt:

for (int i = 10; i > 0; i--) {}

Wird zu:

for i in range(10, 0, -1):
    pass

While

while()-Schleifen sind überall gleich:

var i = 0

while i < strings.size():
    print(strings[i])
    i += 1

Benutzerdefinierte Iteratoren

Sie können benutzerdefinierte Iteratoren generieren, wenn die vorgegebenen nicht ganz Ihren Anforderungen entsprechen, indem Sie die _iter_init, _iter_next, und _iter_get-Funktionen der Variant-Klasse in Ihrem Skript überschreiben. Hier ist ein Beispiel einer Implementierung für einen Vorwärts-Iterator:

class ForwardIterator:
    var start
    var current
    var end
    var increment

    func _init(start, stop, increment):
        self.start = start
        self.current = start
        self.end = stop
        self.increment = increment

    func should_continue():
        return (current < end)

    func _iter_init(arg):
        current = start
        return should_continue()

    func _iter_next(arg):
        current += increment
        return should_continue()

    func _iter_get(arg):
        return current

Und er kann wie jeder andere Iterator verwendet werden:

var itr = ForwardIterator.new(0, 6, 2)
for i in itr:
    print(i) # Will print 0, 2, and 4.

Stellen Sie sicher, dass Sie den Zustand des Iterators in _iter_init zurücksetzen, da sonst verschachtelte for-Schleifen, die benutzerdefinierte Iteratoren verwenden, nicht wie erwartet funktionieren.

Duck-Typing

Eines der am schwersten zu greifenden Konzepte, wenn man von einer statisch typisierten zu einer dynamisch typisierten Sprache wechselt, ist Duck-Typing. Duck-Typing macht allgemeines Code-Design viel leichter und unkomplizierter zu schreiben, aber es ist vielleicht nicht gleich ersichtlich, wie es funktioniert.

Stellen Sie sich zum Beispiel eine Situation vor, in der ein großer Felsen einen Tunnel hinunterfällt und alles auf seinem Weg zertrümmert. Der Code für den Felsen würde in einer statisch typisierten Sprache etwa so aussehen:

void BigRollingRock::on_object_hit(Smashable *entity) {

    entity->smash();
}

Auf diese Weise müsste alles, was von einem Stein zerschlagen werden kann, von Smashable erben. Wenn ein Charakter, ein Gegner, ein Möbelstück oder ein kleiner Stein zerschmettert werden könnten, müssten sie von der Klasse Smashable erben, was möglicherweise eine Mehrfachvererbung erfordert. Wenn Mehrfachvererbung unerwünscht ist, müssten sie von einer gemeinsame Klasse wie Entity erben. Es wäre jedoch nicht sehr elegant, zu Entity nur dann eine virtuelle Methode smash() hinzuzufügen, wenn nur wenige von ihnen zerschlagen werden können.

Bei dynamischen Programmiersprachen ist dies kein Problem. Duck-Typing stellt sicher, dass man nur an benötigter Stelle eine smash()-Funktion definieren muss, und das war's. Vererbung, Basisklassen usw. müssen nicht berücksichtigt werden.

func _on_object_hit(object):
    object.smash()

Und das ist es auch schon. Falls das Objekt, dass den großen Felsen trifft, eine smash() Methode hat, wird diese aufgerufen. Vererbung oder Polymorphie ist nicht nötig. Dynamischen Programmiersprachen ist es nur wichtig, dass die Instanz die gewünschte Methode oder Member hat, nicht was sie vererbt oder deren Klassentyp. Die Definition von "Duck-Typing" (Konzept der objektorientierten Programmierung) macht das deutlicher:

"Wenn ich einen Vogel sehe, der wie eine Ente läuft, wie eine Ente schwimmt und wie eine Ente quakt, nenne ich diesen Vogel eine Ente."

In diesem Fall lautet es übersetzt:

"Falls das Objekt zerstört werden kann, ist es egal was es ist, zerstöre es einfach"

Ja, wir sollten es stattdessen Hulk-Typing nennen.

Es ist möglich, dass das zu treffende Objekt keine smash()-Funktion hat. Einige dynamisch typisierte Sprachen ignorieren einfach einen Methodenaufruf, wenn er nicht existiert, aber GDScript ist strenger, so dass eine Überprüfung, ob die Funktion existiert, wünschenswert ist:

func _on_object_hit(object):
    if object.has_method("smash"):
        object.smash()

Dann definieren Sie einfach diese Methode und alles, was der Stein berührt, kann zerschlagen werden.