GDScript: Wprowadzenie do języków dynamicznych

O silniku

This tutorial aims to be a quick reference for how to use GDScript more efficiently. It focuses on common cases specific to the language, but also covers a lot of information on dynamically typed languages.

Ma to być szczególnie przydatne dla programistów, którzy nie mają doświadczenia z dynamicznie typowanymi językami lub mają niewielkie doświadczenie w posługiwaniu się nimi.

Dynamiczna natura

Zalety i wady dynamicznie typowanego języka

GDScript jest dynamicznie typowanym językiem. Jego głównymi zaletami są:

  • Jest to prosty język i łatwy do nauki.
  • Większość kodu można zapisywane i zmieniane, szybko i bez kłopotów.
  • Mniej kodu do napisania oznacza mniej błędów i pomyłek do naprawy.
  • Łatwiejszy do czytania kod.
  • Nie potrzeba kompilacji do testowania.
  • Jest niewielki.
  • Duck-typing and polymorphism by nature.

Podczas gdy głównymi wadami są:

  • Mniejsza wydajność niż statycznie typowane języki.
  • Trudnejsze do sprawdzania (symbole nie mogą być śledzone)
  • Niektóre błędy, które zwykle są wykrywane w czasie kompilacji w statycznie wpisanych językach, pojawiają się tylko podczas uruchamiania kodu (ponieważ parsowanie wyrażeń jest bardziej rygorystyczne).
  • Mniejsza elastyczność przy kodowaniu (niektóre typy zmiennych są znane tylko w czasie pracy programu).

This, translated to reality, means that Godot+GDScript are a combination designed to create games quickly and efficiently. For games that are very computationally intensive and can’t benefit from the engine built-in tools (such as the Vector types, Physics Engine, Math library, etc), the possibility of using C++ is present too. This allows you to still create most of the game in GDScript and add small bits of C++ in the areas that need a performance boost.

Zmienne i przypisania

All variables in a dynamically typed language are „variant”-like. This means that their type is not fixed, and is only modified through assignment. Example:

Statyczny:

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

Dynamiczny:

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

Jako argumenty funkcji:

Funkcje mają również charakter dynamiczny, co oznacza, że można je wywoływać za pomocą różnych argumentów, przykład użycia:

Statyczny:

