GDScript: Eine Einführung in dynamische Programmiersprachen

Über

Dieses Handbuch soll eine kurze Referenz sein um GDScript effektiver nutzen zu können. Es zeigt übliche Beispiele speziell dieser Sprache, enthält aber auch viele Informationen über dynamische Programmiersprachen.

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:

  • Die Sprache kann spielend einfach erlernt werden.
  • Der meiste Code kann schnell und problemlos geschrieben und geändert werden.
  • Weniger geschriebener Code bedeutet weniger mögliche Fehler .
  • Einfacher den Code zu lesen (weniger verwirrend).
  • Es ist keine kompilierung nötig um den Code zu testen.
  • Laufzeit ist gering.
  • Standardmäßig Duck-Typing und polymorph.

Während dies die Nachteile sind:

  • Weniger Leistung als statische Programmiersprachen.
  • Strukturverbesserungen sind schwieriger (Symbole können nicht verfolgt werden)
  • Einige Fehler, die normalerweise zur Kompilierungszeit in statisch typisierten Sprachen erkannt werden, erscheinen nur während der Ausführung des Codes (da die Analyse von Ausdrücken strenger ist).
  • Weniger flexibel zur Code-Vervollständigung (einige Variablentypen sind erst zur Laufzeit bekannt).

In der Realität bedeutet dies, dass Godot + GDScript eine Kombination ist, mit der Spiele schnell und effizient erstellt werden können. Für Spiele, die sehr rechenintensiv sind und nicht von den in der Engine integrierten Tools (wie den Vektortypen, der Physik-Engine, der Mathematikbibliothek usw.) profitieren können, besteht auch die Möglichkeit, C ++ zu verwenden. Auf diese Weise können Sie den größten Teil des Spiels weiterhin in GDScript erstellen und in den Bereichen, in denen eine Leistungssteigerung erforderlich ist, kleine Teile von C ++ hinzufügen.

Variablen & Zuweisungen

In dynamischen Programmiersprachen sind Variablen nicht Typen gebunden. Das bedeutet, dass der Typ nicht festgelegt ist, er wird nur durch Zuweisung von Werten geändert. 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, dass bedeutet sie können mit unterschiedlichen Argumenten aufgerufen werden, 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.

Zeiger & Referenzen:

In statischen Sprachen wie C oder C++ (und einige weitere wie Java und C#), gibt es einen Unterschied zwischen einer Variablen und einem Zeiger 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 eingebauter Typ ist ((int, float, manchmal String) immer ein Zeiger oder eine Referenz. Referenzen werden auch automatisch speicherbereinigt, das bedeutet sie werden gelöscht wenn nicht mehr benötigt. 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 (Wert wird kopiert). Alles andere (Instanzen, Arrays, Wörterbücher usw.) wird als Referenz übergeben. Klassen, die erben Reference (die Standardeinstellung, wenn nichts angegeben ist), werden freigegeben, wenn sie nicht verwendet werden. Die manuelle Speicherverwaltung ist jedoch auch zulässig, wenn sie manuell von Object erbt.

Arrays (Felder)

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] # Simple, and 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 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!")

Wörterbücher

Wörterbücher 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.

Wörterbücher können jeden Wert jedem anderen Wert zuordnen, wobei es völlig egal ist, ob der Datentyp als Schlüssel oder Wert 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 Wörterbücher implementieren.

Beispiel für ein Wörterbuch:

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

Wörterbücher sind auch dynamisch, Schlüssel 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 häufig einfacher mit Wörterbüchern implementiert werden. Hier ist ein einfaches 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))

Wörterbücher können auch als Datenmarkup oder schnelle Strukturen verwendet werden. Während die Wörterbücher von GDScript Python-Wörterbüchern ähneln, unterstützt es auch die Syntax und Indizierung im Lua-Stil, was es nützlich macht, Anfangszustä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

Wiederholen in einigen statisch typisierten Sprachen kann ziemlich komplex sein:

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

[..]

for (int i = 0; i < 50; i++) {

    printf("Value: %s\n", i, strings[i]);
}

// Even in STL:

for (std::list<std::string>::const_iterator it = strings.begin(); it != strings.end(); it++) {

    std::cout << *it << std::endl;
}

Dies ist normalerweise stark vereinfacht in dynamisch typisierten Sprachen:

for s in strings:
    print(s)

Container Datentypen (Arrays und Dictionaries) sind iterierbar, können also auch die Werte der Reihe nach durchgehen. Dictionaries erlauben die Iteration des Schlüssels:

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

Wiederholen mit Indexen ist ebenso möglich:

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

Die range() Funktion nimmt bis zu 3 Argumente an:

range(n) # Will go from 0 to n-1.
range(b, n) # Will go from b to n-1.
range(b, n, s) # Will go from b to n-1, in steps of s.

Hier einige Beispiele statischer Programmiersprachen:

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

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

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

Sind gleichbedeutend mit:

for i in range(10):
    pass

for i in range(5, 10):
    pass

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

Rückwärts durch die Schleife wiederholen kann man mit einem negativen Zähler:

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

Du kannst benutzerdefinierte Iteratoren generieren, wenn die vorgegebenen nicht ganz deinen Anforderungen entsprechen, indem du die _iter_init, _iter_next, und _iter_get Funktionen der Variant Klasse überschreibst. 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.

Stelle sicher, dass der Status des Iterators in _iter_init zurückgesetzt wird, sonst kann es passieren, dass sich verschachtelte for-loops nicht wie gewünscht verhalten.

Duck-Typing

Eines der am schwersten zu erfassenden 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.

Als Beispiel: Stell dir eine Situation vor in der ein Großer Stein einen Tunnel herunter fällt und alles in seinem Weg beiseite stößt. In einer statischen Programmiersprache würde der Code für den Stein ungefähr so aussehen:

void BigRollingRock::on_object_hit(Smashable *entity) {

    entity->smash();
}

Auf diese Weise müsste alles, was von einem Stein zerschlagen werden kann, Smashable erben. Wenn ein Charakter, ein Feind, 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 eine gemeinsame Klasse wie Entity erben. Es wäre jedoch nicht sehr elegant, Entity nur dann eine virtuelle Methode smash() hinzuzufügen, wenn einige 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 ein Vogel wie eine Ente geht, wie eine Ente schwimmt und wie eine Ente quakt, dann ist es 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, vielleicht sollte man es nach Hulk benennen.

Es ist möglich, dass das getroffene Objekt keine smash () -Funktion hat. Einige dynamisch typisierte Sprachen ignorieren einen Methodenaufruf einfach, wenn er nicht vorhanden ist (wie die Objektive Programmiersprache C), aber GDScript ist strenger. Daher ist es wünschenswert, zu überprüfen, ob die Funktion vorhanden ist:

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

Dann wird diese Methode einfach definiert und alles was der Fels dann berührt wird zerstört.