Up to date

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

GDScript: Úvod do dynamických jazyků

O aplikaci

Tento návod má sloužit jako stručný návod, jak efektivněji používat GDScript. Zaměřuje se na běžné případy specifické pro tento jazyk, ale obsahuje také mnoho informací o dynamicky typovaných jazycích.

Má být užitečné zejména pro programátory s malými nebo žádnými předchozími zkušenostmi s dynamicky typovanými jazyky.

Dynamická povaha

Výhody a nevýhody dynamického typování

GDScript je dynamicky typovaný jazyk. Jeho hlavními výhodami jsou:

  • The language is easy to get started with.

  • Většinu kódu lze zapsat a změnit rychle a bez potíží.

  • Méně napsaného kódu znamená méně chyb a omylů k opravě.

  • The code is easy to read (little clutter).

  • Pro testování není nutná žádná kompilace.

  • Runtime je malinký.

  • It has duck-typing and polymorphism by nature.

Hlavními nevýhodami jsou:

  • Menší výkon než staticky typované jazyky.

  • More difficult to refactor (symbols can't be traced).

  • Některé chyby, které by ve staticky typovaných jazycích byly typicky odhaleny při kompilaci, se objeví až za běhu programu(protože parsování výrazů je přísnější).

  • Menší flexibilita při automatickém dokončování kódu (některé typy proměnných jsou známy až za běhu).

This, translated to reality, means that Godot used with GDScript is 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.

Proměnné a přiřazení

Všechny proměnné v dynamicky typovaném jazyce jsou "variantní". To znamená, že jejich typ není pevně stanoven a mění se pouze přiřazením. Příklad:

Statické:

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

Dynamické:

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

Jako argumenty funkce:

Funkce jsou také dynamické povahy, což znamená, že je lze volat s různými argumenty, například:

Statické:

void print_value(int value) {

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

[..]

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

Dynamické:

func print_value(value):
    print(value)

[..]

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

Ukazatele a odkazování:

Ve statických jazycích, jako jsou C nebo C++ (a do jisté míry i Java a C#), se rozlišuje mezi proměnnou a ukazatelem/odkazem na proměnnou. Druhá jmenovaná umožňuje modifikovat objekt jinými funkcemi předáním odkazu na původní objekt.

V jazycích C# nebo Java je vše, co není vestavěný typ (int, float, někdy String), vždy ukazatel nebo reference. Reference se také automaticky uvolňují, což znamená, že jsou vymazány, když se již nepoužívají. Dynamicky typované jazyky mají tendenci používat tento paměťový model také. Některé příklady:

  • 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, only base types (int, float, string and the vector types) are passed by value to functions (value is copied). Everything else (instances, arrays, dictionaries, etc) is passed as reference. Classes that inherit RefCounted (the default if nothing is specified) will be freed when not used, but manual memory management is allowed too if inheriting manually from Object.

Pole

Pole v dynamicky typovaných jazycích mohou obsahovat mnoho různých smíšených datových typů a jsou vždy dynamická (mohou kdykoli měnit velikost). Srovnejte například pole ve staticky typovaných jazycích:

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.

A v jazyce 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.

V dynamicky typovaných jazycích mohou pole sloužit i jako jiné datové typy, například seznamy:

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

Nebo neuspořádané množiny:

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

Slovníky

Slovníky jsou v dynamicky typovaných jazycích mocným nástrojem. Většina programátorů, kteří přišli ze staticky typovaných jazyků (jako je C++ nebo C#), jejich existenci ignoruje a zbytečně si tím komplikuje život. Tento datový typ se v takových jazycích zpravidla nevyskytuje (nebo jen v omezené podobě).

Slovníky mohou mapovat libovolnou hodnotu na libovolnou jinou hodnotu, přičemž se vůbec nehledí na datový typ použitý jako klíč nebo hodnota. Navzdory všeobecnému přesvědčení jsou efektivní, protože je lze implementovat pomocí hashovacích tabulek. Ve skutečnosti jsou tak efektivní, že některé jazyky jdou tak daleko, že implementují pole jako slovníky.

Příklad slovníku:

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

Slovníky jsou také dynamické, klíče lze kdykoli přidat nebo odebrat bez větších nároků na výkon:

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

In most cases, two-dimensional arrays can often be implemented more easily with dictionaries. Here's a battleship game example:

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

Slovníky lze použít také jako datové značky nebo rychlé struktury. Ačkoli se slovníky v jazyce GDScript podobají pythonovským slovníkům, podporují také syntaxi a indexování ve stylu jazyka Lua, což je užitečné pro zápis počátečních stavů a rychlých struktur:

# 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

Iterating using the C-style for loop in C-derived languages can be quite complex:

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

Because of this, GDScript makes the opinionated decision to have a for-in loop over iterables instead:

for s in strings:
    print(s)

Kontejnerové datové typy (pole a slovníky) jsou iterovatelné. Slovníky umožňují procházení podle klíčů:

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

Iterace pomocí indexů je také možná:

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

Funkce range() je schopna pobrat až 3 argumenty:

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.

Some examples involving C-style for loops:

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

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

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

Lze přeložit do:

for i in range(10):
    pass

for i in range(5, 10):
    pass

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

And backwards looping done through a negative counter:

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

Se stane:

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

While

Smyčky while() jsou všude stejné:

var i = 0

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

Vlastní iterátory

Pokud výchozí iterátory nevyhovují vašim potřebám, můžete si vytvořit vlastní iterátory tak, že ve svém skriptu přetížíte funkce _iter_init, _iter_next a _iter_get třídy Variant. Následuje příklad implementace dopředného iterátoru:

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

A lze jej použít jako jakýkoli jiný iterátor:

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

Ujistěte se, že jste resetovali stav iterátoru v _iter_init, jinak vnořené smyčky for, které používají vlastní iterátory, nebudou fungovat podle vašeho očekávání.

Kachní typování (Duck typing)

Jedním z nejobtížnějších konceptů, které je třeba pochopit při přechodu ze staticky typovaného jazyka na dynamický, je kachní typování. Díky kachnímu typování je celkový návrh kódu mnohem jednodušší a přímočařejší, ale na první pohled není zřetelné, jak to celé funguje.

Jako příklad si představte situaci, kdy do tunelu padá velký kámen a rozbíjí vše, co mu stojí v cestě. Kód pro kámen by ve staticky typovaném jazyce vypadal takto:

void BigRollingRock::on_object_hit(Smashable *entity) {

    entity->smash();
}

Tímto způsobem by vše, co lze rozbít kamenem, muselo zdědit Smashable. Pokud by postava, nepřítel, kus nábytku čí malý kámen byly rozbitné, musely by dědit od třídy Smashable, což by případně vyžadovalo vícenásobnou dědičnost. Pokud by vícenásobná dědičnost nebyla žádoucí, pak by musely dědit společnou třídu, jako je Entity. Přesto by nebylo příliš elegantní přidávat virtuální metodu smash() do Entity pouze pro případ, že jen některé Entity budou rozbitné.

V dynamicky typovaných jazycích to není problém. Duck typing zajišťuje, že stačí definovat funkci smash() pouze tam, kde je to nutné, a to je vše. Není třeba uvažovat o dědičnosti, bázových třídách atd.

func _on_object_hit(object):
    object.smash()

A to je vše. Pokud má objekt, který narazil do velkého kamene, metodu smash(), bude zavolána. Není třeba dědičnosti ani polymorfismu. Dynamicky typované jazyky se starají pouze o to, aby instance měla požadovanou metodu nebo člen, nikoli o to, co dědí, nebo o typ třídy. Definice kachního typování by to měla objasnit:

"Když vidím ptáka, který chodí jako kachna, plave jako kachna a kváká jako kachna, říkám mu kachna. "

V tomto případě to znamená:

"Pokud lze předmět rozbít, je jedno, co to je, prostě ho rozbij. "

Ano, měli bychom to nazývat Hulk typing.

Je možné, že zasažený objekt nemá funkci smash(). Některé dynamicky typované jazyky prostě ignorují volání metody, pokud neexistuje (například Objective C), ale GDScript je přísnější, takže je žádoucí kontrolovat, zda funkce existuje:

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

Pak stačí definovat tuto metodu a vše, čeho se kámen dotkne, může být rozbito.