void print_value(int value) {

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

[..]

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

Dynamiczny:

func print_value(value):
    print(value)

[..]

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

Wskaźniki i odniesienia:

In static languages, such as C or C++ (and to some extent Java and C#), there is a distinction between a variable and a pointer/reference to a variable. The latter allows the object to be modified by other functions by passing a reference to the original one.

In C# or Java, everything not a built-in type (int, float, sometimes String) is always a pointer or a reference. References are also garbage-collected automatically, which means they are erased when no longer used. Dynamically typed languages tend to use this memory model, too. Some Examples:

  • 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

W GDScript tylko typy bazowe (int, float, string i typy wektorowe) są przekazywane przez wartość do funkcji (wartość jest kopiowana). Wszystko inne (instancje, tablice, słowniki itp.) jest przekazywane jako odniesienie. Klasy odziedziczone class_Reference (domyślne, jeśli nie podano nic) zostaną zwolnione, jeśli nie są używane, ale ręczne zarządzanie pamięcią jest również dozwolone, jeśli dziedziczenie odbywa się ręcznie po class_Object.

Tablice

Tablice w dynamicznie typowanych językach mogą zawierać wiele różnych mieszanych typów danych i są zawsze dynamiczne (można je zmieniać w dowolnym momencie). Porównaj np. tablice w statycznie wpisanych językach:

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

I w 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 dynamically typed languages, arrays can also double as other datatypes, such as lists:

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

Lub nieuporządkowane zestawy:

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

Słowniki

Dictionaries are a powerful tool in dynamically typed languages. Most programmers that come from statically typed languages (such as C++ or C#) ignore their existence and make their life unnecessarily more difficult. This datatype is generally not present in such languages (or only in limited form).

Słowniki mogą mapować dowolne wartości na dowolne inne wartości, bez względu na to, czy są one kluczem czy wartością. Wbrew powszechnemu przekonaniu są one efektywne, ponieważ można je realizować za pomocą haszowanych tablic. W rzeczywistości są one tak wydajne, że niektóre języki implementują tablice jako słowniki.

Przykład słownika:

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

Słowniki są również dynamiczne, klucze mogą być dodawane lub usuwane w dowolnym momencie niewielkim kosztem:

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

W większości przypadków za pomocą słowników można łatwiej zaimplementować tablice dwuwymiarowe. Oto prosty przykład gry w statki:

# 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 pos
        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))

Dictionaries can also be used as data markup or quick structures. While GDScript’s dictionaries resemble python dictionaries, it also supports Lua style syntax and indexing, which makes it useful for writing initial states and quick structs:

# 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 i while

Iterowanie w niektórych językach statycznych może być złożone:

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

Zazwyczaj jest to znacznie uproszczone w dynamicznie pisanych językach:

for s in strings:
    print(s)

Kontenerowe typy danych (tablice i słowniki) są iteracyjne. Słowniki umożliwiają iterowanie kluczy:

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

Iterating with indices is also possible:

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

Funkcja range() może przyjmować 3 argumenty:

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

Some statically typed programming language examples:

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

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

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

Przetłumacz na:

for i in range(10):
    pass

for i in range(5, 10):
    pass

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

Odliczanie od tyłu jest realizowane poprzez używanie ujemnego indeksu:

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

Staje się:

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

While

Pętle while() są wszędzie takie same:

var i = 0

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

Niestandardowe iteratory

You can create custom iterators in case the default ones don’t quite meet your needs by overriding the Variant class’s _iter_init, _iter_next, and _iter_get functions in your script. An example implementation of a forward iterator follows:

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

I może być używany jak każdy inny iterator:

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

Upewnić się, że w _iter_init zresetowany został stan iteratora, w przeciwnym razie, zagnieżdżone pętle, w których używane są niestandardowe iteratory, nie będą działać zgodnie z oczekiwaniami.

Duck typing

One of the most difficult concepts to grasp when moving from a statically typed language to a dynamic one is duck typing. Duck typing makes overall code design much simpler and straightforward to write, but it’s not obvious how it works.

Jako przykład wyobraź sobie sytuację, w której duża skała opada w dół tunelu, rozbijając wszystko po drodze. Kod skały, w statycznie wpisanym języku, byłby czymś w rodzaju:

void BigRollingRock::on_object_hit(Smashable *entity) {

    entity->smash();
}

This way, everything that can be smashed by a rock would have to inherit Smashable. If a character, enemy, piece of furniture, small rock were all smashable, they would need to inherit from the class Smashable, possibly requiring multiple inheritance. If multiple inheritance was undesired, then they would have to inherit a common class like Entity. Yet, it would not be very elegant to add a virtual method smash() to Entity only if a few of them can be smashed.

W przypadku języków dynamicznie typowanych nie stanowi to problemu. Duck typing upewnia się że zdefiniowałeś jedynie funkcję smash() tam, gdzie jest to wymagane i to tyle. Nie trzeba brać pod uwagę dziedziczenia, klas podstawowych itp.

func _on_object_hit(object):
    object.smash()

And that’s it. If the object that hit the big rock has a smash() method, it will be called. No need for inheritance or polymorphism. Dynamically typed languages only care about the instance having the desired method or member, not what it inherits or the class type. The definition of Duck Typing should make this clearer:

„Kiedy widzę ptaka, który spaceruje jak kaczka i pływa jak kaczka i kwacze jak kaczka, nazywam go kaczką”

W tym przypadku można przetłumaczyć to na:

„Jeśli obiekt może zostać rozbity, nie przejmuj się tym, czym jest, po prostu go rozbij.”

Yes, we should call it Hulk typing instead.

It’s possible that the object being hit doesn’t have a smash() function. Some dynamically typed languages simply ignore a method call when it doesn’t exist (like Objective C), but GDScript is stricter, so checking if the function exists is desirable:

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

Następnie po prostu zdefiniuj tę metodę i zdefiniuj wszystko, co może zostać rozbite przez skałę.