Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

GDScript: Úvod do dynamických jazyků

Úvod

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íží.

  • 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ší).

  • Less flexibility for code-completion (some variable types are only known at runtime).

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

Dictionaries are a powerful tool in dynamically typed languages. In GDScript, untyped dictionaries can be used for many cases where a statically typed language would tend to use another data structure.

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

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

class ForwardIterator:
    var _start
    var _end
    var _increment

    func _init(start, end, increment):
        _start = start
        _end = end
        _increment = increment

    func _should_continue(current):
        return current < _end

    func _iter_init(iter):
        # Initialize the state to store the current value.
        iter[0] = _start
        return _should_continue(iter[0])

    func _iter_next(iter):
        iter[0] += _increment
        return _should_continue(iter[0])

    func _iter_get(iter):
        # The state is not wrapped in an array for `_iter_get()`.
        # The iteration value is the same as the state.
        return iter

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.

It is possible but discouraged to store the state in a member variable. Multiple states are necessary in cases such as nested loops where the same iterator instance is used simultaneously. The iter parameter in _iter_init() and _iter_next() is a single-element array so that updates can persist. Whereas in _iter_get(), the state is is not wrapped because it is supposed to be read-only.

Returning true from _iter_init() and _iter_next() indicates that the iterator is valid. Returning false will terminate the loop.

For more details see _iter_init(), _iter_next(), and _iter_get().

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

Then, define that method and anything the rock touches can be smashed.